diff --git a/internal/client/models.go b/internal/client/models.go index 2d839bf..9f5aca7 100644 --- a/internal/client/models.go +++ b/internal/client/models.go @@ -176,16 +176,21 @@ type UserUpdateRequest struct { Metadata map[string]string `json:"metadata,omitempty"` } +// OrganizationMembershipRole represents the role assigned in a membership +type OrganizationMembershipRole struct { + Slug string `json:"slug"` +} + // OrganizationMembership represents a user's membership in an organization type OrganizationMembership struct { - ID string `json:"id"` - Object string `json:"object"` - UserID string `json:"user_id"` - OrganizationID string `json:"organization_id"` - RoleSlug string `json:"role_slug,omitempty"` - Status string `json:"status"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID string `json:"id"` + Object string `json:"object"` + UserID string `json:"user_id"` + OrganizationID string `json:"organization_id"` + Role OrganizationMembershipRole `json:"role"` + Status string `json:"status"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } // OrganizationMembershipCreateRequest represents the request to create a membership @@ -195,6 +200,11 @@ type OrganizationMembershipCreateRequest struct { RoleSlug string `json:"role_slug,omitempty"` } +// OrganizationMembershipUpdateRequest represents the request to update a membership +type OrganizationMembershipUpdateRequest struct { + RoleSlug string `json:"role_slug,omitempty"` +} + // OrganizationRole represents a WorkOS Organization Role type OrganizationRole struct { ID string `json:"id"` diff --git a/internal/client/users.go b/internal/client/users.go index 9438e56..8145d38 100644 --- a/internal/client/users.go +++ b/internal/client/users.go @@ -124,6 +124,16 @@ func (c *Client) GetOrganizationMembership(ctx context.Context, id string) (*Org return &membership, nil } +// UpdateOrganizationMembership updates an organization membership by ID +func (c *Client) UpdateOrganizationMembership(ctx context.Context, id string, req *OrganizationMembershipUpdateRequest) (*OrganizationMembership, error) { + var membership OrganizationMembership + err := c.Put(ctx, "/user_management/organization_memberships/"+id, req, &membership) + if err != nil { + return nil, fmt.Errorf("failed to update organization membership: %w", err) + } + return &membership, nil +} + // DeleteOrganizationMembership deletes an organization membership by ID func (c *Client) DeleteOrganizationMembership(ctx context.Context, id string) error { err := c.Delete(ctx, "/user_management/organization_memberships/"+id) diff --git a/internal/provider/resource_organization.go b/internal/provider/resource_organization.go index 45508c1..d2a9c26 100644 --- a/internal/provider/resource_organization.go +++ b/internal/provider/resource_organization.go @@ -8,7 +8,6 @@ import ( "fmt" "time" - "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" @@ -192,12 +191,6 @@ func (r *OrganizationResource) Create(ctx context.Context, req resource.CreateRe return } - // Validate that domains are not already in use by another organization - r.validateDomainUniqueness(ctx, domains, "", &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - for _, domain := range domains { createReq.DomainData = append(createReq.DomainData, client.DomainData{ Domain: domain, @@ -375,12 +368,6 @@ func (r *OrganizationResource) Update(ctx context.Context, req resource.UpdateRe return } - // Validate that domains are not already in use by another organization - r.validateDomainUniqueness(ctx, domains, state.ID.ValueString(), &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - for _, domain := range domains { updateReq.DomainData = append(updateReq.DomainData, client.DomainData{ Domain: domain, @@ -473,34 +460,3 @@ func (r *OrganizationResource) ImportState(ctx context.Context, req resource.Imp resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) } -// validateDomainUniqueness checks that the given domains are not already assigned -// to a different organization. excludeOrgID can be empty for creates. -func (r *OrganizationResource) validateDomainUniqueness(ctx context.Context, domains []string, excludeOrgID string, diags *diag.Diagnostics) { - for _, domain := range domains { - orgs, err := r.client.ListOrganizationsByDomain(ctx, domain) - if err != nil { - diags.AddError( - "Error Checking Domain Uniqueness", - fmt.Sprintf("Could not check if domain %q is already in use: %s", domain, err.Error()), - ) - return - } - - for _, org := range orgs { - if org.ID != excludeOrgID { - diags.AddError( - "Domain Already In Use", - fmt.Sprintf( - "Domain %q is already associated with organization %q (%s). "+ - "The WorkOS API does not enforce domain uniqueness, but assigning the same "+ - "domain to multiple organizations can cause unpredictable SSO routing and "+ - "data source lookups. Remove the domain from the existing organization first, "+ - "or use a different domain.", - domain, org.Name, org.ID, - ), - ) - return - } - } - } -} diff --git a/internal/provider/resource_organization_membership.go b/internal/provider/resource_organization_membership.go index 14391ed..72f7761 100644 --- a/internal/provider/resource_organization_membership.go +++ b/internal/provider/resource_organization_membership.go @@ -182,7 +182,7 @@ func (r *OrganizationMembershipResource) Create(ctx context.Context, req resourc OrganizationID: plan.OrganizationID.ValueString(), } - if !plan.RoleSlug.IsNull() { + if !plan.RoleSlug.IsNull() && !plan.RoleSlug.IsUnknown() { createReq.RoleSlug = plan.RoleSlug.ValueString() } @@ -199,12 +199,8 @@ func (r *OrganizationMembershipResource) Create(ctx context.Context, req resourc plan.ID = types.StringValue(membership.ID) plan.UserID = types.StringValue(membership.UserID) plan.OrganizationID = types.StringValue(membership.OrganizationID) - // The API may not return role_slug in the response, so preserve the plan value - // if it was set, since we know it was applied during creation. - if membership.RoleSlug != "" { - plan.RoleSlug = types.StringValue(membership.RoleSlug) - } else if !plan.RoleSlug.IsNull() && plan.RoleSlug.ValueString() != "" { - // Preserve plan value - API accepted it but didn't return it + if membership.Role.Slug != "" { + plan.RoleSlug = types.StringValue(membership.Role.Slug) } else { plan.RoleSlug = types.StringNull() } @@ -253,12 +249,8 @@ func (r *OrganizationMembershipResource) Read(ctx context.Context, req resource. // Map response to state state.UserID = types.StringValue(membership.UserID) state.OrganizationID = types.StringValue(membership.OrganizationID) - // The API may not return role_slug in the response, so preserve the - // existing state value if the API returns empty. - if membership.RoleSlug != "" { - state.RoleSlug = types.StringValue(membership.RoleSlug) - } else if !state.RoleSlug.IsNull() && state.RoleSlug.ValueString() != "" { - // Preserve existing state value - API didn't return it but it was set + if membership.Role.Slug != "" { + state.RoleSlug = types.StringValue(membership.Role.Slug) } else { state.RoleSlug = types.StringNull() } @@ -270,10 +262,6 @@ func (r *OrganizationMembershipResource) Read(ctx context.Context, req resource. } func (r *OrganizationMembershipResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { - // Organization memberships cannot be updated - user_id and organization_id - // both require replacement. The only updatable field would be role_slug, - // but WorkOS API doesn't currently support updating membership roles directly. - // For now, we just read the current state. var plan OrganizationMembershipResourceModel var state OrganizationMembershipResourceModel @@ -287,12 +275,19 @@ func (r *OrganizationMembershipResource) Update(ctx context.Context, req resourc "id": state.ID.ValueString(), }) - // Read current state from API - membership, err := r.client.GetOrganizationMembership(ctx, state.ID.ValueString()) + updateReq := &client.OrganizationMembershipUpdateRequest{} + + if !plan.RoleSlug.Equal(state.RoleSlug) && !plan.RoleSlug.IsUnknown() { + if !plan.RoleSlug.IsNull() { + updateReq.RoleSlug = plan.RoleSlug.ValueString() + } + } + + membership, err := r.client.UpdateOrganizationMembership(ctx, state.ID.ValueString(), updateReq) if err != nil { resp.Diagnostics.AddError( "Error Updating Organization Membership", - "Could not read organization membership: "+err.Error(), + "Could not update organization membership: "+err.Error(), ) return } @@ -301,8 +296,10 @@ func (r *OrganizationMembershipResource) Update(ctx context.Context, req resourc plan.ID = state.ID plan.UserID = types.StringValue(membership.UserID) plan.OrganizationID = types.StringValue(membership.OrganizationID) - if membership.RoleSlug != "" { - plan.RoleSlug = types.StringValue(membership.RoleSlug) + if membership.Role.Slug != "" { + plan.RoleSlug = types.StringValue(membership.Role.Slug) + } else { + plan.RoleSlug = types.StringNull() } plan.Status = types.StringValue(membership.Status) plan.CreatedAt = state.CreatedAt diff --git a/internal/provider/resource_user.go b/internal/provider/resource_user.go index 4d4c8ed..b09870b 100644 --- a/internal/provider/resource_user.go +++ b/internal/provider/resource_user.go @@ -285,10 +285,10 @@ func (r *UserResource) Create(ctx context.Context, req resource.CreateRequest, r if !plan.PasswordHashType.IsNull() { createReq.PasswordHashType = plan.PasswordHashType.ValueString() } - if !plan.ExternalID.IsNull() { + if !plan.ExternalID.IsNull() && !plan.ExternalID.IsUnknown() { createReq.ExternalID = plan.ExternalID.ValueString() } - if !plan.Metadata.IsNull() { + if !plan.Metadata.IsNull() && !plan.Metadata.IsUnknown() { metadata := make(map[string]string) resp.Diagnostics.Append(plan.Metadata.ElementsAs(ctx, &metadata, false)...) if resp.Diagnostics.HasError() { @@ -470,7 +470,7 @@ func (r *UserResource) Update(ctx context.Context, req resource.UpdateRequest, r if !plan.ExternalID.Equal(state.ExternalID) { updateReq.ExternalID = plan.ExternalID.ValueString() } - if !plan.Metadata.Equal(state.Metadata) { + if !plan.Metadata.Equal(state.Metadata) && !plan.Metadata.IsUnknown() { metadata := make(map[string]string) resp.Diagnostics.Append(plan.Metadata.ElementsAs(ctx, &metadata, false)...) if resp.Diagnostics.HasError() {