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
32 changes: 28 additions & 4 deletions internal/client/organizations.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"context"
"fmt"
"net/url"
"strings"
)

// CreateOrganization creates a new organization
Expand Down Expand Up @@ -58,8 +59,8 @@ func (c *Client) ListOrganizations(ctx context.Context) (*OrganizationListRespon
return &resp, nil
}

// GetOrganizationByDomain finds an organization by domain
func (c *Client) GetOrganizationByDomain(ctx context.Context, domain string) (*Organization, error) {
// ListOrganizationsByDomain returns all organizations matching a given domain
func (c *Client) ListOrganizationsByDomain(ctx context.Context, domain string) ([]Organization, error) {
params := url.Values{}
params.Set("domains", domain)

Expand All @@ -69,12 +70,35 @@ func (c *Client) GetOrganizationByDomain(ctx context.Context, domain string) (*O
return nil, fmt.Errorf("failed to search organizations by domain: %w", err)
}

if len(resp.Data) == 0 {
return resp.Data, nil
}

// GetOrganizationByDomain finds a single organization by domain.
// Returns an error if no organizations or multiple organizations match the domain.
func (c *Client) GetOrganizationByDomain(ctx context.Context, domain string) (*Organization, error) {
orgs, err := c.ListOrganizationsByDomain(ctx, domain)
if err != nil {
return nil, err
}

if len(orgs) == 0 {
return nil, &APIError{
StatusCode: 404,
Message: fmt.Sprintf("no organization found with domain: %s", domain),
}
}

return &resp.Data[0], nil
if len(orgs) > 1 {
orgIDs := make([]string, len(orgs))
for i, org := range orgs {
orgIDs[i] = fmt.Sprintf("%s (%s)", org.ID, org.Name)
}
return nil, fmt.Errorf(
"ambiguous domain lookup: domain %q is associated with %d organizations: [%s]. "+
"Use the organization ID to look up a specific organization instead",
domain, len(orgs), strings.Join(orgIDs, ", "),
)
}

return &orgs[0], nil
}
45 changes: 45 additions & 0 deletions internal/provider/resource_organization.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ 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 @@ -156,6 +157,12 @@ 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 @@ -284,6 +291,12 @@ 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 @@ -360,3 +373,35 @@ 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
}
}
}
}
Loading