restructure and process additions and deleteions of relationships, roles and memberships

This commit is contained in:
Mike Mason
2023-07-17 15:36:52 +00:00
parent 10f45c56a0
commit 2f9f0675f9
9 changed files with 441 additions and 218 deletions

View File

@@ -0,0 +1,71 @@
package permissions
import (
"context"
"fmt"
"net/http"
"net/url"
"go.infratographer.com/x/gidx"
)
type resourceRelationship struct {
ResourceID string `json:"resource_id"`
Relation string `json:"relation"`
SubjectID string `json:"subject_id"`
}
type ResourceRelationship struct {
ResourceID gidx.PrefixedID
Relation string
SubjectID gidx.PrefixedID
}
func (c *Client) ListResourceRelationships(ctx context.Context, resourceID gidx.PrefixedID, relatedResourceType string) ([]ResourceRelationship, error) {
query := url.Values{
"resourceType": []string{relatedResourceType},
}
url := url.URL{
Path: fmt.Sprintf("/api/v1/resources/%s/relationships", resourceID.String()),
RawQuery: query.Encode(),
}
var response struct {
Data []resourceRelationship `json:"data"`
}
if _, err := c.DoRequest(ctx, http.MethodGet, url.String(), nil, &response); err != nil {
return nil, err
}
data := make([]ResourceRelationship, len(response.Data))
for i, entry := range response.Data {
var (
resID, subID gidx.PrefixedID
err error
)
if entry.ResourceID != "" {
resID, err = gidx.Parse(entry.ResourceID)
if err != nil {
return nil, err
}
}
if entry.SubjectID != "" {
subID, err = gidx.Parse(entry.SubjectID)
if err != nil {
return nil, err
}
}
data[i] = ResourceRelationship{
ResourceID: resID,
Relation: entry.Relation,
SubjectID: subID,
}
}
return data, nil
}

View File

@@ -39,7 +39,7 @@ func WithPermissionsClient(client *permissions.Client) Option {
}
// WithPrefixMap sets the id prefix map relating id prefixes to type names.
func WithPrefixMap(idMap map[string]string) Option {
func WithPrefixMap(idMap map[string]ObjectType) Option {
return func(s *service) error {
s.idPrefixMap = idMap

View File

@@ -13,14 +13,12 @@ const organizationEvent = "metalorganization"
func (s *service) buildOrganizationRelationships(org *models.OrganizationDetails) (Relationships, error) {
relations := Relationships{
Relationships: []Relationship{
// Related org to the root tenant.
{
Resource: org,
Parent: Relation{
Relation: RelateParent,
RelatedResource: s.rootResource,
},
Resource: s.rootResource,
},
SubjectType: TypeProject,
}
for _, member := range org.Memberships {
@@ -32,7 +30,6 @@ func (s *service) buildOrganizationRelationships(org *models.OrganizationDetails
}
relations.Memberships = append(relations.Memberships, ResourceMemberships{
Resource: org,
Role: role,
Member: member.User,
})
@@ -40,27 +37,10 @@ func (s *service) buildOrganizationRelationships(org *models.OrganizationDetails
}
for _, project := range org.Projects {
relations.Relationships = append(relations.Relationships, Relationship{
relations.SubjectRelationships = append(relations.SubjectRelationships, Relation{
Resource: project,
Relation: RelateParent,
RelatedResource: org,
})
for _, member := range project.Memberships {
for _, role := range member.Roles {
if _, ok := s.roles[role]; !ok {
s.logger.Warnf("unrecognized project role '%s' for %s on %s", role, member.User.PrefixedID(), project.PrefixedID())
continue
}
relations.Memberships = append(relations.Memberships, ResourceMemberships{
Resource: project,
Role: role,
Member: member.User,
})
}
}
}
return relations, nil
@@ -91,10 +71,15 @@ func (s *service) TouchOrganization(ctx context.Context, id gidx.PrefixedID) err
return err
}
s.processRelationships(ctx, organizationEvent, relationships.Relationships)
s.syncMemberships(ctx, relationships.Memberships)
relationshipChanges := s.processRelationships(ctx, organizationEvent, relationships)
rolesChanged, assignmentsChanged := s.syncMemberships(ctx, relationships, false)
s.logger.Infow("organization sync complete", "relationships", len(relationships.Relationships), "memberships", len(relationships.Memberships))
s.logger.Infow("organization sync complete",
"resource.id", org.PrefixedID(),
"relationships.changed", relationshipChanges,
"membership.roles_changed", rolesChanged,
"membership.assignments_changed", assignmentsChanged,
)
return nil
}

View File

@@ -10,40 +10,29 @@ import (
"go.equinixmetal.net/infra9-metal-bridge/internal/permissions"
)
func (s *service) syncMemberships(ctx context.Context, memberships []ResourceMemberships) error {
if len(memberships) == 0 {
return nil
func (s *service) syncMemberships(ctx context.Context, relationships Relationships, skipDeletions bool) (int, int) {
if len(relationships.Memberships) == 0 {
return 0, 0
}
resourceMap := make(map[gidx.PrefixedID][]ResourceMemberships)
rlogger := s.logger.With("resource.id", relationships.Resource.PrefixedID())
resourceRoleIDs := make(map[gidx.PrefixedID]map[string]gidx.PrefixedID)
resourceRoleCreations := make(map[gidx.PrefixedID]map[string][]string)
resourceRoleDeletions := make(map[gidx.PrefixedID][]gidx.PrefixedID)
resourceRoleAssignments := make(map[gidx.PrefixedID]map[string][]gidx.PrefixedID)
resourceRoleAssignmentRemovals := make(map[gidx.PrefixedID]map[string][]gidx.PrefixedID)
roleIDs := make(map[string]gidx.PrefixedID)
var (
totalRoleCreate, totalRoleDelete int
totalRoleAssign, totalRoleUnassign int
)
for _, membership := range memberships {
resourceID := membership.Resource.PrefixedID()
wantRoles, wantAssignments := s.mapResourceWants(relationships.Memberships)
resourceMap[resourceID] = append(resourceMap[resourceID], membership)
}
for resourceID, memberships := range resourceMap {
resourceRoleIDs[resourceID] = make(map[string]gidx.PrefixedID)
wantRoles, wantAssignments := s.mapResourceWants(memberships)
liveRoles, liveAssignments, err := s.mapResourceDetails(ctx, resourceID)
liveRoles, liveAssignments, err := s.mapResourceDetails(ctx, relationships.Resource.PrefixedID())
if err != nil {
return err
rlogger.Errorw("failed to get membership resource details map",
"error", err,
)
return 0, 0
}
roleCreations := make(map[string][]string)
@@ -56,15 +45,17 @@ func (s *service) syncMemberships(ctx context.Context, memberships []ResourceMem
}
for roleKey, role := range liveRoles {
if !skipDeletions {
if _, ok := wantRoles[roleKey]; !ok {
roleDeletions = append(roleDeletions, role.ID)
}
resourceRoleIDs[resourceID][roleKey] = role.ID
}
roleAssignments := make(map[string][]gidx.PrefixedID)
roleAssignmentRemovals := make(map[string][]gidx.PrefixedID)
roleIDs[roleKey] = role.ID
}
roleMembershipsAdd := make(map[string][]gidx.PrefixedID)
roleMembershipsRemove := make(map[string][]gidx.PrefixedID)
for roleKey, assignments := range wantAssignments {
for memberID := range assignments {
@@ -74,11 +65,12 @@ func (s *service) syncMemberships(ctx context.Context, memberships []ResourceMem
}
}
roleAssignments[roleKey] = append(roleAssignments[roleKey], memberID)
roleMembershipsAdd[roleKey] = append(roleMembershipsAdd[roleKey], memberID)
totalRoleAssign++
}
}
if !skipDeletions {
for roleKey, assignments := range liveAssignments {
for memberID := range assignments {
if _, ok := wantAssignments[roleKey]; ok {
@@ -87,22 +79,16 @@ func (s *service) syncMemberships(ctx context.Context, memberships []ResourceMem
}
}
roleAssignmentRemovals[roleKey] = append(roleAssignmentRemovals[roleKey], memberID)
roleMembershipsRemove[roleKey] = append(roleMembershipsRemove[roleKey], memberID)
totalRoleUnassign++
}
}
resourceRoleCreations[resourceID] = roleCreations
resourceRoleDeletions[resourceID] = roleDeletions
resourceRoleAssignments[resourceID] = roleAssignments
resourceRoleAssignmentRemovals[resourceID] = roleAssignmentRemovals
}
totalRoleCreate += len(roleCreations)
totalRoleDelete += len(roleDeletions)
}
s.logger.Debugw("processing memberships",
"resources", len(resourceMap),
rlogger.Debugw("processing memberships",
"role.create", totalRoleCreate,
"role.delete", totalRoleDelete,
"role.assign", totalRoleAssign,
@@ -114,21 +100,19 @@ func (s *service) syncMemberships(ctx context.Context, memberships []ResourceMem
roleAssignments, roleUnassignments int
)
for resourceID := range resourceMap {
rlogger := s.logger.With("resource.id", resourceID.String())
for roleKey, actions := range resourceRoleCreations[resourceID] {
roleID, err := s.perms.CreateRole(ctx, resourceID, actions)
for roleKey, actions := range roleCreations {
roleID, err := s.perms.CreateRole(ctx, relationships.Resource.PrefixedID(), actions)
if err != nil {
rlogger.Errorw("error creating role", "actions", actions, "error", err)
continue
}
resourceRoleIDs[resourceID][roleKey] = roleID
roleIDs[roleKey] = roleID
rolesCreated++
}
for _, roleID := range resourceRoleDeletions[resourceID] {
for _, roleID := range roleDeletions {
if err := s.perms.DeleteRole(ctx, roleID); err != nil {
rlogger.Errorw("error deleting role", "role.id", roleID, "error", err)
@@ -138,8 +122,8 @@ func (s *service) syncMemberships(ctx context.Context, memberships []ResourceMem
rolesDeleted++
}
for roleKey, members := range resourceRoleAssignments[resourceID] {
roleID, ok := resourceRoleIDs[resourceID][roleKey]
for roleKey, members := range roleMembershipsAdd {
roleID, ok := roleIDs[roleKey]
if !ok {
rlogger.Errorw("role id not found for role actions key", "role_actions_key", roleKey, "members", len(members))
@@ -157,8 +141,8 @@ func (s *service) syncMemberships(ctx context.Context, memberships []ResourceMem
}
}
for roleKey, members := range resourceRoleAssignmentRemovals[resourceID] {
roleID, ok := resourceRoleIDs[resourceID][roleKey]
for roleKey, members := range roleMembershipsRemove {
roleID, ok := roleIDs[roleKey]
if !ok {
rlogger.Errorw("role id not found for role actions key", "role_actions_key", roleKey, "members", len(members))
@@ -175,17 +159,15 @@ func (s *service) syncMemberships(ctx context.Context, memberships []ResourceMem
roleUnassignments++
}
}
}
s.logger.Debugw("memberships processed",
"resources", len(resourceMap),
rlogger.Debugw("memberships processed",
"role.create", rolesCreated,
"role.delete", rolesDeleted,
"role.assign", roleAssignments,
"role.unassign", roleUnassignments,
)
return nil
return rolesCreated + rolesDeleted, roleAssignments + roleUnassignments
}
func (s *service) mapResourceWants(memberships []ResourceMemberships) (map[string][]string, map[string]map[gidx.PrefixedID]bool) {

View File

@@ -5,27 +5,207 @@ import (
"go.infratographer.com/x/events"
"go.infratographer.com/x/gidx"
"go.equinixmetal.net/infra9-metal-bridge/internal/permissions"
)
func (s *service) processRelationships(ctx context.Context, subjectType string, relationships []Relationship) {
var err error
type relationshipStats struct {
parentCreated bool
parentsDeleted int
subjectRelationshipsCreated int
subjectRelationshipsDeleted int
}
for _, rel := range relationships {
err = s.publisher.PublishChange(ctx, subjectType, events.ChangeMessage{
SubjectID: rel.Resource.PrefixedID(),
func (s *service) processRelationships(ctx context.Context, eventType string, relationships Relationships) int {
rlogger := s.logger.With("resource.id", relationships.Resource.PrefixedID())
wantParentRelationship, wantSubjectRelationships := s.mapRelationWants(relationships)
liveParentRelationships, liveSubjectRelationships, err := s.getRelationshipMap(ctx, relationships.Resource, relationships.SubjectType)
if err != nil {
rlogger.Errorw("failed to get relationship map",
"relationships.subject_type", relationships.SubjectType,
"error", err,
)
return 0
}
var (
createParentRelationship *Relation
deleteParentRelationships []gidx.PrefixedID
foundParent bool
createSubjectRelationships []Relation
deleteSubjectRelationships []Relation
)
if wantParentRelationship != nil {
for subjID := range liveParentRelationships {
if subjID == wantParentRelationship.Resource.PrefixedID() {
foundParent = true
continue
}
deleteParentRelationships = append(deleteParentRelationships, subjID)
}
if !foundParent {
createParentRelationship = wantParentRelationship
}
} else {
for subjID := range liveParentRelationships {
deleteParentRelationships = append(deleteParentRelationships, subjID)
}
}
for resID, relation := range wantSubjectRelationships {
if _, ok := liveSubjectRelationships[resID]; ok {
continue
}
createSubjectRelationships = append(createSubjectRelationships, Relation{
Resource: prefixedID{resID},
Relation: relation,
})
}
for resID, relation := range liveSubjectRelationships {
if _, ok := wantSubjectRelationships[resID]; ok {
continue
}
deleteSubjectRelationships = append(deleteSubjectRelationships, Relation{
Resource: prefixedID{resID},
Relation: relation,
})
}
var processEvents []events.ChangeMessage
rlogger.Debugw("processing relationships",
"parent.create", createParentRelationship != nil,
"parent.delete", len(deleteParentRelationships),
"subject.create", len(createSubjectRelationships),
"subject.delete", len(deleteSubjectRelationships),
)
if createParentRelationship != nil {
processEvents = append(processEvents, events.ChangeMessage{
SubjectID: relationships.Resource.PrefixedID(),
EventType: string(events.CreateChangeType),
AdditionalSubjectIDs: []gidx.PrefixedID{
rel.RelatedResource.PrefixedID(),
createParentRelationship.Resource.PrefixedID(),
},
})
}
for _, relatedResourceID := range deleteParentRelationships {
processEvents = append(processEvents, events.ChangeMessage{
SubjectID: relationships.Resource.PrefixedID(),
EventType: string(events.DeleteChangeType),
AdditionalSubjectIDs: []gidx.PrefixedID{
relatedResourceID,
},
})
}
for _, relation := range createSubjectRelationships {
processEvents = append(processEvents, events.ChangeMessage{
SubjectID: relation.Resource.PrefixedID(),
EventType: string(events.CreateChangeType),
AdditionalSubjectIDs: []gidx.PrefixedID{
relationships.Resource.PrefixedID(),
},
})
}
for _, relation := range deleteSubjectRelationships {
processEvents = append(processEvents, events.ChangeMessage{
SubjectID: relation.Resource.PrefixedID(),
EventType: string(events.DeleteChangeType),
AdditionalSubjectIDs: []gidx.PrefixedID{
relationships.Resource.PrefixedID(),
},
})
}
for _, event := range processEvents {
err = s.publisher.PublishChange(ctx, eventType, event)
if err != nil {
s.logger.Errorw("error publishing change",
"subject_type", subjectType,
"resource.id", rel.Resource.PrefixedID(),
"related_resource.id", rel.RelatedResource.PrefixedID(),
rlogger.Errorw("error publishing change",
"subject_type", eventType,
"subject.id", event.SubjectID,
"event.type", event.EventType,
"additional_subject_ids", event.AdditionalSubjectIDs,
"error", err,
)
}
}
rlogger.Debugw("relationships processed",
"parent.create", createParentRelationship != nil,
"parent.delete", len(deleteParentRelationships),
"subject.create", len(createSubjectRelationships),
"subject.delete", len(deleteSubjectRelationships),
)
changes := len(deleteParentRelationships) + len(createSubjectRelationships) + len(deleteSubjectRelationships)
if createParentRelationship != nil {
changes++
}
return changes
}
func (s *service) mapRelationWants(relationships Relationships) (*Relation, map[gidx.PrefixedID]RelationshipType) {
var wantParent *Relation
wantSubject := make(map[gidx.PrefixedID]RelationshipType)
if relationships.Parent.Resource != nil {
wantParent = &relationships.Parent
}
for _, relationship := range relationships.SubjectRelationships {
wantSubject[relationship.Resource.PrefixedID()] = relationship.Relation
}
return wantParent, wantSubject
}
func (s *service) getRelationshipMap(ctx context.Context, resource IDPrefixableResource, relatedObjectType ObjectType) (map[gidx.PrefixedID]RelationshipType, map[gidx.PrefixedID]RelationshipType, error) {
liveResource, err := s.perms.ListResourceRelationships(ctx, resource.PrefixedID(), "")
if err != nil {
return nil, nil, err
}
var liveSubject []permissions.ResourceRelationship
if relatedObjectType != "" {
liveSubject, err = s.perms.ListResourceRelationships(ctx, resource.PrefixedID(), relatedObjectType.Prefix())
if err != nil {
return nil, nil, err
}
}
parents := make(map[gidx.PrefixedID]RelationshipType, len(liveResource))
for _, relationship := range liveResource {
if relationship.Relation != string(RelateParent) {
continue
}
parents[relationship.SubjectID] = RelationshipType(relationship.Relation)
}
subject := make(map[gidx.PrefixedID]RelationshipType, len(liveSubject))
for _, relationship := range liveSubject {
subject[relationship.ResourceID] = RelationshipType(relationship.Relation)
}
return parents, subject, nil
}

View File

@@ -13,13 +13,11 @@ const projectEvent = "metalproject"
func (s *service) buildProjectRelationships(project *models.ProjectDetails) (Relationships, error) {
relations := Relationships{
Relationships: []Relationship{
// Relate project to organization.
{
Resource: project,
// Relate project to organization.
Parent: Relation{
Resource: project.Organization,
Relation: RelateParent,
RelatedResource: project.Organization,
},
},
}
@@ -32,7 +30,6 @@ func (s *service) buildProjectRelationships(project *models.ProjectDetails) (Rel
}
relations.Memberships = append(relations.Memberships, ResourceMemberships{
Resource: project,
Role: role,
Member: member.User,
})
@@ -67,10 +64,15 @@ func (s *service) TouchProject(ctx context.Context, id gidx.PrefixedID) error {
return err
}
s.processRelationships(ctx, projectEvent, relationships.Relationships)
s.syncMemberships(ctx, relationships.Memberships)
relationshipChanges := s.processRelationships(ctx, projectEvent, relationships)
rolesChanged, assignmentsChanged := s.syncMemberships(ctx, relationships, false)
s.logger.Infow("project sync complete", "relationships", len(relationships.Relationships), "memberships", len(relationships.Memberships))
s.logger.Infow("project sync complete",
"resource.id", project.PrefixedID(),
"relationships.changed", relationshipChanges,
"membership.roles_changed", rolesChanged,
"membership.assignments_changed", assignmentsChanged,
)
return nil
}

View File

@@ -16,47 +16,19 @@ type IDPrefixableResource interface {
}
type Relationships struct {
Relationships []Relationship
Resource IDPrefixableResource
Parent Relation
SubjectType ObjectType
SubjectRelationships []Relation
Memberships []ResourceMemberships
}
func (r Relationships) DeDupe() Relationships {
rels := make(map[string]bool)
mems := make(map[string]bool)
var results Relationships
for _, rel := range r.Relationships {
key := rel.Resource.PrefixedID().String() + "/" + string(rel.Relation) + "/" + rel.RelatedResource.PrefixedID().String()
if _, ok := rels[key]; !ok {
rels[key] = true
results.Relationships = append(results.Relationships, rel)
}
}
for _, member := range r.Memberships {
key := member.Resource.PrefixedID().String() + "/" + member.Role + "/" + member.Member.PrefixedID().String()
if _, ok := mems[key]; !ok {
mems[key] = true
results.Memberships = append(results.Memberships, member)
}
}
return results
}
type Relationship struct {
Resource IDPrefixableResource
type Relation struct {
Relation RelationshipType
RelatedResource IDPrefixableResource
Resource IDPrefixableResource
}
type ResourceMemberships struct {
Resource IDPrefixableResource
Role string
Member IDPrefixableResource
}

View File

@@ -13,20 +13,39 @@ import (
const (
// TypeOrganization defines the organization type.
TypeOrganization = "organization"
TypeOrganization ObjectType = "organization"
// TypeProject defines the project type.
TypeProject = "project"
TypeProject ObjectType = "project"
// TypeUser defines the user type.
TypeUser = "user"
TypeUser ObjectType = "user"
)
// DefaultPrefixMap is the default id prefix to type relationship.
var DefaultPrefixMap = map[string]string{
"metlorg": TypeOrganization,
"metlprj": TypeProject,
"metlusr": TypeUser,
var DefaultPrefixMap = map[string]ObjectType{
TypeOrganization.Prefix(): TypeOrganization,
TypeProject.Prefix(): TypeProject,
TypeUser.Prefix(): TypeUser,
}
type ObjectType string
func (t ObjectType) Prefix() string {
switch t {
case TypeOrganization:
return "metlorg"
case TypeProject:
return "metlprj"
case TypeUser:
return "metlusr"
default:
return ""
}
}
func (t ObjectType) String() string {
return string(t)
}
// Service defines a bridge service methods
@@ -63,7 +82,7 @@ type service struct {
publisher *events.Publisher
metal *metal.Client
perms *permissions.Client
idPrefixMap map[string]string
idPrefixMap map[string]ObjectType
rootResource prefixedID
roles map[string][]string
@@ -82,7 +101,7 @@ func New(publisher *events.Publisher, metal *metal.Client, perms *permissions.Cl
publisher: publisher,
metal: metal,
perms: perms,
idPrefixMap: make(map[string]string),
idPrefixMap: make(map[string]ObjectType),
}
for _, opt := range options {

View File

@@ -28,13 +28,16 @@ func (s *service) IsAssignableResource(id gidx.PrefixedID) bool {
}
func (s *service) AssignUser(ctx context.Context, userID gidx.PrefixedID, resourceIDs ...gidx.PrefixedID) error {
var memberships []ResourceMemberships
var totalResources, rolesChanged, assignmentsChanged int
mlogger := s.logger.With("member.id", userID.String())
memberID := prefixedID{userID}
for _, resourceID := range resourceIDs {
role, err := s.getUserResourceRole(ctx, userID, resourceID)
if err != nil {
s.logger.Warnw("failed to determine role for user resource", "error", err)
mlogger.Warnw("failed to determine role for user resource", "error", err)
continue
}
@@ -42,16 +45,25 @@ func (s *service) AssignUser(ctx context.Context, userID gidx.PrefixedID, resour
continue
}
memberships = append(memberships, ResourceMemberships{
roles, assignments := s.syncMemberships(ctx, Relationships{
Resource: prefixedID{resourceID},
Memberships: []ResourceMemberships{
{
Role: role,
Member: prefixedID{userID},
})
Member: memberID,
},
},
}, true)
totalResources++
rolesChanged += roles
assignmentsChanged += assignments
}
s.syncMemberships(ctx, memberships)
s.logger.Infow("assignment sync complete", "memberships", len(memberships))
mlogger.Infow("assignment sync complete",
"membership.roles_changed", rolesChanged,
"membership.assignments_changed", assignmentsChanged,
)
return nil
}