Skip to content
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
26 changes: 18 additions & 8 deletions internal/client/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"`
Expand Down
10 changes: 10 additions & 0 deletions internal/client/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
44 changes: 0 additions & 44 deletions internal/provider/resource_organization.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
}
}
}
}
41 changes: 19 additions & 22 deletions internal/provider/resource_organization_membership.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}

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

Expand All @@ -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
}
Expand All @@ -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
Expand Down
6 changes: 3 additions & 3 deletions internal/provider/resource_user.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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() {
Expand Down