Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
7 changes: 7 additions & 0 deletions docs/resources/auth_method.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ resource "boundary_auth_method" "password" {
scope_id = boundary_scope.org.id
type = "password"
}

resource "boundary_auth_method" "password_is_primary" {
scope_id = boundary_scope.org.id
type = "password"
is_primary_for_scope = true
}
```

<!-- schema generated by tfplugindocs -->
Expand All @@ -38,6 +44,7 @@ resource "boundary_auth_method" "password" {
### Optional

- `description` (String) The auth method description.
- `is_primary_for_scope` (Boolean) When true, makes this auth method the primary auth method for the scope in which it resides.
- `min_login_name_length` (Number, Deprecated) The minimum login name length.
- `min_password_length` (Number, Deprecated) The minimum password length.
- `name` (String) The auth method name. Defaults to the resource name.
Expand Down
32 changes: 31 additions & 1 deletion docs/resources/auth_method_password.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,26 @@ description: |-

The auth method resource allows you to configure a Boundary auth_method_password.


## Example Usage

```terraform
resource "boundary_scope" "org" {
name = "organization_one"
description = "My first scope!"
scope_id = "global"
auto_create_admin_role = true
auto_create_default_role = true
}

resource "boundary_auth_method_password" "password" {
scope_id = boundary_scope.org.id
}

resource "boundary_auth_method_password" "password_is_primary" {
scope_id = boundary_scope.org.id
is_primary_for_scope = true
}
```

<!-- schema generated by tfplugindocs -->
## Schema
Expand All @@ -22,6 +41,7 @@ The auth method resource allows you to configure a Boundary auth_method_password
### Optional

- `description` (String) The auth method description.
- `is_primary_for_scope` (Boolean) When true, makes this auth method the primary auth method for the scope in which it resides.
- `min_login_name_length` (Number) The minimum login name length.
- `min_password_length` (Number) The minimum password length.
- `name` (String) The auth method name. Defaults to the resource name.
Expand All @@ -30,3 +50,13 @@ The auth method resource allows you to configure a Boundary auth_method_password
### Read-Only

- `id` (String) The ID of the account.

## Import

Import is supported using the following syntax:

The [`terraform import` command](https://developer.hashicorp.com/terraform/cli/commands/import) can be used, for example:

```shell
terraform import boundary_auth_method_password.foo <my-id>
```
6 changes: 6 additions & 0 deletions examples/resources/boundary_auth_method/resource.tf
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,9 @@ resource "boundary_auth_method" "password" {
scope_id = boundary_scope.org.id
type = "password"
}

resource "boundary_auth_method" "password_is_primary" {
scope_id = boundary_scope.org.id
type = "password"
is_primary_for_scope = true
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
terraform import boundary_auth_method_password.foo <my-id>
16 changes: 16 additions & 0 deletions examples/resources/boundary_auth_method_password/resource.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
resource "boundary_scope" "org" {
name = "organization_one"
description = "My first scope!"
scope_id = "global"
auto_create_admin_role = true
auto_create_default_role = true
}

resource "boundary_auth_method_password" "password" {
scope_id = boundary_scope.org.id
}

resource "boundary_auth_method_password" "password_is_primary" {
scope_id = boundary_scope.org.id
is_primary_for_scope = true
}
74 changes: 69 additions & 5 deletions internal/provider/resource_auth_method.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ func resourceAuthMethod() *schema.Resource {
Computed: true,
Deprecated: "Will be removed in favor of using attributes parameter",
},
authmethodIsPrimaryAuthMethodForScopeKey: {
Description: "When true, makes this auth method the primary auth method for the scope in which it resides.",
Type: schema.TypeBool,
Optional: true,
},
},
}
}
Expand All @@ -91,6 +96,10 @@ func setFromAuthMethodResponseMap(d *schema.ResourceData, raw map[string]interfa
return err
}

if p, ok := raw[authmethodIsPrimaryAuthMethodForScopeKey]; ok {
d.Set(authmethodIsPrimaryAuthMethodForScopeKey, p.(bool))
}

switch raw["type"].(string) {
case authmethodTypePassword:
if attrsVal, ok := raw["attributes"]; ok {
Expand Down Expand Up @@ -160,6 +169,19 @@ func resourceAuthMethodCreate(ctx context.Context, d *schema.ResourceData, meta
return diag.Errorf("nil auth method after create")
}

amid := amcr.GetResponse().Map["id"].(string)

// update scope when set to primary
if p, ok := d.GetOk(authmethodIsPrimaryAuthMethodForScopeKey); ok {
if p.(bool) {
if err := updateScopeWithPrimaryAuthMethodId(ctx, scopeId, amid, meta); err != nil {
return diag.Errorf("%v", err)
}

amcr.GetResponse().Map[authmethodIsPrimaryAuthMethodForScopeKey] = true
}
}

if err := setFromAuthMethodResponseMap(d, amcr.GetResponse().Map); err != nil {
return diag.FromErr(err)
}
Expand All @@ -183,6 +205,13 @@ func resourceAuthMethodRead(ctx context.Context, d *schema.ResourceData, meta in
return diag.Errorf("auth method nil after read")
}

serr, isPrimary := readScopeIsPrimaryAuthMethodId(ctx, amrr.GetResponse().Map["scope_id"].(string), amrr.GetResponse().Map["id"].(string), meta)
if serr != nil {
return diag.Errorf("%v", serr)
}

amrr.GetResponse().Map[authmethodIsPrimaryAuthMethodForScopeKey] = isPrimary

if err := setFromAuthMethodResponseMap(d, amrr.GetResponse().Map); err != nil {
return diag.FromErr(err)
}
Expand Down Expand Up @@ -225,13 +254,48 @@ func resourceAuthMethodUpdate(ctx context.Context, d *schema.ResourceData, meta
}
}

opts = append(opts, authmethods.WithAutomaticVersioning(true))
amu, err := amClient.Update(ctx, d.Id(), 0, opts...)
if err != nil {
return diag.Errorf("error updating auth method: %v", err)
if d.HasChange(authmethodIsPrimaryAuthMethodForScopeKey) {
amrr, err := amClient.Read(ctx, d.Id())
if err != nil {
return diag.Errorf("error updating auth method: %v", err)
}
if amrr == nil {
return diag.Errorf("error updating auth method: nil resource")
}
scopeId := amrr.GetResponse().Map["scope_id"].(string)
authMethodId := amrr.GetResponse().Map["id"].(string)

isPrimary := d.Get(authmethodIsPrimaryAuthMethodForScopeKey).(bool)

if isPrimary {
if err := updateScopeWithPrimaryAuthMethodId(ctx, scopeId, authMethodId, meta); err != nil {
return diag.Errorf("%v", err)
}
} else {
if err := updateScopeWithPrimaryAuthMethodId(ctx, scopeId, "", meta); err != nil {
return diag.Errorf("%v", err)
}
}
}

setFromAuthMethodResponseMap(d, amu.GetResponse().Map)
if len(opts) > 0 {
opts = append(opts, authmethods.WithAutomaticVersioning(true))
amu, err := amClient.Update(ctx, d.Id(), 0, opts...)
if err != nil {
return diag.Errorf("error updating auth method: %v", err)
}

if d.HasChange(authmethodIsPrimaryAuthMethodForScopeKey) {
amu.GetResponse().Map[authmethodIsPrimaryAuthMethodForScopeKey] = d.Get(authmethodIsPrimaryAuthMethodForScopeKey).(bool)
}

setFromAuthMethodResponseMap(d, amu.GetResponse().Map)
}

// If only is_primary_for_scope changed
if d.HasChange(authmethodIsPrimaryAuthMethodForScopeKey) {
return resourceAuthMethodPasswordRead(ctx, d, meta)
}

return nil
}
Expand Down
7 changes: 6 additions & 1 deletion internal/provider/resource_auth_method_oidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,12 @@ func updateScopeWithPrimaryAuthMethodId(ctx context.Context, scopeId, authmethod

opts := []scopes.Option{}
opts = append(opts, scopes.WithAutomaticVersioning(true))
opts = append(opts, scopes.WithPrimaryAuthMethodId(authmethodId))

if authmethodId != "" {
opts = append(opts, scopes.WithPrimaryAuthMethodId(authmethodId))
} else {
opts = append(opts, scopes.DefaultPrimaryAuthMethodId())
}

_, err := scp.Update(ctx, scopeId, 0, opts...)
if err != nil {
Expand Down
68 changes: 65 additions & 3 deletions internal/provider/resource_auth_method_password.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ import (
)

const (
authmethodTypePassword = "password"
authmethodMinLoginNameLengthKey = "min_login_name_length"
authmethodMinPasswordLengthKey = "min_password_length"
authmethodTypePassword = "password"
authmethodMinLoginNameLengthKey = "min_login_name_length"
authmethodMinPasswordLengthKey = "min_password_length"
authmethodIsPrimaryAuthMethodForScopeKey = "is_primary_for_scope"
)

func resourceAuthMethodPassword() *schema.Resource {
Expand Down Expand Up @@ -72,6 +73,11 @@ func resourceAuthMethodPassword() *schema.Resource {
Optional: true,
Computed: true,
},
authmethodIsPrimaryAuthMethodForScopeKey: {
Description: "When true, makes this auth method the primary auth method for the scope in which it resides.",
Type: schema.TypeBool,
Optional: true,
},
},
}
}
Expand All @@ -82,6 +88,10 @@ func setFromPasswordAuthMethodResponseMap(d *schema.ResourceData, raw map[string
d.Set(ScopeIdKey, raw[ScopeIdKey])
d.Set(TypeKey, raw[TypeKey])

if p, ok := raw[authmethodIsPrimaryAuthMethodForScopeKey]; ok {
d.Set(authmethodIsPrimaryAuthMethodForScopeKey, p.(bool))
}

if attrsVal, ok := raw["attributes"]; ok {
attrs := attrsVal.(map[string]interface{})

Expand Down Expand Up @@ -158,6 +168,19 @@ func resourceAuthMethodPasswordCreate(ctx context.Context, d *schema.ResourceDat
return diag.Errorf("nil auth method after create")
}

amid := amcr.GetResponse().Map["id"].(string)

// update scope when set to primary
if p, ok := d.GetOk(authmethodIsPrimaryAuthMethodForScopeKey); ok {
if p.(bool) {
if err := updateScopeWithPrimaryAuthMethodId(ctx, scopeId, amid, meta); err != nil {
return diag.Errorf("%v", err)
}

amcr.GetResponse().Map[authmethodIsPrimaryAuthMethodForScopeKey] = true
}
}

return setFromPasswordAuthMethodResponseMap(d, amcr.GetResponse().Map)
}

Expand All @@ -177,6 +200,13 @@ func resourceAuthMethodPasswordRead(ctx context.Context, d *schema.ResourceData,
return diag.Errorf("auth method nil after read")
}

serr, isPrimary := readScopeIsPrimaryAuthMethodId(ctx, amrr.GetResponse().Map["scope_id"].(string), amrr.GetResponse().Map["id"].(string), meta)
if serr != nil {
return diag.Errorf("%v", serr)
}

amrr.GetResponse().Map[authmethodIsPrimaryAuthMethodForScopeKey] = isPrimary

return setFromPasswordAuthMethodResponseMap(d, amrr.GetResponse().Map)
}

Expand Down Expand Up @@ -218,15 +248,47 @@ func resourceAuthMethodPasswordUpdate(ctx context.Context, d *schema.ResourceDat
}
}

if d.HasChange(authmethodIsPrimaryAuthMethodForScopeKey) {
amrr, err := amClient.Read(ctx, d.Id())
if err != nil {
return diag.Errorf("error updating auth method: %v", err)
}
if amrr == nil {
return diag.Errorf("error updating auth method: nil resource")
}
scopeId := amrr.GetResponse().Map["scope_id"].(string)
authMethodId := amrr.GetResponse().Map["id"].(string)

if d.Get(authmethodIsPrimaryAuthMethodForScopeKey).(bool) {
if err := updateScopeWithPrimaryAuthMethodId(ctx, scopeId, authMethodId, meta); err != nil {
return diag.Errorf("%v", err)
}
} else {
Copy link
Collaborator

Choose a reason for hiding this comment

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

As @moduli pointed out there are a few differences in how this is being implemented here vs oidc and ldap is there a specific reason for this?
I would need to think through this specific else case here, but the initial thoughts I am thinking about are unsetting this here might break another update that is trying to set this for the scope.

Copy link
Contributor Author

@rayennh rayennh Nov 5, 2025

Choose a reason for hiding this comment

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

@moduli @louisruch Many thanks for the review !

The differences in my implementation were intended to address two issues I encountered when reproducing the OIDC and LDAP auth method implementations:

  1. Ignored updates: When is_primary_for_scope is the only attribute changed, the update is silently ignored
  2. Cannot unset primary: Once an auth method is set as primary, removing or setting is_primary_for_scope = false has no effect, the auth method remains primary

My initial implementation solved both issues, but as @louisruch correctly identified, the else block introduces a race condition: if auth method A is being unset while auth method B is being set as primary simultaneously, one of them will fail with a scope version conflict error.

Proposed solution:

  1. Remove the else block to prevent the race condition. This reintroduces issue 2. (cannot unset primary), but this is the same behavior as OIDC/LDAP and is probably safer from a concurrency perspective.

  2. Keep the fix for issue 1. (ignored updates when only is_primary_for_scope changes) and apply it consistently to OIDC and LDAP auth methods as well.

The workaround for users who want to unset a primary auth method would be to either :

  • Set another auth method as primary (which automatically unsets the previous one)
  • Manually unset via the Boundary API/CLI

We could update the attribute description to document this limitation:

Description: "When true, makes this auth method the primary auth method for the scope. To unset, either set another auth method as primary or use the Boundary API/CLI directly."

What are you thoughts on this approach ?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I would want to be consistent in how we approach this for all scenarios. If we want to allow unsettling - I would need to chat with the rest of the team how we want to approach this and we would need to update oidc/ldap.

Overall I am happy with this PR, my recommendation is that we remove the else block and then merge it. We can then, if we feel it is the correct approach, open a different PR to update the behavior of all oidc/ldap/password

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@louisruch Thank you for the feedback, that makes complete sense.

I have removed the else block as recommended so we can merge the PR with the current implementation if this looks good to you

if err := updateScopeWithPrimaryAuthMethodId(ctx, scopeId, "", meta); err != nil {
return diag.Errorf("%v", err)
}
}
}

if len(opts) > 0 {
opts = append(opts, authmethods.WithAutomaticVersioning(true))
amur, err := amClient.Update(ctx, d.Id(), 0, opts...)
if err != nil {
return diag.Errorf("error updating auth method: %v", err)
}

if d.HasChange(authmethodIsPrimaryAuthMethodForScopeKey) {
amur.GetResponse().Map[authmethodIsPrimaryAuthMethodForScopeKey] = d.Get(authmethodIsPrimaryAuthMethodForScopeKey).(bool)
}

return setFromPasswordAuthMethodResponseMap(d, amur.GetResponse().Map)
}

// If only is_primary_for_scope changed
if d.HasChange(authmethodIsPrimaryAuthMethodForScopeKey) {
return resourceAuthMethodPasswordRead(ctx, d, meta)
}

return nil
}

Expand Down
Loading