Files
member-console/internal/server/external_claim_gate.go
T
cgalo5758 88db730fcc Add dual licensing and SPDX headers
Introduce a commercial license option alongside AGPL-3.0-only, require a
CLA for contributors, and document the terms in COMMERCIAL.md and
NOTICE. Add a script to stamp SPDX headers on Go files and apply it
across the tree.
2026-09-06 02:29:42 -05:00

68 lines
2.5 KiB
Go

// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
// SPDX-FileCopyrightText: 2025-2026 Christian Galo
// 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)
}