Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: Use default auth method if no auth method ID is provided for provider #385

Merged
merged 3 commits into from
Apr 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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!
14 changes: 14 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
```

<!-- schema generated by tfplugindocs -->
Expand All @@ -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.
1 change: 1 addition & 0 deletions docs/resources/credential_store_vault.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions docs/resources/target.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions examples/provider/provider.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
77 changes: 72 additions & 5 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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
elimt marked this conversation as resolved.
Show resolved Hide resolved
authMethodId, authMethodIdOk := d.GetOk("auth_method_id")
if !authMethodIdOk {
defaultAuthMethodId, err := getDefaultAuthMethodId(ctx, amClient, providerScope, PASSWORD_AUTH_METHOD_PREFIX)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we want to support ldap as well?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think we can add support for that as well. A separate PR where we add LDAP support and also use the default primary method for that.

if err != nil {
return err
}
authMethodIdOk = true
authMethodId = defaultAuthMethodId
}

switch {
case recoveryKmsHclOk:
recoveryHclStr, _, err := ReadPathOrContents(recoveryKmsHcl.(string))
Expand Down Expand Up @@ -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"):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we want to allow ldap auth methods in this switch?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think we can add support for that as well. A separate PR where we add LDAP support and also use the default primary method for that.

// 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()
Expand Down Expand Up @@ -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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to support ldap auth methods in this func?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think we can add support for that as well. A separate PR where we add LDAP support and also use the default primary method for that.

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)
}
106 changes: 106 additions & 0 deletions internal/provider/provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"crypto/rand"
"encoding/base64"
"fmt"
"regexp"
"strings"
"testing"

Expand All @@ -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 (
Expand Down Expand Up @@ -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" {
Expand Down Expand Up @@ -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
}
}