Files
gamertan 920e68f57f
verify / verify (push) Successful in 3m2s
release: publish Web Foundations Preview 2 snapshot
Sanitized allowlisted snapshot of private source 0acd276fb3423405daf7fff26dedc92b8281e2bd. Adds organization, team, invitation, resource hierarchy, scoped access, and audited break-glass foundations while preserving Preview 1.

AI-Assistance: OpenAI Codex assisted implementation, testing, security review, and release preparation.
Signed-off-by: Cole Speelman <crspeelman@gmail.com>
2026-08-17 00:26:22 -04:00

271 lines
8.7 KiB
Go

// SPDX-License-Identifier: MPL-2.0
// Package access defines organization-scoped role bindings and audited,
// short-lived break-glass authorization.
package access
import (
"context"
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"io"
"regexp"
"sort"
"strings"
"time"
)
var (
idPattern = regexp.MustCompile(`^[A-Za-z0-9_-]{8,128}$`)
namePattern = regexp.MustCompile(`^[a-z][a-z0-9._-]{1,127}$`)
)
type SubjectKind string
const (
User SubjectKind = "user"
Team SubjectKind = "team"
)
type Scope struct {
OrganizationID string
ProjectID string
EnvironmentID string
ServiceID string
}
func (scope Scope) Validate() error {
if !idPattern.MatchString(scope.OrganizationID) || scope.ProjectID != "" && !idPattern.MatchString(scope.ProjectID) || scope.EnvironmentID != "" && !idPattern.MatchString(scope.EnvironmentID) || scope.ServiceID != "" && !idPattern.MatchString(scope.ServiceID) {
return errors.New("access: invalid scope")
}
if scope.EnvironmentID != "" && scope.ProjectID == "" || scope.ServiceID != "" && scope.EnvironmentID == "" {
return errors.New("access: incomplete scope hierarchy")
}
return nil
}
func (scope Scope) contains(requested Scope) bool {
if scope.OrganizationID != requested.OrganizationID {
return false
}
for _, pair := range [][2]string{{scope.ProjectID, requested.ProjectID}, {scope.EnvironmentID, requested.EnvironmentID}, {scope.ServiceID, requested.ServiceID}} {
if pair[0] != "" && pair[0] != pair[1] {
return false
}
}
return true
}
type Binding struct {
ID string
SubjectKind SubjectKind
SubjectID string
Role string
Scope Scope
GrantedBy string
GrantedAt time.Time
}
type Policy struct {
Roles map[string]string
Permissions map[string]string
Grants map[string][]string
}
func (policy Policy) Validate() error {
if len(policy.Roles) == 0 || len(policy.Roles) > 1000 || len(policy.Permissions) == 0 || len(policy.Permissions) > 10000 || len(policy.Grants) > 1000 {
return errors.New("access: invalid policy size")
}
for name, description := range policy.Roles {
if !namePattern.MatchString(name) || !text(description, 512, true) {
return errors.New("access: invalid role")
}
}
for name, description := range policy.Permissions {
if !namePattern.MatchString(name) || !text(description, 512, true) {
return errors.New("access: invalid permission")
}
}
for role, permissions := range policy.Grants {
if _, ok := policy.Roles[role]; !ok || len(permissions) > 10000 {
return errors.New("access: invalid role grant")
}
for _, permission := range permissions {
if _, ok := policy.Permissions[permission]; !ok {
return errors.New("access: role references unknown permission")
}
}
}
return nil
}
type BreakGlass struct {
ID, OrganizationID, UserID, Permission, Reason string
CreatedAt, ExpiresAt time.Time
}
type AuditEvent struct {
ID, OrganizationID, ActorUserID, Action, ResourceType, ResourceID, RequestID, Summary string
CreatedAt time.Time
}
type Repository interface {
SeedAccessPolicy(context.Context, Policy) error
Grant(context.Context, Binding) error
Revoke(context.Context, string, string, time.Time) error
EffectiveBindings(context.Context, string, string) ([]Binding, error)
CreateBreakGlass(context.Context, BreakGlass, AuditEvent) error
ActiveBreakGlass(context.Context, string, string, time.Time) ([]BreakGlass, error)
AppendAccessAudit(context.Context, AuditEvent) error
AccessAudit(context.Context, string, int) ([]AuditEvent, error)
}
type Options struct {
Random io.Reader
Now func() time.Time
}
type Service struct {
repository Repository
policy Policy
random io.Reader
now func() time.Time
}
func New(repository Repository, policy Policy, options Options) (*Service, error) {
if repository == nil {
return nil, errors.New("access: repository is required")
}
if err := policy.Validate(); err != nil {
return nil, err
}
if options.Random == nil {
options.Random = rand.Reader
}
if options.Now == nil {
options.Now = time.Now
}
return &Service{repository: repository, policy: policy, random: options.Random, now: options.Now}, nil
}
func (service *Service) Seed(ctx context.Context) error {
return service.repository.SeedAccessPolicy(ctx, service.policy)
}
type Grant struct {
SubjectKind SubjectKind
SubjectID string
Role string
Scope Scope
GrantedBy string
}
func (service *Service) Grant(ctx context.Context, input Grant) (Binding, error) {
if (input.SubjectKind != User && input.SubjectKind != Team) || !idPattern.MatchString(input.SubjectID) || !idPattern.MatchString(input.GrantedBy) {
return Binding{}, errors.New("access: invalid binding subject")
}
if _, ok := service.policy.Roles[input.Role]; !ok {
return Binding{}, errors.New("access: unknown role")
}
if err := input.Scope.Validate(); err != nil {
return Binding{}, err
}
id, err := randomID(service.random)
if err != nil {
return Binding{}, err
}
binding := Binding{ID: id, SubjectKind: input.SubjectKind, SubjectID: input.SubjectID, Role: input.Role, Scope: input.Scope, GrantedBy: input.GrantedBy, GrantedAt: service.now().UTC()}
if err = service.repository.Grant(ctx, binding); err != nil {
return Binding{}, err
}
return binding, nil
}
type Decision struct {
Allowed bool
Source string
Role string
}
func (service *Service) Authorize(ctx context.Context, userID string, scope Scope, permission string) (Decision, error) {
if !idPattern.MatchString(userID) || !namePattern.MatchString(permission) {
return Decision{}, errors.New("access: invalid authorization request")
}
if err := scope.Validate(); err != nil {
return Decision{}, err
}
if _, ok := service.policy.Permissions[permission]; !ok {
return Decision{}, errors.New("access: unknown permission")
}
bindings, err := service.repository.EffectiveBindings(ctx, scope.OrganizationID, userID)
if err != nil {
return Decision{}, err
}
sort.Slice(bindings, func(i, j int) bool { return bindings[i].ID < bindings[j].ID })
for _, binding := range bindings {
if !binding.Scope.contains(scope) {
continue
}
for _, granted := range service.policy.Grants[binding.Role] {
if granted == permission {
return Decision{Allowed: true, Source: "role", Role: binding.Role}, nil
}
}
}
breakGlass, err := service.repository.ActiveBreakGlass(ctx, scope.OrganizationID, userID, service.now().UTC())
if err != nil {
return Decision{}, err
}
for _, grant := range breakGlass {
if grant.Permission == permission {
return Decision{Allowed: true, Source: "break_glass"}, nil
}
}
return Decision{}, nil
}
func (service *Service) ActivateBreakGlass(ctx context.Context, organizationID, userID, permission, reason, requestID string, lifetime time.Duration) (BreakGlass, error) {
if !idPattern.MatchString(organizationID) || !idPattern.MatchString(userID) || !namePattern.MatchString(permission) || !text(strings.TrimSpace(reason), 1024, false) || !text(requestID, 128, true) || lifetime < time.Minute || lifetime > time.Hour {
return BreakGlass{}, errors.New("access: invalid break-glass request")
}
if _, ok := service.policy.Permissions[permission]; !ok {
return BreakGlass{}, errors.New("access: unknown permission")
}
id, err := randomID(service.random)
if err != nil {
return BreakGlass{}, err
}
auditID, err := randomID(service.random)
if err != nil {
return BreakGlass{}, err
}
now := service.now().UTC()
grant := BreakGlass{ID: id, OrganizationID: organizationID, UserID: userID, Permission: permission, Reason: strings.TrimSpace(reason), CreatedAt: now, ExpiresAt: now.Add(lifetime)}
audit := AuditEvent{ID: auditID, OrganizationID: organizationID, ActorUserID: userID, Action: "break_glass.activate", ResourceType: "organization", ResourceID: organizationID, RequestID: requestID, Summary: "Temporary emergency access activated", CreatedAt: now}
if err = service.repository.CreateBreakGlass(ctx, grant, audit); err != nil {
return BreakGlass{}, err
}
return grant, nil
}
func (service *Service) Audit(ctx context.Context, organizationID string, limit int) ([]AuditEvent, error) {
if !idPattern.MatchString(organizationID) || limit < 1 || limit > 1000 {
return nil, errors.New("access: invalid audit query")
}
return service.repository.AccessAudit(ctx, organizationID, limit)
}
func randomID(random io.Reader) (string, error) {
value := make([]byte, 18)
if _, err := io.ReadFull(random, value); err != nil {
return "", fmt.Errorf("access: secure randomness unavailable: %w", err)
}
return base64.RawURLEncoding.EncodeToString(value), nil
}
func text(value string, limit int, emptyOK bool) bool {
return (emptyOK || value != "") && len(value) <= limit && !strings.ContainsAny(value, "\x00\r\n")
}