From 614984d1bc2d5a60458f539ae0ae3f7511a596f5 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Fri, 5 Jun 2026 14:40:30 +0000 Subject: [PATCH 1/2] feat: add default_org_member_roles to coderd_organization Adds a new `default_org_member_roles` set attribute to the `coderd_organization` resource so callers can override the deployment-wide default member roles per organization. The attribute is set after `CreateOrganization` (the create endpoint doesn't accept it) and through `UpdateOrganization` thereafter. Also bumps coder/coder to a SHA that contains the gateway-accounts stack (coder/coder#25994) and reacts to coder/coder#24984, which migrated UpdateTemplateMeta fields from values to optional pointers. --- docs/resources/organization.md | 1 + internal/provider/organization_resource.go | 82 ++++++++++++++++++- .../provider/organization_resource_test.go | 25 +++++- 3 files changed, 103 insertions(+), 5 deletions(-) diff --git a/docs/resources/organization.md b/docs/resources/organization.md index 28d717e1..000b1077 100644 --- a/docs/resources/organization.md +++ b/docs/resources/organization.md @@ -54,6 +54,7 @@ resource "coderd_organization" "blueberry" { ### Optional +- `default_org_member_roles` (List of String) Built-in organization role names that are unioned into every member's effective roles. Changes propagate to members on their next request. Setting any value other than the deployment defaults requires the `minimum-implicit-member` experiment to be enabled on the Coder Deployment. - `description` (String) - `display_name` (String) Display name of the organization. Defaults to name. - `group_sync` (Block, Optional, Deprecated) Group sync settings to sync groups from an IdP. diff --git a/internal/provider/organization_resource.go b/internal/provider/organization_resource.go index 568809eb..57371ca5 100644 --- a/internal/provider/organization_resource.go +++ b/internal/provider/organization_resource.go @@ -45,6 +45,8 @@ type OrganizationResourceModel struct { Icon types.String `tfsdk:"icon"` WorkspaceSharing types.String `tfsdk:"workspace_sharing"` + DefaultOrgMemberRoles types.List `tfsdk:"default_org_member_roles"` + OrgSyncIdpGroups types.Set `tfsdk:"org_sync_idp_groups"` GroupSync types.Object `tfsdk:"group_sync"` RoleSync types.Object `tfsdk:"role_sync"` @@ -149,6 +151,15 @@ This resource is only compatible with Coder version [2.16.0](https://github.com/ }, }, + "default_org_member_roles": schema.ListAttribute{ + ElementType: types.StringType, + MarkdownDescription: "Built-in organization role names that are unioned into every member's effective roles. " + + "Changes propagate to members on their next request. Setting any value other than the deployment defaults " + + "requires the `minimum-implicit-member` experiment to be enabled on the Coder Deployment.", + Optional: true, + Computed: true, + }, + "org_sync_idp_groups": schema.SetAttribute{ ElementType: types.StringType, Optional: true, @@ -352,6 +363,13 @@ func (r *OrganizationResource) Read(ctx context.Context, req resource.ReadReques data.Icon = types.StringValue(org.Icon) data.WorkspaceSharing = workspaceSharing + defaultOrgMemberRoles, diags := defaultOrgMemberRolesValueFromAPI(ctx, org.DefaultOrgMemberRoles) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + data.DefaultOrgMemberRoles = defaultOrgMemberRoles + // Save updated data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } @@ -456,6 +474,26 @@ func (r *OrganizationResource) Create(ctx context.Context, req resource.CreateRe } } + // Apply default_org_member_roles if the user specified them. + if !data.DefaultOrgMemberRoles.IsNull() && !data.DefaultOrgMemberRoles.IsUnknown() { + tflog.Trace(ctx, "updating default org member roles", map[string]any{ + "orgID": orgID, + }) + + var roles []string + resp.Diagnostics.Append(data.DefaultOrgMemberRoles.ElementsAs(ctx, &roles, false)...) + if resp.Diagnostics.HasError() { + return + } + org, err = r.Client.UpdateOrganization(ctx, orgID.String(), codersdk.UpdateOrganizationRequest{ + DefaultOrgMemberRoles: &roles, + }) + if err != nil { + resp.Diagnostics.AddError("Default Org Member Roles Update error", err.Error()) + return + } + } + // This is computed, we need to write a known value to the state // in any case. workspaceSharing, err := fetchWorkspaceSharingValue(ctx, r.Client, orgID.String()) @@ -466,6 +504,13 @@ func (r *OrganizationResource) Create(ctx context.Context, req resource.CreateRe } data.WorkspaceSharing = workspaceSharing + defaultOrgMemberRoles, diags := defaultOrgMemberRolesValueFromAPI(ctx, org.DefaultOrgMemberRoles) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + data.DefaultOrgMemberRoles = defaultOrgMemberRoles + // Save data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } @@ -488,11 +533,23 @@ func (r *OrganizationResource) Update(ctx context.Context, req resource.UpdateRe "new_description": data.Description.ValueString(), "new_icon": data.Icon.ValueString(), }) + + var defaultRolesPtr *[]string + if !data.DefaultOrgMemberRoles.IsNull() && !data.DefaultOrgMemberRoles.IsUnknown() { + var roles []string + resp.Diagnostics.Append(data.DefaultOrgMemberRoles.ElementsAs(ctx, &roles, false)...) + if resp.Diagnostics.HasError() { + return + } + defaultRolesPtr = &roles + } + org, err := r.Client.UpdateOrganization(ctx, orgID.String(), codersdk.UpdateOrganizationRequest{ - Name: data.Name.ValueString(), - DisplayName: data.DisplayName.ValueString(), - Description: data.Description.ValueStringPointer(), - Icon: data.Icon.ValueStringPointer(), + Name: data.Name.ValueString(), + DisplayName: data.DisplayName.ValueString(), + Description: data.Description.ValueStringPointer(), + Icon: data.Icon.ValueStringPointer(), + DefaultOrgMemberRoles: defaultRolesPtr, }) if err != nil { resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to update organization %s, got error: %s", orgID, err)) @@ -579,6 +636,13 @@ func (r *OrganizationResource) Update(ctx context.Context, req resource.UpdateRe } data.WorkspaceSharing = workspaceSharing + defaultOrgMemberRoles, diags := defaultOrgMemberRolesValueFromAPI(ctx, org.DefaultOrgMemberRoles) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + data.DefaultOrgMemberRoles = defaultOrgMemberRoles + // Save updated data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } @@ -790,3 +854,13 @@ func isWorkspaceSharingExperimentOff(err error) bool { } return false } + +// defaultOrgMemberRolesValueFromAPI converts the API's []string into a +// types.List[string]. A nil slice from an older server is treated as an +// empty list so the attribute always has a known value. +func defaultOrgMemberRolesValueFromAPI(ctx context.Context, roles []string) (types.List, diag.Diagnostics) { + if roles == nil { + roles = []string{} + } + return types.ListValueFrom(ctx, types.StringType, roles) +} diff --git a/internal/provider/organization_resource_test.go b/internal/provider/organization_resource_test.go index c850beb4..3156a5db 100644 --- a/internal/provider/organization_resource_test.go +++ b/internal/provider/organization_resource_test.go @@ -26,7 +26,7 @@ func TestAccOrganizationResource(t *testing.T) { } ctx := t.Context() - client := integration.StartCoder(ctx, t, "organization_acc", integration.UseLicense, integration.CoderExperiments("workspace-sharing")) + client := integration.StartCoder(ctx, t, "organization_acc", integration.UseLicense, integration.CoderExperiments("workspace-sharing,minimum-implicit-member")) _, err := client.User(ctx, codersdk.Me) require.NoError(t, err) runOrganizationResourceTest(t, client, true) @@ -166,6 +166,9 @@ func runOrganizationResourceTest(t *testing.T, client *codersdk.Client, enableEx cfg7 := cfg6 cfg7.WorkspaceSharing = ptr.Ref("everyone") + cfg8 := cfg7 + cfg8.DefaultOrgMemberRoles = ptr.Ref([]string{"organization-member", "organization-template-admin"}) + steps = append(steps, // Disable workspace sharing for org resource.TestStep{ @@ -181,6 +184,16 @@ func runOrganizationResourceTest(t *testing.T, client *codersdk.Client, enableEx statecheck.ExpectKnownValue("coderd_organization.test", tfjsonpath.New("workspace_sharing"), knownvalue.StringExact("everyone")), }, }, + // Set default_org_member_roles to a non-default value + resource.TestStep{ + Config: cfg8.String(t), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("coderd_organization.test", tfjsonpath.New("default_org_member_roles"), knownvalue.ListExact([]knownvalue.Check{ + knownvalue.StringExact("organization-member"), + knownvalue.StringExact("organization-template-admin"), + })), + }, + }, ) } return steps @@ -222,6 +235,8 @@ type testAccOrganizationResourceConfig struct { Icon *string WorkspaceSharing *string + DefaultOrgMemberRoles *[]string + OrgSyncIdpGroups []string GroupSync *codersdk.GroupSyncSettings RoleSync *codersdk.RoleSyncSettings @@ -242,6 +257,14 @@ resource "coderd_organization" "test" { icon = {{orNull .Icon}} workspace_sharing = {{orNull .WorkspaceSharing}} + {{- if .DefaultOrgMemberRoles}} + default_org_member_roles = [ + {{- range $role := .DefaultOrgMemberRoles }} + "{{$role}}", + {{- end}} + ] + {{- end}} + {{- if .OrgSyncIdpGroups}} org_sync_idp_groups = [ {{- range $name := .OrgSyncIdpGroups }} From 202b36ff8a4258ccdd9f65c88d2c83f8d4e8d1e6 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 22 Jul 2026 19:36:41 +0000 Subject: [PATCH 2/2] test: use assignable built-in roles after minimum-implicit-member role split "organization-member" is now the implicit floor role and no longer an assignable built-in, and rbac.DefaultOrgMemberRoles() is ["organization-workspace-access"]. Use organization-template-admin + organization-workspace-access as the non-default test value. --- internal/provider/organization_resource_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/provider/organization_resource_test.go b/internal/provider/organization_resource_test.go index 3156a5db..ae06626b 100644 --- a/internal/provider/organization_resource_test.go +++ b/internal/provider/organization_resource_test.go @@ -167,7 +167,7 @@ func runOrganizationResourceTest(t *testing.T, client *codersdk.Client, enableEx cfg7.WorkspaceSharing = ptr.Ref("everyone") cfg8 := cfg7 - cfg8.DefaultOrgMemberRoles = ptr.Ref([]string{"organization-member", "organization-template-admin"}) + cfg8.DefaultOrgMemberRoles = ptr.Ref([]string{"organization-template-admin", "organization-workspace-access"}) steps = append(steps, // Disable workspace sharing for org @@ -189,8 +189,8 @@ func runOrganizationResourceTest(t *testing.T, client *codersdk.Client, enableEx Config: cfg8.String(t), ConfigStateChecks: []statecheck.StateCheck{ statecheck.ExpectKnownValue("coderd_organization.test", tfjsonpath.New("default_org_member_roles"), knownvalue.ListExact([]knownvalue.Check{ - knownvalue.StringExact("organization-member"), knownvalue.StringExact("organization-template-admin"), + knownvalue.StringExact("organization-workspace-access"), })), }, },