2023-01-03 17:19:20 -05:00
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
2020-05-14 17:06:23 -07:00
package provider
import (
2020-05-29 09:13:31 -07:00
"context"
2020-07-12 15:09:40 -07:00
"errors"
2020-09-15 19:56:43 -04:00
"fmt"
2022-10-17 11:13:45 -07:00
"net/http"
2020-09-15 19:56:43 -04:00
"strings"
2020-05-29 09:13:31 -07:00
2020-08-13 11:18:36 -07:00
"github.com/hashicorp/boundary/api"
"github.com/hashicorp/boundary/api/authmethods"
2020-09-15 19:56:43 -04:00
"github.com/hashicorp/boundary/sdk/wrapper"
2022-05-13 16:30:40 -07:00
wrapping "github.com/hashicorp/go-kms-wrapping/v2"
"github.com/hashicorp/go-secure-stdlib/configutil/v2"
"github.com/hashicorp/go-secure-stdlib/pluginutil/v2"
2020-09-15 19:56:43 -04:00
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
2022-07-27 16:08:17 -04:00
kms_plugin_assets "github.com/hashicorp/terraform-provider-boundary/plugins/kms"
2020-05-14 17:06:23 -07:00
)
2023-04-28 13:13:11 -04:00
const (
PASSWORD_AUTH_METHOD_PREFIX = "ampw"
2023-05-03 14:33:05 -04:00
LDAP_AUTH_METHOD_PREFIX = "amldap"
2023-04-28 13:13:11 -04:00
DEFAULT_PROVIDER_SCOPE = "global"
)
2020-10-06 20:34:16 -04:00
func init () {
// descriptions are written in markdown for docs
schema . DescriptionKind = schema . StringMarkdown
}
2020-09-15 19:56:43 -04:00
func New () * schema . Provider {
2020-05-29 09:13:31 -07:00
p := & schema . Provider {
Schema : map [ string ] * schema . Schema {
2020-09-15 19:56:43 -04:00
"addr" : {
Type : schema . TypeString ,
Required : true ,
2021-01-14 14:07:36 -08:00
Description : `The base url of the Boundary API, e.g. "http://127.0.0.1:9200". If not set, it will be read from the "BOUNDARY_ADDR" env var.` ,
2020-09-15 19:56:43 -04:00
},
"token" : {
2020-05-29 09:13:31 -07:00
Type : schema . TypeString ,
Optional : true ,
2020-09-15 19:56:43 -04:00
Description : `The Boundary token to use, as a string or path on disk containing just the string. If set, the token read here will be used in place of authenticating with the auth method specified in "auth_method_id", although the recovery KMS mechanism will still override this. Can also be set with the BOUNDARY_TOKEN environment variable.` ,
2020-05-29 09:13:31 -07:00
},
2020-09-15 19:56:43 -04:00
"recovery_kms_hcl" : {
2020-05-29 09:13:31 -07:00
Type : schema . TypeString ,
2020-09-15 19:56:43 -04:00
Optional : true ,
2020-10-13 11:02:52 -07:00
Description : "Can be a heredoc string or a path on disk. If set, the string/file will be parsed as HCL and used with the recovery KMS mechanism. While this is set, it will override any other authentication information; the KMS mechanism will always be used. See Boundary's KMS docs for examples: https://boundaryproject.io/docs/configuration/kms" ,
2020-05-29 09:13:31 -07:00
},
2020-07-12 15:09:40 -07:00
"auth_method_id" : {
Type : schema . TypeString ,
2020-09-15 19:56:43 -04:00
Optional : true ,
2023-05-16 15:47:43 -04:00
Description : "The auth method ID e.g. ampw_1234567890. If not set, the default auth method for the given scope ID will be used." ,
2020-07-12 15:09:40 -07:00
},
2020-09-15 19:56:43 -04:00
"password_auth_method_login_name" : {
2020-07-12 15:09:40 -07:00
Type : schema . TypeString ,
2020-09-15 19:56:43 -04:00
Optional : true ,
Description : "The auth method login name for password-style auth methods" ,
2023-05-03 14:33:05 -04:00
Deprecated : "Use auth_method_login_name instead" ,
2020-07-12 15:09:40 -07:00
},
2020-09-15 19:56:43 -04:00
"password_auth_method_password" : {
2020-07-12 15:09:40 -07:00
Type : schema . TypeString ,
2020-09-15 19:56:43 -04:00
Optional : true ,
Description : "The auth method password for password-style auth methods" ,
2023-05-03 14:33:05 -04:00
Deprecated : "Use auth_method_password instead" ,
},
"auth_method_login_name" : {
Type : schema . TypeString ,
Optional : true ,
Description : "The auth method login name for password-style or ldap-style auth methods" ,
},
"auth_method_password" : {
Type : schema . TypeString ,
Optional : true ,
Description : "The auth method password for password-style or ldap-style auth methods" ,
2020-07-12 15:09:40 -07:00
},
2021-12-18 05:24:09 +01:00
"tls_insecure" : {
Type : schema . TypeBool ,
Optional : true ,
Description : "When set to true, does not validate the Boundary API endpoint certificate" ,
},
2022-10-03 17:03:14 -07:00
"plugin_execution_dir" : {
Type : schema . TypeString ,
Optional : true ,
Description : `Specifies a directory that the Boundary provider can use to write and execute its built-in plugins.` ,
},
2023-04-28 13:13:11 -04:00
"scope_id" : {
Type : schema . TypeString ,
Optional : true ,
Description : `The scope ID for the default auth method.` ,
},
2020-05-14 17:06:23 -07:00
},
ResourcesMap : map [ string ] * schema . Resource {
2023-02-09 16:02:15 +00:00
"boundary_account" : resourceAccount (),
"boundary_account_password" : resourceAccountPassword (),
"boundary_account_oidc" : resourceAccountOidc (),
2023-03-15 14:30:06 -04:00
"boundary_account_ldap" : resourceAccountLdap (),
2023-02-09 16:02:15 +00:00
"boundary_auth_method" : resourceAuthMethod (),
"boundary_auth_method_password" : resourceAuthMethodPassword (),
"boundary_auth_method_oidc" : resourceAuthMethodOidc (),
2023-03-15 14:30:06 -04:00
"boundary_auth_method_ldap" : resourceAuthMethodLdap (),
2023-02-09 16:02:15 +00:00
"boundary_credential_library_vault" : resourceCredentialLibraryVault (),
"boundary_credential_library_vault_ssh_certificate" : resourceCredentialLibraryVaultSshCertificate (),
"boundary_credential_store_vault" : resourceCredentialStoreVault (),
"boundary_credential_store_static" : resourceCredentialStoreStatic (),
"boundary_credential_username_password" : resourceCredentialUsernamePassword (),
"boundary_credential_ssh_private_key" : resourceCredentialSshPrivateKey (),
"boundary_credential_json" : resourceCredentialJson (),
"boundary_managed_group" : resourceManagedGroup (),
2023-03-15 14:30:06 -04:00
"boundary_managed_group_ldap" : resourceManagedGroupLdap (),
2023-02-09 16:02:15 +00:00
"boundary_group" : resourceGroup (),
"boundary_host" : resourceHost (),
"boundary_host_static" : resourceHostStatic (),
"boundary_host_catalog" : resourceHostCatalog (),
"boundary_host_catalog_static" : resourceHostCatalogStatic (),
"boundary_host_catalog_plugin" : resourceHostCatalogPlugin (),
"boundary_host_set" : resourceHostSet (),
"boundary_host_set_static" : resourceHostSetStatic (),
"boundary_host_set_plugin" : resourceHostSetPlugin (),
"boundary_role" : resourceRole (),
"boundary_scope" : resourceScope (),
2023-06-28 10:38:22 -04:00
"boundary_storage_bucket" : resourceStorageBucket (),
2023-02-09 16:02:15 +00:00
"boundary_target" : resourceTarget (),
"boundary_user" : resourceUser (),
"boundary_worker" : resourceWorker (),
2020-05-14 17:06:23 -07:00
},
2023-10-12 02:21:48 +11:00
DataSourcesMap : map [ string ] * schema . Resource {
"boundary_scope" : dataSourceScope (),
2023-10-17 19:36:51 -04:00
"boundary_user" : dataSourceUser (),
2023-10-12 02:21:48 +11:00
},
2020-05-14 17:06:23 -07:00
}
2020-05-29 09:13:31 -07:00
2020-09-15 19:56:43 -04:00
p . ConfigureContextFunc = providerConfigure ( p )
2020-05-29 09:13:31 -07:00
return p
}
type metaData struct {
2020-09-15 19:56:43 -04:00
client * api . Client
recoveryKmsWrapper wrapping . Wrapper
2020-05-29 09:13:31 -07:00
}
2020-09-15 19:56:43 -04:00
func providerAuthenticate ( ctx context . Context , d * schema . ResourceData , md * metaData ) error {
var credentials map [ string ] interface {}
2023-04-28 13:13:11 -04:00
amClient := authmethods . NewClient ( md . client )
var providerScope string
scopeId , scopeIdOk := d . GetOk ( "scope_id" )
switch {
case scopeIdOk :
providerScope = scopeId .( string )
default :
providerScope = DEFAULT_PROVIDER_SCOPE
}
2020-07-12 15:09:40 -07:00
2020-09-15 19:56:43 -04:00
recoveryKmsHcl , recoveryKmsHclOk := d . GetOk ( "recovery_kms_hcl" )
if token , ok := d . GetOk ( "token" ); ok {
md . client . SetToken ( token .( string ))
2020-07-12 15:09:40 -07:00
}
2023-05-12 10:56:58 -04:00
// If auth_method_id is not set, get the default auth method ID for the given scope ID.
// Skip fetching default auth method when recovery_kms_hcl is set or the client token isn't null.
2023-04-28 13:13:11 -04:00
authMethodId , authMethodIdOk := d . GetOk ( "auth_method_id" )
2023-05-12 10:56:58 -04:00
if ! authMethodIdOk && ! recoveryKmsHclOk && md . client . Token () == "" {
2023-05-03 14:33:05 -04:00
defaultAuthMethodId , err := getDefaultAuthMethodId ( ctx , amClient , providerScope , "" )
2023-04-28 13:13:11 -04:00
if err != nil {
return err
}
authMethodIdOk = true
authMethodId = defaultAuthMethodId
}
2020-09-15 19:56:43 -04:00
switch {
case recoveryKmsHclOk :
recoveryHclStr , _ , err := ReadPathOrContents ( recoveryKmsHcl .( string ))
if err != nil {
return fmt . Errorf ( `error reading data from "recovery_kms_hcl": %v` , err )
}
2022-10-03 17:03:14 -07:00
opts := [] pluginutil . Option {
pluginutil . WithPluginsMap ( kms_plugin_assets . BuiltinKmsPlugins ()),
pluginutil . WithPluginsFilesystem ( kms_plugin_assets . KmsPluginPrefix , kms_plugin_assets . FileSystem ()),
}
if execDir , ok := d . GetOk ( "plugin_execution_dir" ); ok {
opts = append ( opts , pluginutil . WithPluginExecutionDirectory ( execDir .( string )))
}
2022-10-12 10:53:08 -07:00
// TODO: cleanup plugin when finished
wrapper , _ , err := wrapper . GetWrapperFromHcl (
2022-10-03 17:03:14 -07:00
ctx ,
recoveryHclStr ,
"recovery" ,
configutil . WithPluginOptions ( opts ... ))
2020-09-15 19:56:43 -04:00
if err != nil {
return fmt . Errorf ( `error reading wrappers from "recovery_kms_hcl": %v` , err )
}
if wrapper == nil {
return errors . New ( `No "kms" block with purpose "recovery" found in "recovery_kms_hcl"` )
}
md . recoveryKmsWrapper = wrapper
md . client . SetRecoveryKmsWrapper ( wrapper )
return nil
case md . client . Token () != "" :
// Use the token sourced from the conf file or env var
case authMethodIdOk :
switch {
2023-05-03 14:33:05 -04:00
case strings . HasPrefix ( authMethodId .( string ), PASSWORD_AUTH_METHOD_PREFIX ) || strings . HasPrefix ( authMethodId .( string ), LDAP_AUTH_METHOD_PREFIX ):
// Password-style & LDAP-style
authMethodLoginName , ok := d . GetOk ( "auth_method_login_name" )
2020-09-15 19:56:43 -04:00
if ! ok {
2023-05-03 14:33:05 -04:00
authMethodLoginName , ok = d . GetOk ( "password_auth_method_login_name" )
if ! ok {
return errors . New ( "auth method login name not set, please set auth_method_login_name on the provider" )
}
2020-09-15 19:56:43 -04:00
}
2023-05-03 14:33:05 -04:00
authMethodPassword , ok := d . GetOk ( "auth_method_password" )
2020-09-15 19:56:43 -04:00
if ! ok {
2023-05-03 14:33:05 -04:00
authMethodPassword , ok = d . GetOk ( "password_auth_method_password" )
if ! ok {
return errors . New ( "auth method password not set, please set auth_method_password on the provider" )
}
2020-09-15 19:56:43 -04:00
}
credentials = map [ string ] interface {}{
"login_name" : authMethodLoginName ,
"password" : authMethodPassword ,
}
2023-04-28 13:13:11 -04:00
case strings . HasPrefix ( authMethodId .( string ), "amoidc" ):
// OIDC-style
return errors . New ( "OIDC auth method is currently not supported by Boundary Terraform Provider. only password auth method is supported at this time" )
2020-09-15 19:56:43 -04:00
default :
return errors . New ( "no suitable typed auth method information found" )
}
2020-07-12 15:09:40 -07:00
2023-04-28 13:13:11 -04:00
at , err := amClient . Authenticate ( ctx , authMethodId .( string ), "login" , credentials )
2020-09-15 19:56:43 -04:00
if err != nil {
2022-10-17 11:13:45 -07:00
if apiErr := api . AsServerError ( err ); apiErr != nil {
statusCode := apiErr . Response (). StatusCode ()
if statusCode == http . StatusNotFound {
return fmt . Errorf ( "unknown auth_method_id: %s" , err . Error ())
}
if statusCode == http . StatusUnauthorized {
return fmt . Errorf ( "invalid login name or password: %s" , err . Error ())
}
}
2020-09-15 19:56:43 -04:00
return err
}
2021-04-28 18:22:18 -04:00
md . client . SetToken ( at . Attributes [ "token" ].( string ))
2020-09-15 19:56:43 -04:00
default :
return errors . New ( "no suitable auth method information found" )
2020-07-12 15:09:40 -07:00
}
2020-09-15 19:56:43 -04:00
return nil
2020-07-12 15:09:40 -07:00
}
2020-09-15 19:56:43 -04:00
func providerConfigure ( p * schema . Provider ) schema . ConfigureContextFunc {
return func ( ctx context . Context , d * schema . ResourceData ) ( interface {}, diag . Diagnostics ) {
2020-05-29 09:13:31 -07:00
client , err := api . NewClient ( nil )
if err != nil {
2020-09-15 19:56:43 -04:00
return nil , diag . FromErr ( err )
2020-05-29 09:13:31 -07:00
}
2020-08-15 14:49:09 -07:00
2020-09-15 19:56:43 -04:00
if url , ok := d . GetOk ( "addr" ); ok {
2021-04-28 18:22:18 -04:00
if err := client . SetAddr ( url .( string )); err != nil {
return nil , diag . FromErr ( err )
}
2020-09-15 19:56:43 -04:00
}
if client . Addr () == "" {
return nil , diag . Errorf ( `"no valid address could be determined from "addr" or "BOUNDARY_ADDR" env var` )
2020-05-29 09:13:31 -07:00
}
2021-12-18 05:24:09 +01:00
if tlsInsecure , ok := d . GetOk ( "tls_insecure" ); ok {
if client . SetTLSConfig ( & api . TLSConfig { Insecure : tlsInsecure .( bool )}) != nil {
return nil , diag . Errorf ( "could not set insecure tls" )
}
}
2020-05-29 09:13:31 -07:00
client . SetLimiter ( 5 , 5 )
2020-09-15 19:56:43 -04:00
md := & metaData {
client : client ,
2020-07-12 15:09:40 -07:00
}
2020-09-15 19:56:43 -04:00
if err := providerAuthenticate ( ctx , d , md ); err != nil {
return nil , diag . FromErr ( err )
}
return md , nil
2020-05-29 09:13:31 -07:00
}
2020-05-14 17:06:23 -07:00
}
2023-04-28 13:13:11 -04:00
// getDefaultAuthMethodId iterates over boundary client.List() to find the default auth method ID for the given scopeId.
// If there is only one auth method, it'll return it even if it's not the primary auth method
// If scope ID is empty or no primary auth method is found, it returns an error.
func getDefaultAuthMethodId ( ctx context . Context , client * authmethods . Client , scopeId , amType string ) ( string , error ) {
if scopeId == "" {
return "" , fmt . Errorf ( "must pass a non empty scope ID string to get default auth method ID" )
}
authMethodListResult , err := client . List ( ctx , scopeId )
if err != nil {
return "" , err
}
authMethodItems := authMethodListResult . GetItems ()
// If there is only one auth method that matches the auth method prefix, return it even if it's not the primary auth method
if len ( authMethodItems ) == 1 {
authMethod := authMethodItems [ 0 ]
if ! strings . HasPrefix ( authMethod . Id , amType ) {
return "" , fmt . Errorf ( "error looking up default auth method for scope ID: '%s'. got '%s' but the provider requires an auth method prefix of '%s'" , scopeId , authMethod . Id , amType )
}
return authMethod . Id , nil
}
// find the primary auth method that matches auth method prefix
for _ , m := range authMethodItems {
if m . IsPrimary {
if ! strings . HasPrefix ( m . Id , amType ) {
return "" , fmt . Errorf ( "error looking up primary auth method for scope ID: '%s'. got '%s' but the provider requires an auth method prefix of '%s'" , scopeId , m . Id , amType )
}
return m . Id , nil
}
}
return "" , fmt . Errorf ( "default auth method not found for scope ID: '%s'. Please set a primary auth method on this scope or pass one explicitly using the `auth_method_id` field" , scopeId )
}