package registry import ( "encoding/json" "fmt" "os" "path/filepath" "sort" "github.com/netkingdom/flex-auth/pkg/api" ) // Store is a deterministic local registry for standalone development and tests. type Store struct { systems map[string]api.ProtectedSystemManifest resourceManifests map[string]api.ResourceManifest resources map[string]api.Resource subjects map[string]api.Subject groups map[string]api.Group teams map[string]api.Team tenants map[string]api.Tenant relationships map[string]api.RelationshipFact } // Snapshot is the file-backed representation of a Store. type Snapshot struct { Systems []api.ProtectedSystemManifest `json:"systems,omitempty"` ResourceManifests []api.ResourceManifest `json:"resource_manifests,omitempty"` Subjects []api.Subject `json:"subjects,omitempty"` Groups []api.Group `json:"groups,omitempty"` Teams []api.Team `json:"teams,omitempty"` Tenants []api.Tenant `json:"tenants,omitempty"` Relationships []api.RelationshipFact `json:"relationships,omitempty"` } // NewStore returns an empty local registry store. func NewStore() *Store { return &Store{ systems: make(map[string]api.ProtectedSystemManifest), resourceManifests: make(map[string]api.ResourceManifest), resources: make(map[string]api.Resource), subjects: make(map[string]api.Subject), groups: make(map[string]api.Group), teams: make(map[string]api.Team), tenants: make(map[string]api.Tenant), relationships: make(map[string]api.RelationshipFact), } } // LoadFile loads a Store from a JSON snapshot file. func LoadFile(path string) (*Store, error) { data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("read registry snapshot: %w", err) } var snapshot Snapshot if err := json.Unmarshal(data, &snapshot); err != nil { return nil, fmt.Errorf("unmarshal registry snapshot: %w", err) } store := NewStore() if err := store.ApplySnapshot(snapshot); err != nil { return nil, err } return store, nil } // SaveFile writes the store as deterministic pretty-printed JSON. func (s *Store) SaveFile(path string) error { if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { return fmt.Errorf("create registry snapshot directory: %w", err) } data, err := json.MarshalIndent(s.Snapshot(), "", " ") if err != nil { return fmt.Errorf("marshal registry snapshot: %w", err) } data = append(data, '\n') if err := os.WriteFile(path, data, 0o644); err != nil { return fmt.Errorf("write registry snapshot: %w", err) } return nil } // ApplySnapshot imports every record from a snapshot into the store. func (s *Store) ApplySnapshot(snapshot Snapshot) error { for _, system := range snapshot.Systems { if err := s.PutProtectedSystem(system); err != nil { return err } } for _, manifest := range snapshot.ResourceManifests { if err := s.ImportResourceManifest(manifest); err != nil { return err } } for _, tenant := range snapshot.Tenants { if err := s.PutTenant(tenant); err != nil { return err } } for _, subject := range snapshot.Subjects { if err := s.PutSubject(subject); err != nil { return err } } for _, group := range snapshot.Groups { if err := s.PutGroup(group); err != nil { return err } } for _, team := range snapshot.Teams { if err := s.PutTeam(team); err != nil { return err } } for _, relationship := range snapshot.Relationships { if err := s.PutRelationship(relationship); err != nil { return err } } return nil } // Snapshot returns a deterministic store snapshot. func (s *Store) Snapshot() Snapshot { return Snapshot{ Systems: sortedValues(s.systems), ResourceManifests: sortedValues(s.resourceManifests), Subjects: sortedValues(s.subjects), Groups: sortedValues(s.groups), Teams: sortedValues(s.teams), Tenants: sortedValues(s.tenants), Relationships: sortedValues(s.relationships), } } // PutProtectedSystem stores or replaces a protected system manifest. func (s *Store) PutProtectedSystem(system api.ProtectedSystemManifest) error { if system.ID == "" { return fmt.Errorf("protected system id is required") } s.systems[system.ID] = system return nil } // ImportResourceManifest stores a resource manifest and indexes its resources. func (s *Store) ImportResourceManifest(manifest api.ResourceManifest) error { if manifest.ID == "" { return fmt.Errorf("resource manifest id is required") } if manifest.System == "" { return fmt.Errorf("resource manifest system is required") } for _, resource := range manifest.Resources { if resource.ID == "" || resource.Type == "" { return fmt.Errorf("resource manifest %q contains resource with missing id or type", manifest.ID) } s.resources[resourceKey(manifest.System, resource.ID)] = resource } s.resourceManifests[manifest.ID] = manifest return nil } // ImportSubjectManifest stores subjects, groups, teams, and tenants. func (s *Store) ImportSubjectManifest(manifest api.SubjectManifest) error { if manifest.ID == "" { return fmt.Errorf("subject manifest id is required") } for _, tenant := range manifest.Tenants { if err := s.PutTenant(tenant); err != nil { return err } } for _, subject := range manifest.Subjects { if err := s.PutSubject(subject); err != nil { return err } } for _, group := range manifest.Groups { if err := s.PutGroup(group); err != nil { return err } } for _, team := range manifest.Teams { if err := s.PutTeam(team); err != nil { return err } } return nil } // PutSubject stores or replaces a subject. func (s *Store) PutSubject(subject api.Subject) error { if subject.ID == "" { return fmt.Errorf("subject id is required") } s.subjects[subject.ID] = subject return nil } // PutGroup stores or replaces a group. func (s *Store) PutGroup(group api.Group) error { if group.ID == "" { return fmt.Errorf("group id is required") } s.groups[group.ID] = group return nil } // PutTeam stores or replaces a team. func (s *Store) PutTeam(team api.Team) error { if team.ID == "" { return fmt.Errorf("team id is required") } s.teams[team.ID] = team return nil } // PutTenant stores or replaces a tenant. func (s *Store) PutTenant(tenant api.Tenant) error { if tenant.ID == "" { return fmt.Errorf("tenant id is required") } s.tenants[tenant.ID] = tenant return nil } // PutRelationship stores or replaces a relationship fact. func (s *Store) PutRelationship(relationship api.RelationshipFact) error { if relationship.ID == "" { return fmt.Errorf("relationship id is required") } if relationship.Subject == "" || relationship.Relation == "" || relationship.Object == "" { return fmt.Errorf("relationship %q requires subject, relation, and object", relationship.ID) } s.relationships[relationship.ID] = relationship return nil } // Resource looks up a resource by protected system and resource id. func (s *Store) Resource(system, id string) (api.Resource, bool) { resource, ok := s.resources[resourceKey(system, id)] return resource, ok } // ResourceRefs returns deterministic resource references filtered by system and // resource type. Empty filters match all values. func (s *Store) ResourceRefs(system, resourceType string) []api.ResourceRef { keys := make([]string, 0, len(s.resources)) for key, resource := range s.resources { resourceSystem, _ := splitResourceKey(key) if system != "" && resourceSystem != system { continue } if resourceType != "" && resource.Type != resourceType { continue } keys = append(keys, key) } sort.Strings(keys) refs := make([]api.ResourceRef, 0, len(keys)) for _, key := range keys { resourceSystem, _ := splitResourceKey(key) resource := s.resources[key] refs = append(refs, api.ResourceRef{ ID: resource.ID, Type: resource.Type, System: resourceSystem, Attributes: resourceRefAttributes(resource), }) } return refs } // Subject looks up a subject by id. func (s *Store) Subject(id string) (api.Subject, bool) { subject, ok := s.subjects[id] return subject, ok } // Group looks up a group by id. func (s *Store) Group(id string) (api.Group, bool) { group, ok := s.groups[id] return group, ok } // Team looks up a team by id. func (s *Store) Team(id string) (api.Team, bool) { team, ok := s.teams[id] return team, ok } // Tenant looks up a tenant by id. func (s *Store) Tenant(id string) (api.Tenant, bool) { tenant, ok := s.tenants[id] return tenant, ok } // RelationshipsForSubject returns deterministic relationship facts for a subject. func (s *Store) RelationshipsForSubject(subject string) []api.RelationshipFact { return s.relationshipsWhere(func(fact api.RelationshipFact) bool { return fact.Subject == subject }) } // RelationshipsForObject returns deterministic relationship facts for an object. func (s *Store) RelationshipsForObject(object string) []api.RelationshipFact { return s.relationshipsWhere(func(fact api.RelationshipFact) bool { return fact.Object == object }) } func (s *Store) relationshipsWhere(match func(api.RelationshipFact) bool) []api.RelationshipFact { var out []api.RelationshipFact for _, fact := range s.relationships { if match(fact) { out = append(out, fact) } } sort.Slice(out, func(i, j int) bool { return out[i].ID < out[j].ID }) return out } func resourceKey(system, id string) string { return system + "\x00" + id } func splitResourceKey(key string) (string, string) { for i := range key { if key[i] == '\x00' { return key[:i], key[i+1:] } } return "", key } func resourceRefAttributes(resource api.Resource) map[string]any { attrs := make(map[string]any, len(resource.Attributes)+5) for key, value := range resource.Attributes { attrs[key] = value } if resource.Path != "" { attrs["path"] = resource.Path } if resource.Parent != "" { attrs["parent"] = resource.Parent } if len(resource.Labels) > 0 { attrs["labels"] = resource.Labels } if resource.TrustZone != "" { attrs["trust_zone"] = resource.TrustZone } if resource.Owner != "" { attrs["owner"] = resource.Owner } return attrs } func sortedValues[T any](items map[string]T) []T { keys := make([]string, 0, len(items)) for key := range items { keys = append(keys, key) } sort.Strings(keys) out := make([]T, 0, len(keys)) for _, key := range keys { out = append(out, items[key]) } return out }