// Package access - resolved request permissions and context plumbing. package access import ( "context" "log/slog" "atlas9.dev/c/core" "atlas9.dev/c/core/iam" ) // Access holds the resolved permissions for a request. // The zero value denies everything. type Access struct { admin bool grants []accessGrant } type accessGrant struct { // system grants cover all tenants. // For non-system grants, tenant + path form the full scope. system bool tenant core.ID path core.Path cap iam.Cap } func (a *Access) Authenticated() { a.GrantSystem(iam.CapTenantsCreate) } func (a *Access) Grant(cap iam.Cap, tenant core.ID, path core.Path) { a.grants = append(a.grants, accessGrant{ tenant: tenant, path: path, cap: cap, }) } func (a *Access) GrantSystem(caps ...iam.Cap) { for _, cap := range caps { a.grants = append(a.grants, accessGrant{ system: true, cap: cap, }) } } func Admin() Access { return Access{admin: true} } func (a *Access) Check(cap iam.Cap, tenant core.ID, path core.Path) error { if a == nil { return iam.ErrForbidden } if a.admin { return nil } if tenant.IsEmpty() { return iam.ErrForbidden } for _, g := range a.grants { if g.cap != cap { continue } if g.system { return nil } if g.tenant != tenant { continue } if g.path.Contains(path) { return nil } } return iam.ErrForbidden } func (a *Access) Partial(cap iam.Cap) error { if a == nil { return iam.ErrForbidden } if a.admin { return nil } for _, g := range a.grants { if g.cap == cap { return nil } } return iam.ErrForbidden } func (a *Access) System(cap iam.Cap) error { if a == nil { return iam.ErrForbidden } if a.admin { return nil } for _, g := range a.grants { if g.system && g.cap == cap { return nil } } return iam.ErrForbidden } type accessContextKey struct{} func Get(ctx context.Context) *Access { a, _ := ctx.Value(accessContextKey{}).(*Access) return a } func Put(ctx context.Context, a Access) context.Context { return context.WithValue(ctx, accessContextKey{}, &a) } func PutSystem(ctx context.Context, caps ...iam.Cap) context.Context { var access Access access.GrantSystem(caps...) return Put(ctx, access) } type ContextGuard struct{} func (ContextGuard) Check(ctx context.Context, cap iam.Cap, tenant core.ID, path core.Path) error { err := Get(ctx).Check(cap, tenant, path) logCheck(ctx, err, "cap", cap, "tenant", tenant, "path", path) return err } func (ContextGuard) Partial(ctx context.Context, cap iam.Cap) error { err := Get(ctx).Partial(cap) logCheck(ctx, err, "cap", cap) return err } func (ContextGuard) System(ctx context.Context, cap iam.Cap) error { err := Get(ctx).System(cap) logCheck(ctx, err, "cap", cap, "system", true) return err } func logCheck(ctx context.Context, err error, args ...any) { if err != nil { slog.WarnContext(ctx, "access denied", args...) return } slog.DebugContext(ctx, "access check", args...) } type Store interface { Load(ctx context.Context, principalID core.ID, out *Access) error }