package topaz import ( "fmt" "sort" "strings" "github.com/netkingdom/flex-auth/internal/registry" "github.com/netkingdom/flex-auth/pkg/api" ) // SnapshotToDirectory converts flex-auth's canonical registry snapshot into // Topaz directory objects and relations. func SnapshotToDirectory(snapshot registry.Snapshot) DirectorySnapshot { index := newSnapshotIndex(snapshot) objects := map[string]DirectoryObject{} relations := map[string]DirectoryRelation{} addObject := func(object DirectoryObject) { if object.Type == "" || object.ID == "" { return } objects[objectKey(object.Type, object.ID)] = object } addRelation := func(relation DirectoryRelation) { if relation.ObjectType == "" || relation.ObjectID == "" || relation.Relation == "" || relation.SubjectType == "" || relation.SubjectID == "" { return } relations[relationKey(relation)] = relation } for _, tenant := range snapshot.Tenants { addObject(DirectoryObject{Type: "tenant", ID: tenant.ID, DisplayName: tenant.Name, Properties: copyStringAnyMap(tenant.Metadata)}) } for _, subject := range snapshot.Subjects { properties := copyStringAnyMap(subject.Metadata) addProperty(properties, "principal_type", principalType(subject.Type)) addProperty(properties, "organization_relation", subject.OrganizationRelation) addProperty(properties, "roles", subject.Roles) addProperty(properties, "tenant", subject.Tenant) addProperties(properties, subject.Claims) addObject(DirectoryObject{Type: "user", ID: subject.ID, DisplayName: subject.DisplayName, Properties: properties}) identityID := identityObjectID(subject.ID) addObject(DirectoryObject{ Type: "identity", ID: identityID, Properties: map[string]any{ "identifier": subject.ID, "subject": subject.ID, }, }) addRelation(DirectoryRelation{ ObjectType: "identity", ObjectID: identityID, Relation: "identifier", SubjectType: "user", SubjectID: subject.ID, }) } for _, group := range snapshot.Groups { properties := copyStringAnyMap(group.Metadata) addProperty(properties, "tenant", group.Tenant) addObject(DirectoryObject{Type: "group", ID: group.ID, DisplayName: group.DisplayName, Properties: properties}) for _, member := range group.Members { addRelation(DirectoryRelation{ ObjectType: "group", ObjectID: group.ID, Relation: "member", SubjectType: index.subjectType(member), SubjectID: index.subjectID(member), }) } } for _, team := range snapshot.Teams { teamID := teamObjectID(team.ID) properties := copyStringAnyMap(team.Metadata) addProperty(properties, "kind", "team") addProperty(properties, "flex_auth_id", team.ID) addProperty(properties, "tenant", team.Tenant) addObject(DirectoryObject{Type: "group", ID: teamID, DisplayName: team.DisplayName, Properties: properties}) for _, member := range team.Members { addRelation(DirectoryRelation{ ObjectType: "group", ObjectID: teamID, Relation: "member", SubjectType: index.subjectType(member), SubjectID: index.subjectID(member), }) } } for _, manifest := range snapshot.ResourceManifests { for _, resource := range manifest.Resources { properties := copyStringAnyMap(resource.Attributes) addProperty(properties, "system", manifest.System) addProperty(properties, "path", resource.Path) addProperty(properties, "parent", resource.Parent) addProperty(properties, "labels", resource.Labels) addProperty(properties, "trust_zone", resource.TrustZone) addProperty(properties, "owner", resource.Owner) addObject(DirectoryObject{Type: resource.Type, ID: resource.ID, Properties: properties}) if resource.Parent != "" { addRelation(DirectoryRelation{ ObjectType: resource.Type, ObjectID: resource.ID, Relation: "parent", SubjectType: index.resourceType(resource.Parent), SubjectID: resource.Parent, }) } if resource.Owner != "" { addRelation(DirectoryRelation{ ObjectType: resource.Type, ObjectID: resource.ID, Relation: "owner_team", SubjectType: "group", SubjectID: teamOrGroupObjectID(resource.Owner), }) } } } for _, relationship := range snapshot.Relationships { subjectType := index.subjectType(relationship.Subject) subjectRelation := "" if subjectType == "group" && relationship.Relation != "member" && relationship.Relation != "owner_team" { subjectRelation = "member" } addRelation(DirectoryRelation{ ObjectType: index.resourceType(relationship.Object), ObjectID: relationship.Object, Relation: relationship.Relation, SubjectType: subjectType, SubjectID: index.subjectID(relationship.Subject), SubjectRelation: subjectRelation, }) } return DirectorySnapshot{ Objects: sortedObjects(objects), Relations: sortedRelations(relations), } } type snapshotIndex struct { subjects map[string]api.Subject groups map[string]api.Group teams map[string]api.Team resourceTypes map[string]string } func newSnapshotIndex(snapshot registry.Snapshot) snapshotIndex { index := snapshotIndex{ subjects: make(map[string]api.Subject), groups: make(map[string]api.Group), teams: make(map[string]api.Team), resourceTypes: make(map[string]string), } for _, subject := range snapshot.Subjects { index.subjects[subject.ID] = subject } for _, group := range snapshot.Groups { index.groups[group.ID] = group } for _, team := range snapshot.Teams { index.teams[team.ID] = team index.teams[teamObjectID(team.ID)] = team } for _, manifest := range snapshot.ResourceManifests { for _, resource := range manifest.Resources { index.resourceTypes[resource.ID] = resource.Type } } return index } func (i snapshotIndex) subjectType(id string) string { if _, ok := i.groups[id]; ok { return "group" } if _, ok := i.teams[id]; ok { return "group" } if strings.HasPrefix(id, "group:") || strings.HasPrefix(id, "team:") || strings.HasPrefix(id, "reader:") { return "group" } if resourceType := i.resourceType(id); resourceType != "" { return resourceType } return "user" } func (i snapshotIndex) subjectID(id string) string { if _, ok := i.teams[id]; ok { return teamObjectID(id) } return id } func (i snapshotIndex) resourceType(id string) string { if resourceType := i.resourceTypes[id]; resourceType != "" { return resourceType } if inferred := inferTypeFromID(id); inferred != "" { return inferred } return "resource" } func objectKey(objectType, objectID string) string { return objectType + "\x00" + objectID } func relationKey(relation DirectoryRelation) string { return fmt.Sprintf( "%s\x00%s\x00%s\x00%s\x00%s\x00%s", relation.ObjectType, relation.ObjectID, relation.Relation, relation.SubjectType, relation.SubjectID, relation.SubjectRelation, ) } func sortedObjects(objects map[string]DirectoryObject) []DirectoryObject { keys := make([]string, 0, len(objects)) for key := range objects { keys = append(keys, key) } sort.Strings(keys) out := make([]DirectoryObject, 0, len(keys)) for _, key := range keys { out = append(out, objects[key]) } return out } func sortedRelations(relations map[string]DirectoryRelation) []DirectoryRelation { keys := make([]string, 0, len(relations)) for key := range relations { keys = append(keys, key) } sort.Strings(keys) out := make([]DirectoryRelation, 0, len(keys)) for _, key := range keys { out = append(out, relations[key]) } return out } func identityObjectID(subjectID string) string { return "identity:" + subjectID } func teamObjectID(id string) string { if strings.HasPrefix(id, "team:") { return id } return "team:" + id } func teamOrGroupObjectID(id string) string { if strings.HasPrefix(id, "team:") || strings.HasPrefix(id, "group:") || strings.HasPrefix(id, "reader:") { return id } return teamObjectID(id) } func principalType(subjectType api.SubjectType) string { switch subjectType { case api.SubjectTypeService, api.SubjectTypeAutomation, api.SubjectTypeAgent: return "service" case api.SubjectTypeHuman: return "human" case "": return "" default: return strings.ToLower(string(subjectType)) } } func copyStringAnyMap(in map[string]any) map[string]any { out := make(map[string]any, len(in)) for key, value := range in { out[key] = value } return out } func addProperties(target map[string]any, attrs map[string]any) { for key, value := range attrs { addProperty(target, key, value) } } func addProperty(target map[string]any, key string, value any) { if target == nil || emptyProperty(value) { return } if _, exists := target[key]; !exists { target[key] = value } } func emptyProperty(value any) bool { switch typed := value.(type) { case string: return typed == "" case api.OrganizationRelation: return typed == "" case []string: return len(typed) == 0 case []api.CanonicalRole: return len(typed) == 0 default: return value == nil } }