diff --git a/CHANGELOG.md b/CHANGELOG.md index 252af231..21fdac30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ Canonical reference for changes, improvements, and bugfixes for the Boundary Ter ([PR](https://github.com/hashicorp/terraform-provider-boundary/pull/375)) * Add support for target default client port ([PR])(https://github.com/hashicorp/terraform-provider-boundary/pull/379)) +* Add support for using default auth method if no auth method ID is provided for provider + ([PR])(https://github.com/hashicorp/terraform-provider-boundary/pull/385)) ### Bug Fixes @@ -232,4 +234,4 @@ Update provider to handle new domain errors ([PR](https://github.com/hashicorp/t ## 0.1.0 (October 14, 2020) -Initial release! +Initial release! \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index 25b6e57b..0820462d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -20,6 +20,19 @@ provider "boundary" { password_auth_method_login_name = "myuser" # changeme password_auth_method_password = "passpass" # changeme } + +provider "boundary" { + addr = "http://127.0.0.1:9200" + password_auth_method_login_name = "myuser" + password_auth_method_password = "passpass" +} + +provider "boundary" { + addr = "http://127.0.0.1:9200" + password_auth_method_login_name = "myuser" + password_auth_method_password = "passpass" + scope_id = "s_1234567890" +} ``` @@ -36,5 +49,6 @@ provider "boundary" { - `password_auth_method_password` (String) The auth method password for password-style auth methods - `plugin_execution_dir` (String) Specifies a directory that the Boundary provider can use to write and execute its built-in plugins. - `recovery_kms_hcl` (String) 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 +- `scope_id` (String) The scope ID for the default auth method. - `tls_insecure` (Boolean) When set to true, does not validate the Boundary API endpoint certificate - `token` (String) 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. \ No newline at end of file diff --git a/docs/resources/credential_store_vault.md b/docs/resources/credential_store_vault.md index 976810c6..e7db077a 100644 --- a/docs/resources/credential_store_vault.md +++ b/docs/resources/credential_store_vault.md @@ -56,6 +56,7 @@ resource "boundary_credential_store_vault" "example" { - `namespace` (String) The namespace within Vault to use. - `tls_server_name` (String) Name to use as the SNI host when connecting to Vault via TLS. - `tls_skip_verify` (Boolean) Whether or not to skip TLS verification. +- `worker_filter` (String) HCP Only. A filter used to control which PKI workers can handle Vault requests. This allows the use of private Vault instances with Boundary. ### Read-Only diff --git a/docs/resources/target.md b/docs/resources/target.md index f87d3aa7..37a4f1bb 100644 --- a/docs/resources/target.md +++ b/docs/resources/target.md @@ -134,6 +134,7 @@ resource "boundary_target" "address_foo" { - `address` (String) Optionally, a valid network address to connect to for this target. Cannot be used alongside host_source_ids. - `brokered_credential_source_ids` (Set of String) A list of brokered credential source ID's. +- `default_client_port` (Number) The default client port for this target. - `default_port` (Number) The default port for this target. - `description` (String) The target description. - `egress_worker_filter` (String) Boolean expression to filter the workers used to access this target diff --git a/examples/provider/provider.tf b/examples/provider/provider.tf index 88d5afb0..265cfd9a 100644 --- a/examples/provider/provider.tf +++ b/examples/provider/provider.tf @@ -4,3 +4,16 @@ provider "boundary" { password_auth_method_login_name = "myuser" # changeme password_auth_method_password = "passpass" # changeme } + +provider "boundary" { + addr = "http://127.0.0.1:9200" + password_auth_method_login_name = "myuser" + password_auth_method_password = "passpass" +} + +provider "boundary" { + addr = "http://127.0.0.1:9200" + password_auth_method_login_name = "myuser" + password_auth_method_password = "passpass" + scope_id = "s_1234567890" +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 7ae1a411..65687f3a 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -21,6 +21,11 @@ import ( kms_plugin_assets "github.com/hashicorp/terraform-provider-boundary/plugins/kms" ) +const ( + PASSWORD_AUTH_METHOD_PREFIX = "ampw" + DEFAULT_PROVIDER_SCOPE = "global" +) + func init() { // descriptions are written in markdown for docs schema.DescriptionKind = schema.StringMarkdown @@ -69,6 +74,11 @@ func New() *schema.Provider { Optional: true, Description: `Specifies a directory that the Boundary provider can use to write and execute its built-in plugins.`, }, + "scope_id": { + Type: schema.TypeString, + Optional: true, + Description: `The scope ID for the default auth method.`, + }, }, ResourcesMap: map[string]*schema.Resource{ "boundary_account": resourceAccount(), @@ -117,13 +127,33 @@ type metaData struct { func providerAuthenticate(ctx context.Context, d *schema.ResourceData, md *metaData) error { var credentials map[string]interface{} + 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 + } - authMethodId, authMethodIdOk := d.GetOk("auth_method_id") recoveryKmsHcl, recoveryKmsHclOk := d.GetOk("recovery_kms_hcl") if token, ok := d.GetOk("token"); ok { md.client.SetToken(token.(string)) } + // If auth_method_id is not set, get the default auth method ID for the given scope ID + authMethodId, authMethodIdOk := d.GetOk("auth_method_id") + if !authMethodIdOk { + defaultAuthMethodId, err := getDefaultAuthMethodId(ctx, amClient, providerScope, PASSWORD_AUTH_METHOD_PREFIX) + if err != nil { + return err + } + authMethodIdOk = true + authMethodId = defaultAuthMethodId + } + switch { case recoveryKmsHclOk: recoveryHclStr, _, err := ReadPathOrContents(recoveryKmsHcl.(string)) @@ -176,14 +206,14 @@ func providerAuthenticate(ctx context.Context, d *schema.ResourceData, md *metaD "login_name": authMethodLoginName, "password": authMethodPassword, } - + 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") default: return errors.New("no suitable typed auth method information found") } - am := authmethods.NewClient(md.client) - - at, err := am.Authenticate(ctx, authMethodId.(string), "login", credentials) + at, err := amClient.Authenticate(ctx, authMethodId.(string), "login", credentials) if err != nil { if apiErr := api.AsServerError(err); apiErr != nil { statusCode := apiErr.Response().StatusCode() @@ -240,3 +270,40 @@ func providerConfigure(p *schema.Provider) schema.ConfigureContextFunc { return md, nil } } + +// 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) +} diff --git a/internal/provider/provider_test.go b/internal/provider/provider_test.go index ff82d696..0791ed40 100644 --- a/internal/provider/provider_test.go +++ b/internal/provider/provider_test.go @@ -8,6 +8,7 @@ import ( "crypto/rand" "encoding/base64" "fmt" + "regexp" "strings" "testing" @@ -16,6 +17,7 @@ import ( "github.com/hashicorp/go-kms-wrapping/v2/aead" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) var ( @@ -98,6 +100,42 @@ provider "boundary" { return strings.Join(c, "\n") } +func testConfigWithDefaultAuthMethod(url string, res ...string) string { + provider := fmt.Sprintf(` +provider "boundary" { + addr = "%s" + password_auth_method_login_name = "%s" + password_auth_method_password = "%s" +}`, url, tcLoginName, tcPassword) + + c := []string{provider} + c = append(c, res...) + return strings.Join(c, "\n") +} + +func testConfigWithOIDCAuthMethod(url string, res ...string) string { + provider := fmt.Sprintf(` +provider "boundary" { + addr = "%s" + auth_method_id = "amoidc_0000000000" +}`, url) + + c := []string{provider} + c = append(c, res...) + return strings.Join(c, "\n") +} + +func testConfigWithoutAMPWCredentials(url string, res ...string) string { + provider := fmt.Sprintf(` +provider "boundary" { + addr = "%s" +}`, url) + + c := []string{provider} + c = append(c, res...) + return strings.Join(c, "\n") +} + func testConfigWithRecovery(url string, res ...string) string { provider := fmt.Sprintf(` provider "boundary" { @@ -139,3 +177,71 @@ func TestProvider(t *testing.T) { t.Fatalf("err: %s", err) } } + +func TestConfigWithDefaultAuthMethod(t *testing.T) { + tc := controller.NewTestController(t, tcConfig...) + defer tc.Shutdown() + url := tc.ApiAddrs()[0] + + var provider *schema.Provider + resource.Test(t, resource.TestCase{ + IsUnitTest: true, + ProviderFactories: providerFactories(&provider), + CheckDestroy: testAccCheckScopeResourceDestroy(t, provider), + Steps: []resource.TestStep{ + { + Config: testConfigWithDefaultAuthMethod(url, fooOrg, firstProjectFoo, secondProject), + Check: resource.ComposeTestCheckFunc( + testAccCheckScopeResourceExists(provider, "boundary_scope.org1"), + testProviderTokenExists(provider), + ), + }, + }, + }) +} + +func TestConfigWithoutAMPWCredentials(t *testing.T) { + tc := controller.NewTestController(t, tcConfig...) + defer tc.Shutdown() + url := tc.ApiAddrs()[0] + + var provider *schema.Provider + resource.Test(t, resource.TestCase{ + IsUnitTest: true, + ProviderFactories: providerFactories(&provider), + Steps: []resource.TestStep{ + { + Config: testConfigWithoutAMPWCredentials(url, fooOrg, firstProjectFoo, secondProject), + ExpectError: regexp.MustCompile("password-style auth method login name not set, please set password_auth_method_login_name on the provider"), + }, + }, + }) +} + +func TestConfigWithOIDCAuthMethod(t *testing.T) { + tc := controller.NewTestController(t, tcConfig...) + defer tc.Shutdown() + url := tc.ApiAddrs()[0] + + var provider *schema.Provider + resource.Test(t, resource.TestCase{ + IsUnitTest: true, + ProviderFactories: providerFactories(&provider), + Steps: []resource.TestStep{ + { + Config: testConfigWithOIDCAuthMethod(url, fooOrg, firstProjectFoo, secondProject), + ExpectError: regexp.MustCompile("OIDC auth method is currently not supported by Boundary Terraform Provider. only password auth method is supported at this time"), + }, + }, + }) +} + +func testProviderTokenExists(testProvider *schema.Provider) resource.TestCheckFunc { + return func(s *terraform.State) error { + md := testProvider.Meta().(*metaData) + if md.client.Token() == "" { + return fmt.Errorf("token not set") + } + return nil + } +}