generated from coulomb/repo-seed
315 lines
8.9 KiB
Go
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
|
|
}
|