Files
flex-auth/internal/registry/store.go
tegwick 3c4f8fc2b4
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
Implement local registry store
2026-05-17 05:10:17 +02:00

315 lines
8.9 KiB
Go

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
}
// 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 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
}