package relationship import ( "fmt" "sort" "strings" "github.com/netkingdom/flex-auth/internal/registry" "github.com/netkingdom/flex-auth/pkg/api" ) // SnapshotToTuples maps flex-auth registry data to relationship tuples. func SnapshotToTuples(snapshot registry.Snapshot) []Tuple { index := newSnapshotIndex(snapshot) tuples := map[string]Tuple{} addTuple := func(tuple Tuple) { if tuple.ObjectType == "" || tuple.ObjectID == "" || tuple.Relation == "" || tuple.SubjectType == "" || tuple.SubjectID == "" { return } tuples[tupleKey(tuple)] = tuple } for _, group := range snapshot.Groups { for _, member := range group.Members { addTuple(Tuple{ ObjectType: "group", ObjectID: group.ID, Relation: "member", SubjectType: index.subjectType(member), SubjectID: index.subjectID(member), }) } } for _, team := range snapshot.Teams { for _, member := range team.Members { addTuple(Tuple{ ObjectType: "group", ObjectID: teamObjectID(team.ID), Relation: "member", SubjectType: index.subjectType(member), SubjectID: index.subjectID(member), }) } } for _, manifest := range snapshot.ResourceManifests { for _, resource := range manifest.Resources { if resource.Parent != "" { addTuple(Tuple{ ObjectType: resource.Type, ObjectID: resource.ID, Relation: "parent", SubjectType: index.resourceType(resource.Parent), SubjectID: resource.Parent, Metadata: map[string]any{ "system": manifest.System, }, }) } if resource.Owner != "" { addTuple(Tuple{ ObjectType: resource.Type, ObjectID: resource.ID, Relation: "owner_team", SubjectType: "group", SubjectID: teamOrGroupObjectID(resource.Owner), Metadata: map[string]any{ "system": manifest.System, }, }) } } } for _, relationship := range snapshot.Relationships { subjectType := index.subjectType(relationship.Subject) subjectRelation := "" if subjectType == "group" && relationship.Relation != "member" && relationship.Relation != "owner_team" { subjectRelation = "member" } addTuple(Tuple{ ObjectType: index.resourceType(relationship.Object), ObjectID: relationship.Object, Relation: relationship.Relation, SubjectType: subjectType, SubjectID: index.subjectID(relationship.Subject), SubjectRelation: subjectRelation, Conditions: append([]api.Condition(nil), relationship.Conditions...), Caring: relationship.Caring, Provenance: copyMap(relationship.Provenance), Metadata: copyMap(relationship.Metadata), }) } return sortedTuples(tuples) } type snapshotIndex struct { groups map[string]api.Group teams map[string]api.Team resourceTypes map[string]string } func newSnapshotIndex(snapshot registry.Snapshot) snapshotIndex { index := snapshotIndex{ groups: make(map[string]api.Group), teams: make(map[string]api.Team), resourceTypes: make(map[string]string), } 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 tupleKey(tuple Tuple) string { return fmt.Sprintf( "%s\x00%s\x00%s\x00%s\x00%s\x00%s", tuple.ObjectType, tuple.ObjectID, tuple.Relation, tuple.SubjectType, tuple.SubjectID, tuple.SubjectRelation, ) } func sortedTuples(tuples map[string]Tuple) []Tuple { keys := make([]string, 0, len(tuples)) for key := range tuples { keys = append(keys, key) } sort.Strings(keys) out := make([]Tuple, 0, len(keys)) for _, key := range keys { out = append(out, tuples[key]) } return out } 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) }