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.
68 lines
2.5 KiB
Go
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)
|
|
}
|