// Package provision - creating users and tenants with their supporting records. package provision import ( "context" "fmt" "atlas9.dev/c/core" "atlas9.dev/c/core/dbi" "atlas9.dev/c/core/envelope" "atlas9.dev/c/core/iam" "atlas9.dev/c/demo/lib/ents" "atlas9.dev/c/demo/lib/profiles" ) // Provisioner is the single place users and tenants are created. Provisioning a // tenant also provisions its DEK, so every tenant has one by construction; // provisioning a user creates the user's profile, personal tenant, and owner // membership. It sits above the raw stores, which stay pure CRUD. type Provisioner struct { Tenants dbi.Factory[iam.TenantStore] Members dbi.Factory[iam.TenantMemberStore] Profiles dbi.Factory[profiles.Store] Deks dbi.Factory[envelope.DekStore] Ents dbi.Factory[ents.Store] Wrapper envelope.Wrapper } // TenantCaps returns the caps ProvisionTenant's steps require. They all scope // to the tenant being provisioned, so callers grant them with // access.PutScope(ctx, tenant.ID, TenantCaps()...). Kept here, with the // operation, so the set has one home instead of being restated at each call // site. A fresh slice each call — no shared mutable global. func TenantCaps() []iam.Cap { return []iam.Cap{ iam.CapTenantsCreate, iam.CapTenantMembersAdd, envelope.Cap_Dek_Create, ents.Cap_Entitlements_Write, } } // UserCaps returns the caps ProvisionUser's steps require: Profiles_Write for // the user's profile plus everything ProvisionTenant needs for the personal // tenant. The personal tenant's ID is the user's ID, so every cap scopes to // user.ID — grant with access.PutScope(ctx, user.ID, UserCaps()...). func UserCaps() []iam.Cap { return append([]iam.Cap{profiles.Cap_Profiles_Write}, TenantCaps()...) } // ProvisionTenant creates t within tx and establishes the invariants every // tenant is created with: its DEK and an owner membership so it is never // orphaned. The caller must grant TenantCaps (scoped to the tenant); the // provisioner runs on the ctx it is handed and self-grants nothing. func (p *Provisioner) ProvisionTenant(ctx context.Context, tx dbi.DBI, t *iam.Tenant, owner core.ID) error { if err := p.Tenants(tx).Create(ctx, t); err != nil { return err } if err := envelope.Provision(ctx, p.Deks(tx), p.Wrapper, t.ID); err != nil { return err } // Entitlements must exist before membership — the members trigger checks them. e := ents.Defaults() if err := p.Ents(tx).Create(ctx, t.ID, &e); err != nil { return err } return p.Members(tx).Add(ctx, iam.TenantMember{ Tenant: t.ID, User: owner, Owner: true, }) } // ProvisionUser creates the user's profile, personal tenant (with its DEK), and // owner grant on that tenant. The caller must grant UserCaps (scoped to the // user, which is also the personal tenant's ID). func (p *Provisioner) ProvisionUser(ctx context.Context, tx dbi.DBI, user *iam.User) error { // Create profile profile := profiles.Profile{ UserID: user.ID, } if err := p.Profiles(tx).Create(ctx, &profile); err != nil { return fmt.Errorf("creating profile: %w", err) } // Create personal tenant (with its DEK and owner grant), owned by the user. tenant := iam.Tenant{ ID: user.ID, Name: user.ID.String(), } if err := p.ProvisionTenant(ctx, tx, &tenant, user.ID); err != nil { return fmt.Errorf("creating personal tenant: %w", err) } return nil }