Replace the entity slugs on organizations, workspaces, resource pools, and plan ladders with nullable `key` columns and add keys to products, prices, and entitlement sets. Rename `providers.slug` to `provider` and add partial unique indexes for system and org role names. Assign invoice numbers per billing account from a gapless transactional counter; Stripe's number moves to the invoice mapping as an external reference. Seeds, fixtures, and the operator lookup address rows by key, and the returning-login resync no longer blanks a display name when the IdP sends no `name` claim.
137 lines
5.1 KiB
Go
137 lines
5.1 KiB
Go
// Package systemtenant ensures the singleton "System" tenant exists — a
|
|
// platform-owned organization (org_type='system') and its workspace, owned by a
|
|
// reserved synthetic person — and resolves that workspace by natural key.
|
|
//
|
|
// The System tenant is the owning workspace for provider resources that belong
|
|
// to no member (e.g. ownerless FedWiki farm sites projected by the sync). It is
|
|
// identified by what it IS (org_type='system'), not by an opaque UUID copied
|
|
// into configuration, so it cannot drift out of sync the way the former
|
|
// fedwiki-sync-default-workspace-id config key could.
|
|
package systemtenant
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"git.coopcloud.tech/wiki-cafe/member-console/internal/identity"
|
|
"git.coopcloud.tech/wiki-cafe/member-console/internal/organization"
|
|
)
|
|
|
|
// Reserved natural-key identity of the System tenant. These values are stable
|
|
// by contract: renaming one would orphan an existing tenant rather than rename
|
|
// it, so change them only with a deliberate migration.
|
|
const (
|
|
// OrgType is the reserved organization type. Exported because it is the
|
|
// natural key callers resolve the tenant by.
|
|
OrgType = "system"
|
|
|
|
orgName = "System"
|
|
workspaceName = "System"
|
|
|
|
// userOIDCSubject is the reserved synthetic OIDC subject for the system
|
|
// user. The "urn:" scheme guarantees it never collides with a real IdP
|
|
// subject (Keycloak issues UUID subjects), and there is no matching IdP
|
|
// account, so the system user is non-loginable.
|
|
userOIDCSubject = "urn:member-console:system"
|
|
|
|
personName = "System"
|
|
// personEmail uses the RFC 2606 reserved ".invalid" TLD, so it is
|
|
// guaranteed non-routable.
|
|
personEmail = "system@member-console.invalid"
|
|
)
|
|
|
|
// Ensure idempotently guarantees the System tenant exists and returns its
|
|
// workspace ID. It is meant to run at boot, after migrations and before the
|
|
// FedWiki sync schedule is wired. It is safe to call on every boot and under
|
|
// concurrent boots (each insert is ON CONFLICT DO NOTHING). Any returned error
|
|
// should be treated by the caller as fatal: the sync's landing workspace must
|
|
// exist.
|
|
func Ensure(ctx context.Context, db *sql.DB) (workspaceID string, err error) {
|
|
tx, err := db.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return "", fmt.Errorf("system tenant: begin tx: %w", err)
|
|
}
|
|
defer func() { _ = tx.Rollback() }() // no-op after a successful commit
|
|
|
|
idQ := identity.New(tx)
|
|
orgQ := organization.New(tx)
|
|
|
|
// Fast path / adoption: if the system org already has a workspace, adopt
|
|
// it. This is the steady-state no-op and also adopts a pre-existing
|
|
// differently-named workspace (e.g. one left by an ad-hoc seed) without
|
|
// creating a second one.
|
|
if ws, gErr := orgQ.GetSystemOrgWorkspace(ctx); gErr == nil {
|
|
if err = tx.Commit(); err != nil {
|
|
return "", fmt.Errorf("system tenant: commit (adopt): %w", err)
|
|
}
|
|
return ws.WorkspaceID, nil
|
|
} else if !errors.Is(gErr, sql.ErrNoRows) {
|
|
return "", fmt.Errorf("system tenant: resolve workspace: %w", gErr)
|
|
}
|
|
|
|
// Create the chain: user -> person -> org_type -> org -> workspace.
|
|
if err = idQ.EnsureSystemUser(ctx, userOIDCSubject); err != nil {
|
|
return "", fmt.Errorf("system tenant: ensure user: %w", err)
|
|
}
|
|
user, err := idQ.GetUserByOIDCSubject(ctx, userOIDCSubject)
|
|
if err != nil {
|
|
return "", fmt.Errorf("system tenant: get user: %w", err)
|
|
}
|
|
|
|
if err = idQ.EnsureSystemPerson(ctx, identity.EnsureSystemPersonParams{
|
|
UserID: user.UserID,
|
|
DisplayName: personName,
|
|
PrimaryEmail: personEmail,
|
|
}); err != nil {
|
|
return "", fmt.Errorf("system tenant: ensure person: %w", err)
|
|
}
|
|
person, err := idQ.GetPersonByUserID(ctx, user.UserID)
|
|
if err != nil {
|
|
return "", fmt.Errorf("system tenant: get person: %w", err)
|
|
}
|
|
|
|
if err = orgQ.EnsureSystemOrgType(ctx, organization.EnsureSystemOrgTypeParams{
|
|
OrgType: OrgType,
|
|
DisplayName: orgName,
|
|
}); err != nil {
|
|
return "", fmt.Errorf("system tenant: ensure org type: %w", err)
|
|
}
|
|
|
|
// The partial unique index uq_organizations_one_system carries "exactly
|
|
// one System tenant", so the ensure conflicts on org_type and the
|
|
// read-back needs no other handle. EnsureSystemOrganization also writes
|
|
// the organization's key, the constant `system` (entity-keys §4); it is
|
|
// set in the query rather than passed from here so the only path that
|
|
// creates the System tenant is also the only path that names it.
|
|
if err = orgQ.EnsureSystemOrganization(ctx, organization.EnsureSystemOrganizationParams{
|
|
Name: orgName,
|
|
OrgType: OrgType,
|
|
OwnerPersonID: person.PersonID,
|
|
}); err != nil {
|
|
return "", fmt.Errorf("system tenant: ensure org: %w", err)
|
|
}
|
|
org, err := orgQ.GetSystemOrganization(ctx)
|
|
if err != nil {
|
|
return "", fmt.Errorf("system tenant: get org: %w", err)
|
|
}
|
|
|
|
if err = orgQ.EnsureSystemWorkspace(ctx, organization.EnsureSystemWorkspaceParams{
|
|
OrgID: org.OrgID,
|
|
Name: workspaceName,
|
|
}); err != nil {
|
|
return "", fmt.Errorf("system tenant: ensure workspace: %w", err)
|
|
}
|
|
|
|
ws, err := orgQ.GetSystemOrgWorkspace(ctx)
|
|
if err != nil {
|
|
return "", fmt.Errorf("system tenant: resolve workspace after create: %w", err)
|
|
}
|
|
|
|
if err = tx.Commit(); err != nil {
|
|
return "", fmt.Errorf("system tenant: commit: %w", err)
|
|
}
|
|
return ws.WorkspaceID, nil
|
|
}
|