// The one concrete ExternalClaimGate (centralize-external-claim-gate): // built here because this package owns both dependencies the gate needs — // the entitlements querier and the deployment's connect target. Injected // into every member-facing domains.Registry (enforcement) and handed to the // surfaces that render the affordance (presentation), so the two conditions // are decided in exactly one place. package server import ( "context" "database/sql" "errors" "fmt" "strings" "git.coopcloud.tech/wiki-cafe/member-console/internal/domains" "git.coopcloud.tech/wiki-cafe/member-console/internal/entitlements" ) // NewExternalClaimGate builds the gate ClaimExternal enforces: the // deployment must have a connect target (no target ⇒ no serving path, // regardless of plan) AND the workspace's primary active pool must grant // domains.ExternalClaimsResourceKey. A never-conferred key has no row // (entitlements spec) and reads as not granted, not as an error; only // infrastructure failures return a non-refusal error. func NewExternalClaimGate(entQ entitlements.Querier, connectTarget string) domains.ExternalClaimGate { connectTarget = strings.TrimSpace(connectTarget) return func(ctx context.Context, workspaceID string) error { if connectTarget == "" { return domains.ErrNoConnectTarget } if entQ == nil { return domains.ErrExternalClaimsNotEntitled } assignment, err := entQ.GetPrimaryPoolAssignmentByWorkspace(ctx, workspaceID) if err != nil { if errors.Is(err, sql.ErrNoRows) { return domains.ErrExternalClaimsNotEntitled } return fmt.Errorf("external-claim gate: primary pool for %s: %w", workspaceID, err) } ent, err := entQ.GetBooleanEntitlementByPoolAndResource(ctx, entitlements.GetBooleanEntitlementByPoolAndResourceParams{ PoolID: assignment.PoolID, ResourceKey: domains.ExternalClaimsResourceKey, }) if err != nil { if errors.Is(err, sql.ErrNoRows) { return domains.ErrExternalClaimsNotEntitled } return fmt.Errorf("external-claim gate: boolean entitlement for %s: %w", workspaceID, err) } if !ent.Granted { return domains.ErrExternalClaimsNotEntitled } return nil } } // externalClaimGateRefused reports whether err is one of the gate's typed // refusals (as opposed to an infrastructure failure). func externalClaimGateRefused(err error) bool { return errors.Is(err, domains.ErrExternalClaimsNotEntitled) || errors.Is(err, domains.ErrNoConnectTarget) }