From 11fe8f8f2a1ba7b4c2500d19fbee38fc80ed0666 Mon Sep 17 00:00:00 2001 From: Mike Mason Date: Tue, 11 Jul 2023 21:32:30 +0000 Subject: [PATCH 01/33] add remaining org/proj/user initial sync --- internal/metal/metal.go | 13 ++++- internal/metal/providers/emapi/helpers.go | 27 ++++++++++ .../metal/providers/emapi/organizations.go | 12 +++-- internal/metal/providers/emapi/projects.go | 18 ++++--- internal/metal/providers/emapi/users.go | 22 ++++++-- internal/metal/providers/emgql/users.go | 11 +++- internal/metal/providers/provider.go | 5 +- internal/permissions/roles.go | 6 ++- internal/pubsub/subscriber.go | 4 +- internal/service/options.go | 2 +- internal/service/process.go | 1 + internal/service/projects.go | 54 +++++++++++++++++++ internal/service/relationships.go | 4 +- internal/service/service.go | 17 +++--- internal/service/users.go | 53 +++++++++++++++++- 15 files changed, 215 insertions(+), 34 deletions(-) create mode 100644 internal/metal/providers/emapi/helpers.go diff --git a/internal/metal/metal.go b/internal/metal/metal.go index f883070..1e567fc 100644 --- a/internal/metal/metal.go +++ b/internal/metal/metal.go @@ -3,10 +3,11 @@ package metal import ( "context" - "go.equinixmetal.net/infra9-metal-bridge/internal/metal/models" - provider "go.equinixmetal.net/infra9-metal-bridge/internal/metal/providers" "go.infratographer.com/x/gidx" "go.uber.org/zap" + + "go.equinixmetal.net/infra9-metal-bridge/internal/metal/models" + provider "go.equinixmetal.net/infra9-metal-bridge/internal/metal/providers" ) // Client is the Equinix Metal API Client struct. @@ -28,6 +29,14 @@ func (c *Client) GetUserDetails(ctx context.Context, id gidx.PrefixedID) (*model return c.provider.GetUserDetails(ctx, id) } +func (c *Client) GetUserOrganizationRole(ctx context.Context, userID, orgID gidx.PrefixedID) (string, error) { + return c.provider.GetUserOrganizationRole(ctx, userID, orgID) +} + +func (c *Client) GetUserProjectRole(ctx context.Context, userID, projID gidx.PrefixedID) (string, error) { + return c.provider.GetUserProjectRole(ctx, userID, projID) +} + // New creates a new Client. func New(options ...Option) (*Client, error) { client := new(Client) diff --git a/internal/metal/providers/emapi/helpers.go b/internal/metal/providers/emapi/helpers.go new file mode 100644 index 0000000..5b80e43 --- /dev/null +++ b/internal/metal/providers/emapi/helpers.go @@ -0,0 +1,27 @@ +package emapi + +import ( + "net/url" + "strings" +) + +// idOrLinkID returns the id if not empty, otherwise it plucks the last subpath from the link. +// An empty string is returned if nothing is defined or an error occurs. +func idOrLinkID(id, link string) string { + if id != "" || link == "" { + return id + } + + url, err := url.Parse(link) + if err != nil { + return "" + } + + parts := strings.Split(strings.TrimRight(url.Path, "/"), "/") + + if len(parts) != 0 { + return parts[len(parts)-1] + } + + return "" +} diff --git a/internal/metal/providers/emapi/organizations.go b/internal/metal/providers/emapi/organizations.go index 3b72944..639e44f 100644 --- a/internal/metal/providers/emapi/organizations.go +++ b/internal/metal/providers/emapi/organizations.go @@ -48,12 +48,18 @@ type Organization struct { } func (o *Organization) ToDetails() *models.OrganizationDetails { - if o == nil || o.ID == "" { + var id string + + if o != nil { + id = idOrLinkID(o.ID, o.HREF) + } + + if id == "" { return nil } details := &models.OrganizationDetails{ - ID: o.ID, + ID: id, Name: o.Name, Projects: o.Projects.ToDetails(), } @@ -66,7 +72,7 @@ func (o *Organization) ToDetails() *models.OrganizationDetails { func (c *Client) getOrganizationWithMemberships(ctx context.Context, id string) (*Organization, error) { var org Organization - _, err := c.DoRequest(ctx, http.MethodGet, organizationsPath+"/"+id+"?include=memberships.user,projects.memberships.user", nil, &org) + _, err := c.DoRequest(ctx, http.MethodGet, organizationsPath+"/"+id+"?include=memberships.user", nil, &org) if err != nil { return nil, fmt.Errorf("error loading organization: %w", err) } diff --git a/internal/metal/providers/emapi/projects.go b/internal/metal/providers/emapi/projects.go index f2c3e7c..71639a8 100644 --- a/internal/metal/providers/emapi/projects.go +++ b/internal/metal/providers/emapi/projects.go @@ -48,12 +48,18 @@ type Project struct { } func (p *Project) ToDetails() *models.ProjectDetails { - if p == nil || p.ID == "" { + var id string + + if p != nil { + id = idOrLinkID(p.ID, p.HREF) + } + + if id == "" { return nil } details := &models.ProjectDetails{ - ID: p.ID, + ID: id, Name: p.Name, Organization: p.Organization.ToDetails(), } @@ -63,19 +69,19 @@ func (p *Project) ToDetails() *models.ProjectDetails { return details } -func (c *Client) getProject(ctx context.Context, id string) (*Project, error) { +func (c *Client) getProjectWithMemberships(ctx context.Context, id string) (*Project, error) { var project Project - _, err := c.DoRequest(ctx, http.MethodGet, c.baseURL.JoinPath(projectsPath, id).String(), nil, &project) + _, err := c.DoRequest(ctx, http.MethodGet, projectsPath+"/"+id+"?include=memberships.user", nil, &project) if err != nil { - return nil, fmt.Errorf("error loading project: %w", err) + return nil, fmt.Errorf("error loading organization: %w", err) } return &project, nil } func (c *Client) GetProjectDetails(ctx context.Context, id gidx.PrefixedID) (*models.ProjectDetails, error) { - project, err := c.getProject(ctx, id.String()[gidx.PrefixPartLength+1:]) + project, err := c.getProjectWithMemberships(ctx, id.String()[gidx.PrefixPartLength+1:]) if err != nil { return nil, err } diff --git a/internal/metal/providers/emapi/users.go b/internal/metal/providers/emapi/users.go index 354985d..966cbef 100644 --- a/internal/metal/providers/emapi/users.go +++ b/internal/metal/providers/emapi/users.go @@ -47,12 +47,18 @@ type User struct { } func (u *User) ToDetails() *models.UserDetails { - if u.ID == "" { + var id string + + if u != nil { + id = idOrLinkID(u.ID, u.HREF) + } + + if id == "" { return nil } return &models.UserDetails{ - ID: u.ID, + ID: id, FullName: u.FullName, Organizations: nil, Projects: u.Projects.ToDetails(), @@ -62,9 +68,9 @@ func (u *User) ToDetails() *models.UserDetails { func (c *Client) getUser(ctx context.Context, id string) (*User, error) { var user User - _, err := c.DoRequest(ctx, http.MethodGet, c.baseURL.JoinPath(usersPath, id).String(), nil, &user) + _, err := c.DoRequest(ctx, http.MethodGet, usersPath+"/"+id, nil, &user) if err != nil { - return nil, fmt.Errorf("error loading user: %w", err) + return nil, fmt.Errorf("error loading organization: %w", err) } return &user, nil @@ -78,3 +84,11 @@ func (c *Client) GetUserDetails(ctx context.Context, id gidx.PrefixedID) (*model return user.ToDetails(), nil } + +func (c *Client) GetUserOrganizationRole(ctx context.Context, userID, orgID gidx.PrefixedID) (string, error) { + return "collaborator", nil +} + +func (c *Client) GetUserProjectRole(ctx context.Context, userID, projectID gidx.PrefixedID) (string, error) { + return "collaborator", nil +} diff --git a/internal/metal/providers/emgql/users.go b/internal/metal/providers/emgql/users.go index 04e2b4a..52e36c1 100644 --- a/internal/metal/providers/emgql/users.go +++ b/internal/metal/providers/emgql/users.go @@ -3,10 +3,19 @@ package emgql import ( "context" - "go.equinixmetal.net/infra9-metal-bridge/internal/metal/models" "go.infratographer.com/x/gidx" + + "go.equinixmetal.net/infra9-metal-bridge/internal/metal/models" ) func (c *Client) GetUserDetails(ctx context.Context, id gidx.PrefixedID) (*models.UserDetails, error) { return nil, nil } + +func (c *Client) GetUserOrganizationRole(ctx context.Context, userID, orgID gidx.PrefixedID) (string, error) { + return "collaborator", nil +} + +func (c *Client) GetUserProjectRole(ctx context.Context, userID, projID gidx.PrefixedID) (string, error) { + return "collaborator", nil +} diff --git a/internal/metal/providers/provider.go b/internal/metal/providers/provider.go index dba8598..0a99521 100644 --- a/internal/metal/providers/provider.go +++ b/internal/metal/providers/provider.go @@ -3,12 +3,15 @@ package provider import ( "context" - "go.equinixmetal.net/infra9-metal-bridge/internal/metal/models" "go.infratographer.com/x/gidx" + + "go.equinixmetal.net/infra9-metal-bridge/internal/metal/models" ) type Provider interface { GetOrganizationDetails(ctx context.Context, id gidx.PrefixedID) (*models.OrganizationDetails, error) GetProjectDetails(ctx context.Context, id gidx.PrefixedID) (*models.ProjectDetails, error) GetUserDetails(ctx context.Context, id gidx.PrefixedID) (*models.UserDetails, error) + GetUserOrganizationRole(ctx context.Context, userID, orgID gidx.PrefixedID) (string, error) + GetUserProjectRole(ctx context.Context, userID, projID gidx.PrefixedID) (string, error) } diff --git a/internal/permissions/roles.go b/internal/permissions/roles.go index 6bc8e5a..247c5b5 100644 --- a/internal/permissions/roles.go +++ b/internal/permissions/roles.go @@ -51,13 +51,15 @@ func (c *Client) CreateRole(ctx context.Context, resourceID gidx.PrefixedID, act func (c *Client) ListResourceRoles(ctx context.Context, resourceID gidx.PrefixedID) (ResourceRoles, error) { path := fmt.Sprintf("/api/v1/resources/%s/roles", resourceID.String()) - var response ResourceRoles + var response struct { + Data ResourceRoles `json:"data"` + } if _, err := c.DoRequest(ctx, http.MethodGet, path, nil, &response); err != nil { return nil, err } - return response, nil + return response.Data, nil } func (c *Client) FindResourceRoleByActions(ctx context.Context, resourceID gidx.PrefixedID, actions []string) (ResourceRole, error) { diff --git a/internal/pubsub/subscriber.go b/internal/pubsub/subscriber.go index 75949cb..5d74f37 100644 --- a/internal/pubsub/subscriber.go +++ b/internal/pubsub/subscriber.go @@ -173,7 +173,7 @@ func (s *Subscriber) handleTouchEvent(ctx context.Context, msg *message.Message, } if s.svc.IsUser(changeMsg.SubjectID) { - if err := s.svc.TouchUser(ctx, changeMsg.SubjectID); err != nil { + if err := s.svc.AssignUser(ctx, changeMsg.SubjectID, changeMsg.AdditionalSubjectIDs...); err != nil { // TODO: only return errors on retryable errors return err } @@ -206,7 +206,7 @@ func (s *Subscriber) handleDeleteEvent(ctx context.Context, msg *message.Message } if s.svc.IsUser(changeMsg.SubjectID) { - if err := s.svc.DeleteUser(ctx, changeMsg.SubjectID); err != nil { + if err := s.svc.RemoveUser(ctx, changeMsg.SubjectID, changeMsg.AdditionalSubjectIDs...); err != nil { // TODO: only return errors on retryable errors return err } diff --git a/internal/service/options.go b/internal/service/options.go index 63b093e..0e78a65 100644 --- a/internal/service/options.go +++ b/internal/service/options.go @@ -55,7 +55,7 @@ func WithRootTenant(sid string) Option { return err } - s.rootResource = rootResource{id} + s.rootResource = prefixedID{id} return nil } diff --git a/internal/service/process.go b/internal/service/process.go index f310f51..22492da 100644 --- a/internal/service/process.go +++ b/internal/service/process.go @@ -50,6 +50,7 @@ func (s *service) processMemberships(ctx context.Context, memberships []Resource roleActions[role] = s.roles[role] } + resourceRoleID[resourceID][role] = gidx.NullPrefixedID resourceRoleMembers[resourceID][role][memberID] = true } diff --git a/internal/service/projects.go b/internal/service/projects.go index 14b1f4e..aa81d8c 100644 --- a/internal/service/projects.go +++ b/internal/service/projects.go @@ -4,8 +4,41 @@ import ( "context" "go.infratographer.com/x/gidx" + + "go.equinixmetal.net/infra9-metal-bridge/internal/metal/models" ) +func (s *service) buildProjectRelationships(project *models.ProjectDetails) (Relationships, error) { + relations := Relationships{ + Relationships: []Relationship{ + // Relate project to organization. + { + Resource: project, + Relation: RelateParent, + RelatedResource: project.Organization, + }, + }, + } + + 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 +} + func (s *service) IsProjectID(id gidx.PrefixedID) bool { if idType, ok := s.idPrefixMap[id.Prefix()]; ok { return idType == TypeProject @@ -15,6 +48,27 @@ func (s *service) IsProjectID(id gidx.PrefixedID) bool { } func (s *service) TouchProject(ctx context.Context, id gidx.PrefixedID) error { + logger := s.logger.With("project.id", id.String()) + + project, err := s.metal.GetProjectDetails(ctx, id) + if err != nil { + logger.Errorw("failed to get project", "error", err) + + return err + } + + relationships, err := s.buildProjectRelationships(project) + if err != nil { + logger.Errorw("failed to build project relationships", "error", err) + + return err + } + + s.processRelationships(ctx, "metal-relation", relationships.Relationships) + s.processMemberships(ctx, relationships.Memberships) + + s.logger.Infow("project sync complete", "relationships", len(relationships.Relationships), "memberships", len(relationships.Memberships)) + return nil } diff --git a/internal/service/relationships.go b/internal/service/relationships.go index ebcc4ef..199dc96 100644 --- a/internal/service/relationships.go +++ b/internal/service/relationships.go @@ -2,8 +2,6 @@ package service import ( "go.infratographer.com/x/gidx" - - "go.equinixmetal.net/infra9-metal-bridge/internal/metal/models" ) const ( @@ -60,5 +58,5 @@ type Relationship struct { type ResourceMemberships struct { Resource IDPrefixableResource Role string - Member *models.UserDetails + Member IDPrefixableResource } diff --git a/internal/service/service.go b/internal/service/service.go index c344b1b..4b72445 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -47,10 +47,13 @@ type Service interface { // IsUser checks if the provided id has an id prefix which is a user. IsUser(id gidx.PrefixedID) bool - // TouchUser triggers a sync of a user and their permissions. - TouchUser(ctx context.Context, id gidx.PrefixedID) error - // DeleteUser deletes the user and their permissions. - DeleteUser(ctx context.Context, id gidx.PrefixedID) error + // AssignUser assigns a user to the given resource. + AssignUser(ctx context.Context, userID gidx.PrefixedID, resourceIDs ...gidx.PrefixedID) error + // RemoveUser removes the users from the given resource. + RemoveUser(ctx context.Context, userID gidx.PrefixedID, resourceIDs ...gidx.PrefixedID) error + + // IsAssignableResource checks if the provided resource ID may have assigned users. + IsAssignableResource(id gidx.PrefixedID) bool } var _ Service = &service{} @@ -62,15 +65,15 @@ type service struct { perms *permissions.Client idPrefixMap map[string]string - rootResource rootResource + rootResource prefixedID roles map[string][]string } -type rootResource struct { +type prefixedID struct { id gidx.PrefixedID } -func (r rootResource) PrefixedID() gidx.PrefixedID { +func (r prefixedID) PrefixedID() gidx.PrefixedID { return r.id } diff --git a/internal/service/users.go b/internal/service/users.go index 0aa8183..961aad4 100644 --- a/internal/service/users.go +++ b/internal/service/users.go @@ -14,10 +14,59 @@ func (s *service) IsUser(id gidx.PrefixedID) bool { return false } -func (s *service) TouchUser(ctx context.Context, id gidx.PrefixedID) error { +func (s *service) IsAssignableResource(id gidx.PrefixedID) bool { + if idType, ok := s.idPrefixMap[id.Prefix()]; ok { + switch idType { + case TypeOrganization, TypeProject: + return true + default: + return false + } + } + + return false +} + +func (s *service) AssignUser(ctx context.Context, userID gidx.PrefixedID, resourceIDs ...gidx.PrefixedID) error { + var memberships []ResourceMemberships + + for _, resourceID := range resourceIDs { + var ( + role string + err error + ) + + if idType, ok := s.idPrefixMap[resourceID.Prefix()]; ok { + switch idType { + case TypeOrganization: + role, err = s.metal.GetUserOrganizationRole(ctx, userID, resourceID) + case TypeProject: + role, err = s.metal.GetUserProjectRole(ctx, userID, resourceID) + } + } + + if err != nil { + return err + } + + if role == "" { + continue + } + + memberships = append(memberships, ResourceMemberships{ + Resource: prefixedID{resourceID}, + Role: role, + Member: prefixedID{userID}, + }) + } + + s.processMemberships(ctx, memberships) + + s.logger.Infow("assignment sync complete", "memberships", len(memberships)) + return nil } -func (s *service) DeleteUser(ctx context.Context, id gidx.PrefixedID) error { +func (s *service) RemoveUser(ctx context.Context, userID gidx.PrefixedID, resourceIDs ...gidx.PrefixedID) error { return nil } From 2147f0374b6ecfa63df04360cc7795cfcefd4cc0 Mon Sep 17 00:00:00 2001 From: Mike Mason Date: Tue, 11 Jul 2023 22:26:16 +0000 Subject: [PATCH 02/33] handle unassignment of role --- internal/permissions/assignments.go | 23 ++ internal/permissions/errors.go | 6 +- internal/permissions/roles.go | 20 ++ internal/pubsub/subscriber.go | 2 +- internal/service/organizations.go | 2 +- internal/service/process.go | 138 ------------ internal/service/process_memberships.go | 245 ++++++++++++++++++++++ internal/service/process_relationships.go | 31 +++ internal/service/projects.go | 2 +- internal/service/service.go | 4 +- internal/service/users.go | 92 ++++++-- 11 files changed, 403 insertions(+), 162 deletions(-) delete mode 100644 internal/service/process.go create mode 100644 internal/service/process_memberships.go create mode 100644 internal/service/process_relationships.go diff --git a/internal/permissions/assignments.go b/internal/permissions/assignments.go index 5c7f953..ca23d21 100644 --- a/internal/permissions/assignments.go +++ b/internal/permissions/assignments.go @@ -45,6 +45,29 @@ func (c *Client) AssignRole(ctx context.Context, roleID gidx.PrefixedID, memberI return nil } +func (c *Client) UnassignRole(ctx context.Context, roleID gidx.PrefixedID, memberID gidx.PrefixedID) error { + path := fmt.Sprintf("/api/v1/roles/%s/assignments", roleID.String()) + + body, err := encodeJSON(RoleAssign{ + SubjectID: memberID.String(), + }) + if err != nil { + return err + } + + var response RoleAssignResponse + + if _, err = c.DoRequest(ctx, http.MethodDelete, path, body, &response); err != nil { + return err + } + + if !response.Success { + return ErrUnassignmentFailed + } + + return nil +} + func (c *Client) ListRoleAssignments(ctx context.Context, roleID gidx.PrefixedID) ([]gidx.PrefixedID, error) { path := fmt.Sprintf("/api/v1/roles/%s/assignments", roleID.String()) diff --git a/internal/permissions/errors.go b/internal/permissions/errors.go index 1c1c72c..bb51d49 100644 --- a/internal/permissions/errors.go +++ b/internal/permissions/errors.go @@ -3,6 +3,8 @@ package permissions import "errors" var ( - ErrRoleNotFound = errors.New("role not found") - ErrAssignmentFailed = errors.New("assignment failed") + ErrRoleNotFound = errors.New("role not found") + ErrAssignmentFailed = errors.New("assignment failed") + ErrUnassignmentFailed = errors.New("unassignment failed") + ErrUnexpectedRoleDeleteFailed = errors.New("unknown role delete error") ) diff --git a/internal/permissions/roles.go b/internal/permissions/roles.go index 247c5b5..695eae9 100644 --- a/internal/permissions/roles.go +++ b/internal/permissions/roles.go @@ -17,6 +17,10 @@ type ResourceRoleCreateResponse struct { ID string `json:"id"` } +type ResourceRoleDeleteResponse struct { + Success bool `json:"success"` +} + type ResourceRoles []ResourceRole type ResourceRole struct { @@ -48,6 +52,22 @@ func (c *Client) CreateRole(ctx context.Context, resourceID gidx.PrefixedID, act return roleID, nil } +func (c *Client) DeleteRole(ctx context.Context, roleID gidx.PrefixedID) error { + path := fmt.Sprintf("/api/v1/roles/%s", roleID.String()) + + var response ResourceRoleDeleteResponse + + if _, err := c.DoRequest(ctx, http.MethodDelete, path, nil, &response); err != nil { + return err + } + + if !response.Success { + return ErrUnexpectedRoleDeleteFailed + } + + return nil +} + func (c *Client) ListResourceRoles(ctx context.Context, resourceID gidx.PrefixedID) (ResourceRoles, error) { path := fmt.Sprintf("/api/v1/resources/%s/roles", resourceID.String()) diff --git a/internal/pubsub/subscriber.go b/internal/pubsub/subscriber.go index 5d74f37..8ae8164 100644 --- a/internal/pubsub/subscriber.go +++ b/internal/pubsub/subscriber.go @@ -206,7 +206,7 @@ func (s *Subscriber) handleDeleteEvent(ctx context.Context, msg *message.Message } if s.svc.IsUser(changeMsg.SubjectID) { - if err := s.svc.RemoveUser(ctx, changeMsg.SubjectID, changeMsg.AdditionalSubjectIDs...); err != nil { + if err := s.svc.UnassignUser(ctx, changeMsg.SubjectID, changeMsg.AdditionalSubjectIDs...); err != nil { // TODO: only return errors on retryable errors return err } diff --git a/internal/service/organizations.go b/internal/service/organizations.go index 4b41e00..6a6d3f4 100644 --- a/internal/service/organizations.go +++ b/internal/service/organizations.go @@ -89,7 +89,7 @@ func (s *service) TouchOrganization(ctx context.Context, id gidx.PrefixedID) err } s.processRelationships(ctx, "metal-relation", relationships.Relationships) - s.processMemberships(ctx, relationships.Memberships) + s.syncMemberships(ctx, relationships.Memberships) s.logger.Infow("organization sync complete", "relationships", len(relationships.Relationships), "memberships", len(relationships.Memberships)) diff --git a/internal/service/process.go b/internal/service/process.go deleted file mode 100644 index 22492da..0000000 --- a/internal/service/process.go +++ /dev/null @@ -1,138 +0,0 @@ -package service - -import ( - "context" - - "go.infratographer.com/x/events" - "go.infratographer.com/x/gidx" -) - -func (s *service) processRelationships(ctx context.Context, subjectType string, relationships []Relationship) { - var err error - - for _, rel := range relationships { - err = s.publisher.PublishChange(ctx, subjectType, events.ChangeMessage{ - SubjectID: rel.Resource.PrefixedID(), - EventType: string(events.CreateChangeType), - AdditionalSubjectIDs: []gidx.PrefixedID{ - rel.RelatedResource.PrefixedID(), - }, - }) - - if err != nil { - s.logger.Errorw("error publishing change", - "subject_type", subjectType, - "resource.id", rel.Resource.PrefixedID(), - "related_resource.id", rel.RelatedResource.PrefixedID(), - "error", err, - ) - } - } -} - -func (s *service) processMemberships(ctx context.Context, memberships []ResourceMemberships) { - resourceRoleID := make(map[gidx.PrefixedID]map[string]gidx.PrefixedID) - resourceRoleMembers := make(map[gidx.PrefixedID]map[string]map[gidx.PrefixedID]bool) - roleActions := make(map[string][]string) - - for _, membership := range memberships { - resourceID := membership.Resource.PrefixedID() - role := membership.Role - memberID := membership.Member.PrefixedID() - - if _, ok := resourceRoleMembers[resourceID]; !ok { - resourceRoleID[resourceID] = make(map[string]gidx.PrefixedID) - resourceRoleMembers[resourceID] = make(map[string]map[gidx.PrefixedID]bool) - } - - if _, ok := resourceRoleMembers[resourceID][role]; !ok { - resourceRoleMembers[resourceID][role] = make(map[gidx.PrefixedID]bool) - roleActions[role] = s.roles[role] - } - - resourceRoleID[resourceID][role] = gidx.NullPrefixedID - resourceRoleMembers[resourceID][role][memberID] = true - } - - resourceRoleAssignments := make(map[gidx.PrefixedID]map[gidx.PrefixedID]map[gidx.PrefixedID]bool) - - for resourceID, roles := range resourceRoleID { - resourceRoleAssignments[resourceID] = make(map[gidx.PrefixedID]map[gidx.PrefixedID]bool) - - for role := range roles { - actions := roleActions[role] - - resourceRole, err := s.perms.FindResourceRoleByActions(ctx, resourceID, actions) - if err != nil { - s.logger.Warnw("failed to find role by actions for resource", "resource.id", resourceID, "role", role, "actions", actions, "error", err) - - continue - } - - resourceRoleID[resourceID][role] = resourceRole.ID - resourceRoleAssignments[resourceID][resourceRole.ID] = make(map[gidx.PrefixedID]bool) - - assignments, err := s.perms.ListRoleAssignments(ctx, resourceRole.ID) - if err != nil { - s.logger.Warnw("failed to get role assignments for resource", "resource.id", resourceID, "role", role, "error", err) - - continue - } - - for _, assignment := range assignments { - resourceRoleAssignments[resourceID][resourceRole.ID][assignment] = true - } - } - } - - for resourceID, roles := range resourceRoleMembers { - for role, members := range roles { - roleID := resourceRoleID[resourceID][role] - actions := roleActions[role] - - logger := s.logger.With("resource.id", resourceID, "role.name", role, "actions", actions) - - var createdRole bool - - if roleID == gidx.NullPrefixedID { - logger.Infow("creating role for resource") - - resourceRoleID, err := s.perms.CreateRole(ctx, resourceID, actions) - if err != nil { - logger.Errorw("failed to create role for resource", "error", err) - - continue - } - - createdRole = true - roleID = resourceRoleID - } - - logger = logger.With("role.id", roleID) - - assignments := make(map[gidx.PrefixedID]bool) - - if !createdRole { - assignments = resourceRoleAssignments[resourceID][roleID] - } - - for memberID := range members { - mlogger := logger.With("member.id", memberID) - - if _, ok := assignments[memberID]; ok { - mlogger.Infow("skipping already assigned member") - - continue - } - - if err := s.perms.AssignRole(ctx, roleID, memberID); err != nil { - mlogger.Errorw("failed to assign member to role", "error", err) - - continue - } - - mlogger.Infow("role assigned to member") - } - } - } -} diff --git a/internal/service/process_memberships.go b/internal/service/process_memberships.go new file mode 100644 index 0000000..2a3c002 --- /dev/null +++ b/internal/service/process_memberships.go @@ -0,0 +1,245 @@ +package service + +import ( + "context" + "strings" + + "go.infratographer.com/x/gidx" + "golang.org/x/exp/slices" + + "go.equinixmetal.net/infra9-metal-bridge/internal/permissions" +) + +func (s *service) syncMemberships(ctx context.Context, memberships []ResourceMemberships) error { + if len(memberships) == 0 { + return nil + } + + resourceMap := make(map[gidx.PrefixedID][]ResourceMemberships) + + 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) + + var ( + totalRoleCreate, totalRoleDelete int + totalRoleAssign, totalRoleUnassign int + ) + + for _, membership := range memberships { + resourceID := membership.Resource.PrefixedID() + + 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) + if err != nil { + return err + } + + roleCreations := make(map[string][]string) + roleDeletions := make([]gidx.PrefixedID, 0) + + for roleKey, actions := range wantRoles { + if _, ok := liveRoles[roleKey]; !ok { + roleCreations[roleKey] = actions + } + } + + for roleKey, role := range liveRoles { + 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) + + for roleKey, assignments := range wantAssignments { + for memberID := range assignments { + if _, ok := liveAssignments[roleKey]; ok { + if _, ok := liveAssignments[roleKey][memberID]; ok { + continue + } + } + + roleAssignments[roleKey] = append(roleAssignments[roleKey], memberID) + totalRoleAssign++ + } + } + + for roleKey, assignments := range liveAssignments { + for memberID := range assignments { + if _, ok := wantAssignments[roleKey]; ok { + if _, ok := wantAssignments[roleKey][memberID]; ok { + continue + } + } + + roleAssignmentRemovals[roleKey] = append(roleAssignmentRemovals[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), + "role.create", totalRoleCreate, + "role.delete", totalRoleDelete, + "role.assign", totalRoleAssign, + "role.unassign", totalRoleUnassign, + ) + + var ( + rolesCreated, rolesDeleted int + 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) + if err != nil { + rlogger.Errorw("error creating role", "actions", actions, "error", err) + + continue + } + + resourceRoleIDs[resourceID][roleKey] = roleID + rolesCreated++ + } + + for _, roleID := range resourceRoleDeletions[resourceID] { + if err := s.perms.DeleteRole(ctx, roleID); err != nil { + rlogger.Errorw("error deleting role", "role.id", roleID, "error", err) + + continue + } + + rolesDeleted++ + } + + for roleKey, members := range resourceRoleAssignments[resourceID] { + roleID, ok := resourceRoleIDs[resourceID][roleKey] + if !ok { + rlogger.Errorw("role id not found for role actions key", "role_actions_key", roleKey, "members", len(members)) + + continue + } + + for _, memberID := range members { + if err := s.perms.AssignRole(ctx, roleID, memberID); err != nil { + rlogger.Errorw("error assigning member to role", "role.id", roleID, "member.id", memberID, "error", err) + + continue + } + + roleAssignments++ + } + } + + for roleKey, members := range resourceRoleAssignmentRemovals[resourceID] { + roleID, ok := resourceRoleIDs[resourceID][roleKey] + if !ok { + rlogger.Errorw("role id not found for role actions key", "role_actions_key", roleKey, "members", len(members)) + + continue + } + + for _, memberID := range members { + if err := s.perms.UnassignRole(ctx, roleID, memberID); err != nil { + rlogger.Errorw("error removing member from role", "role.id", roleID, "member.id", memberID, "error", err) + + continue + } + + roleUnassignments++ + } + } + } + + s.logger.Debugw("memberships processed", + "resources", len(resourceMap), + "role.create", rolesCreated, + "role.delete", rolesDeleted, + "role.assign", roleAssignments, + "role.unassign", roleUnassignments, + ) + + return nil +} + +func (s *service) mapResourceWants(memberships []ResourceMemberships) (map[string][]string, map[string]map[gidx.PrefixedID]bool) { + roleActionsKey := make(map[string]string) + + for role, actions := range s.roles { + slices.Sort(actions) + + roleActionsKey[role] = strings.Join(actions, "|") + } + + wantRoles := make(map[string][]string) + wantAssignments := make(map[string]map[gidx.PrefixedID]bool) + + for _, membership := range memberships { + roleKey := roleActionsKey[membership.Role] + + if _, ok := wantRoles[roleKey]; !ok { + wantRoles[roleKey] = s.roles[membership.Role] + wantAssignments[roleKey] = make(map[gidx.PrefixedID]bool) + } + + wantAssignments[roleKey][membership.Member.PrefixedID()] = true + } + + return wantRoles, wantAssignments +} + +func (s *service) mapResourceDetails(ctx context.Context, resourceID gidx.PrefixedID) (map[string]permissions.ResourceRole, map[string]map[gidx.PrefixedID]bool, error) { + roles := make(map[string]permissions.ResourceRole) + assignments := make(map[string]map[gidx.PrefixedID]bool) + + liveRoles, err := s.perms.ListResourceRoles(ctx, resourceID) + if err != nil { + return nil, nil, err + } + + for _, role := range liveRoles { + slices.Sort(role.Actions) + + roleKey := strings.Join(role.Actions, "|") + roles[roleKey] = role + + liveAssignments, err := s.perms.ListRoleAssignments(ctx, role.ID) + if err != nil { + return nil, nil, err + } + + assignments[roleKey] = make(map[gidx.PrefixedID]bool) + + for _, assignment := range liveAssignments { + assignments[roleKey][assignment] = true + } + } + + return roles, assignments, nil +} diff --git a/internal/service/process_relationships.go b/internal/service/process_relationships.go new file mode 100644 index 0000000..3da1655 --- /dev/null +++ b/internal/service/process_relationships.go @@ -0,0 +1,31 @@ +package service + +import ( + "context" + + "go.infratographer.com/x/events" + "go.infratographer.com/x/gidx" +) + +func (s *service) processRelationships(ctx context.Context, subjectType string, relationships []Relationship) { + var err error + + for _, rel := range relationships { + err = s.publisher.PublishChange(ctx, subjectType, events.ChangeMessage{ + SubjectID: rel.Resource.PrefixedID(), + EventType: string(events.CreateChangeType), + AdditionalSubjectIDs: []gidx.PrefixedID{ + rel.RelatedResource.PrefixedID(), + }, + }) + + if err != nil { + s.logger.Errorw("error publishing change", + "subject_type", subjectType, + "resource.id", rel.Resource.PrefixedID(), + "related_resource.id", rel.RelatedResource.PrefixedID(), + "error", err, + ) + } + } +} diff --git a/internal/service/projects.go b/internal/service/projects.go index aa81d8c..3091e18 100644 --- a/internal/service/projects.go +++ b/internal/service/projects.go @@ -65,7 +65,7 @@ func (s *service) TouchProject(ctx context.Context, id gidx.PrefixedID) error { } s.processRelationships(ctx, "metal-relation", relationships.Relationships) - s.processMemberships(ctx, relationships.Memberships) + s.syncMemberships(ctx, relationships.Memberships) s.logger.Infow("project sync complete", "relationships", len(relationships.Relationships), "memberships", len(relationships.Memberships)) diff --git a/internal/service/service.go b/internal/service/service.go index 4b72445..b5fccef 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -49,8 +49,8 @@ type Service interface { IsUser(id gidx.PrefixedID) bool // AssignUser assigns a user to the given resource. AssignUser(ctx context.Context, userID gidx.PrefixedID, resourceIDs ...gidx.PrefixedID) error - // RemoveUser removes the users from the given resource. - RemoveUser(ctx context.Context, userID gidx.PrefixedID, resourceIDs ...gidx.PrefixedID) error + // UnassignUser removes the users from the given resource. + UnassignUser(ctx context.Context, userID gidx.PrefixedID, resourceIDs ...gidx.PrefixedID) error // IsAssignableResource checks if the provided resource ID may have assigned users. IsAssignableResource(id gidx.PrefixedID) bool diff --git a/internal/service/users.go b/internal/service/users.go index 961aad4..d712c5e 100644 --- a/internal/service/users.go +++ b/internal/service/users.go @@ -31,22 +31,11 @@ func (s *service) AssignUser(ctx context.Context, userID gidx.PrefixedID, resour var memberships []ResourceMemberships for _, resourceID := range resourceIDs { - var ( - role string - err error - ) - - if idType, ok := s.idPrefixMap[resourceID.Prefix()]; ok { - switch idType { - case TypeOrganization: - role, err = s.metal.GetUserOrganizationRole(ctx, userID, resourceID) - case TypeProject: - role, err = s.metal.GetUserProjectRole(ctx, userID, resourceID) - } - } - + role, err := s.getUserResourceRole(ctx, userID, resourceID) if err != nil { - return err + s.logger.Warnw("failed to determine role for user resource", "error", err) + + continue } if role == "" { @@ -60,13 +49,82 @@ func (s *service) AssignUser(ctx context.Context, userID gidx.PrefixedID, resour }) } - s.processMemberships(ctx, memberships) + s.syncMemberships(ctx, memberships) s.logger.Infow("assignment sync complete", "memberships", len(memberships)) return nil } -func (s *service) RemoveUser(ctx context.Context, userID gidx.PrefixedID, resourceIDs ...gidx.PrefixedID) error { +func (s *service) UnassignUser(ctx context.Context, userID gidx.PrefixedID, resourceIDs ...gidx.PrefixedID) error { + for _, resourceID := range resourceIDs { + rlogger := s.logger.With("user.id", userID, "resource.id", resourceID) + + role, err := s.getUserResourceRole(ctx, userID, resourceID) + if err != nil { + rlogger.Warnw("failed to determine role for user resource", "error", err) + + continue + } + + if role == "" { + continue + } + + actions := s.roles[role] + + rlogger = rlogger.With("role.name", role, "role.actions", actions) + + resourceRole, err := s.perms.FindResourceRoleByActions(ctx, resourceID, actions) + if err != nil { + rlogger.Warnw("failed to find role by actions for resource", "error", err) + + continue + } + + rlogger = rlogger.With("role.id", resourceRole.ID) + + assigned, err := s.perms.RoleHasAssignment(ctx, resourceRole.ID, userID) + if err != nil { + rlogger.Warnw("failed to check role assignment", "error", err) + + continue + } + + if !assigned { + rlogger.Warnw("unable to unassign member which is not assigned") + + continue + } + + if err = s.perms.UnassignRole(ctx, resourceRole.ID, userID); err != nil { + rlogger.Errorw("failed to unassign member from role", "error", err) + + continue + } + } + return nil } + +func (s *service) getUserResourceRole(ctx context.Context, userID, resourceID gidx.PrefixedID) (string, error) { + var ( + role string + err error + ) + + if idType, ok := s.idPrefixMap[resourceID.Prefix()]; ok { + switch idType { + case TypeOrganization: + role, err = s.metal.GetUserOrganizationRole(ctx, userID, resourceID) + case TypeProject: + role, err = s.metal.GetUserProjectRole(ctx, userID, resourceID) + } + } + + if err != nil { + return "", err + } + + return role, nil +} From 10f45c56a052cc09d1a54d8f19be1857290a0960 Mon Sep 17 00:00:00 2001 From: Mike Mason Date: Fri, 14 Jul 2023 15:04:02 +0000 Subject: [PATCH 03/33] add delete event publishes for organization and project, and correct event names --- internal/service/organizations.go | 17 ++++++++++++++++- internal/service/projects.go | 17 ++++++++++++++++- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/internal/service/organizations.go b/internal/service/organizations.go index 6a6d3f4..42fa141 100644 --- a/internal/service/organizations.go +++ b/internal/service/organizations.go @@ -3,11 +3,14 @@ package service import ( "context" + "go.infratographer.com/x/events" "go.infratographer.com/x/gidx" "go.equinixmetal.net/infra9-metal-bridge/internal/metal/models" ) +const organizationEvent = "metalorganization" + func (s *service) buildOrganizationRelationships(org *models.OrganizationDetails) (Relationships, error) { relations := Relationships{ Relationships: []Relationship{ @@ -88,7 +91,7 @@ func (s *service) TouchOrganization(ctx context.Context, id gidx.PrefixedID) err return err } - s.processRelationships(ctx, "metal-relation", relationships.Relationships) + s.processRelationships(ctx, organizationEvent, relationships.Relationships) s.syncMemberships(ctx, relationships.Memberships) s.logger.Infow("organization sync complete", "relationships", len(relationships.Relationships), "memberships", len(relationships.Memberships)) @@ -97,5 +100,17 @@ func (s *service) TouchOrganization(ctx context.Context, id gidx.PrefixedID) err } func (s *service) DeleteOrganization(ctx context.Context, id gidx.PrefixedID) error { + err := s.publisher.PublishChange(ctx, organizationEvent, events.ChangeMessage{ + SubjectID: id, + EventType: string(events.DeleteChangeType), + }) + if err != nil { + s.logger.Errorw("error publishing organization delete", + "subject_type", organizationEvent, + "resource.id", id, + "error", err, + ) + } + return nil } diff --git a/internal/service/projects.go b/internal/service/projects.go index 3091e18..496add4 100644 --- a/internal/service/projects.go +++ b/internal/service/projects.go @@ -3,11 +3,14 @@ package service import ( "context" + "go.infratographer.com/x/events" "go.infratographer.com/x/gidx" "go.equinixmetal.net/infra9-metal-bridge/internal/metal/models" ) +const projectEvent = "metalproject" + func (s *service) buildProjectRelationships(project *models.ProjectDetails) (Relationships, error) { relations := Relationships{ Relationships: []Relationship{ @@ -64,7 +67,7 @@ func (s *service) TouchProject(ctx context.Context, id gidx.PrefixedID) error { return err } - s.processRelationships(ctx, "metal-relation", relationships.Relationships) + s.processRelationships(ctx, projectEvent, relationships.Relationships) s.syncMemberships(ctx, relationships.Memberships) s.logger.Infow("project sync complete", "relationships", len(relationships.Relationships), "memberships", len(relationships.Memberships)) @@ -73,5 +76,17 @@ func (s *service) TouchProject(ctx context.Context, id gidx.PrefixedID) error { } func (s *service) DeleteProject(ctx context.Context, id gidx.PrefixedID) error { + err := s.publisher.PublishChange(ctx, projectEvent, events.ChangeMessage{ + SubjectID: id, + EventType: string(events.DeleteChangeType), + }) + if err != nil { + s.logger.Errorw("error publishing project delete", + "subject_type", projectEvent, + "resource.id", id, + "error", err, + ) + } + return nil } From 2f9f0675f92cf72958737462b4b23191edfdf7a5 Mon Sep 17 00:00:00 2001 From: Mike Mason Date: Mon, 17 Jul 2023 15:36:52 +0000 Subject: [PATCH 04/33] restructure and process additions and deleteions of relationships, roles and memberships --- internal/permissions/relationships.go | 71 ++++++++ internal/service/options.go | 2 +- internal/service/organizations.go | 51 ++---- internal/service/process_memberships.go | 190 ++++++++++---------- internal/service/process_relationships.go | 200 ++++++++++++++++++++-- internal/service/projects.go | 28 +-- internal/service/relationships.go | 48 ++---- internal/service/service.go | 37 +++- internal/service/users.go | 32 ++-- 9 files changed, 441 insertions(+), 218 deletions(-) create mode 100644 internal/permissions/relationships.go diff --git a/internal/permissions/relationships.go b/internal/permissions/relationships.go new file mode 100644 index 0000000..5381c9b --- /dev/null +++ b/internal/permissions/relationships.go @@ -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 +} diff --git a/internal/service/options.go b/internal/service/options.go index 0e78a65..f1f7e6f 100644 --- a/internal/service/options.go +++ b/internal/service/options.go @@ -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 diff --git a/internal/service/organizations.go b/internal/service/organizations.go index 42fa141..01ad064 100644 --- a/internal/service/organizations.go +++ b/internal/service/organizations.go @@ -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, - Relation: RelateParent, - RelatedResource: s.rootResource, - }, + Resource: org, + Parent: Relation{ + Relation: RelateParent, + Resource: s.rootResource, }, + SubjectType: TypeProject, } for _, member := range org.Memberships { @@ -32,35 +30,17 @@ func (s *service) buildOrganizationRelationships(org *models.OrganizationDetails } relations.Memberships = append(relations.Memberships, ResourceMemberships{ - Resource: org, - Role: role, - Member: member.User, + Role: role, + Member: member.User, }) } } for _, project := range org.Projects { - relations.Relationships = append(relations.Relationships, Relationship{ - Resource: project, - Relation: RelateParent, - RelatedResource: org, + relations.SubjectRelationships = append(relations.SubjectRelationships, Relation{ + Resource: project, + Relation: RelateParent, }) - - 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 } diff --git a/internal/service/process_memberships.go b/internal/service/process_memberships.go index 2a3c002..543480b 100644 --- a/internal/service/process_memberships.go +++ b/internal/service/process_memberships.go @@ -10,75 +10,67 @@ 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) + liveRoles, liveAssignments, err := s.mapResourceDetails(ctx, relationships.Resource.PrefixedID()) + if err != nil { + rlogger.Errorw("failed to get membership resource details map", + "error", err, + ) + + return 0, 0 } - for resourceID, memberships := range resourceMap { - resourceRoleIDs[resourceID] = make(map[string]gidx.PrefixedID) + roleCreations := make(map[string][]string) + roleDeletions := make([]gidx.PrefixedID, 0) - wantRoles, wantAssignments := s.mapResourceWants(memberships) - - liveRoles, liveAssignments, err := s.mapResourceDetails(ctx, resourceID) - if err != nil { - return err + for roleKey, actions := range wantRoles { + if _, ok := liveRoles[roleKey]; !ok { + roleCreations[roleKey] = actions } + } - roleCreations := make(map[string][]string) - roleDeletions := make([]gidx.PrefixedID, 0) - - for roleKey, actions := range wantRoles { - if _, ok := liveRoles[roleKey]; !ok { - roleCreations[roleKey] = actions - } - } - - for roleKey, role := range liveRoles { + 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 + } - for roleKey, assignments := range wantAssignments { - for memberID := range assignments { - if _, ok := liveAssignments[roleKey]; ok { - if _, ok := liveAssignments[roleKey][memberID]; ok { - continue - } + roleMembershipsAdd := make(map[string][]gidx.PrefixedID) + roleMembershipsRemove := make(map[string][]gidx.PrefixedID) + + for roleKey, assignments := range wantAssignments { + for memberID := range assignments { + if _, ok := liveAssignments[roleKey]; ok { + if _, ok := liveAssignments[roleKey][memberID]; ok { + continue } - - roleAssignments[roleKey] = append(roleAssignments[roleKey], memberID) - totalRoleAssign++ } - } + 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), + totalRoleCreate += len(roleCreations) + totalRoleDelete += len(roleDeletions) + + rlogger.Debugw("processing memberships", "role.create", totalRoleCreate, "role.delete", totalRoleDelete, "role.assign", totalRoleAssign, @@ -114,78 +100,74 @@ 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) - if err != nil { - rlogger.Errorw("error creating role", "actions", actions, "error", err) + 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 - rolesCreated++ + continue } - for _, roleID := range resourceRoleDeletions[resourceID] { - if err := s.perms.DeleteRole(ctx, roleID); err != nil { - rlogger.Errorw("error deleting role", "role.id", roleID, "error", err) + roleIDs[roleKey] = roleID + rolesCreated++ + } - continue - } + for _, roleID := range roleDeletions { + if err := s.perms.DeleteRole(ctx, roleID); err != nil { + rlogger.Errorw("error deleting role", "role.id", roleID, "error", err) - rolesDeleted++ + continue } - for roleKey, members := range resourceRoleAssignments[resourceID] { - roleID, ok := resourceRoleIDs[resourceID][roleKey] - if !ok { - rlogger.Errorw("role id not found for role actions key", "role_actions_key", roleKey, "members", len(members)) + rolesDeleted++ + } - continue - } + 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)) - for _, memberID := range members { - if err := s.perms.AssignRole(ctx, roleID, memberID); err != nil { - rlogger.Errorw("error assigning member to role", "role.id", roleID, "member.id", memberID, "error", err) - - continue - } - - roleAssignments++ - } + continue } - for roleKey, members := range resourceRoleAssignmentRemovals[resourceID] { - roleID, ok := resourceRoleIDs[resourceID][roleKey] - if !ok { - rlogger.Errorw("role id not found for role actions key", "role_actions_key", roleKey, "members", len(members)) + for _, memberID := range members { + if err := s.perms.AssignRole(ctx, roleID, memberID); err != nil { + rlogger.Errorw("error assigning member to role", "role.id", roleID, "member.id", memberID, "error", err) continue } - for _, memberID := range members { - if err := s.perms.UnassignRole(ctx, roleID, memberID); err != nil { - rlogger.Errorw("error removing member from role", "role.id", roleID, "member.id", memberID, "error", err) - - continue - } - - roleUnassignments++ - } + roleAssignments++ } } - s.logger.Debugw("memberships processed", - "resources", len(resourceMap), + 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)) + + continue + } + + for _, memberID := range members { + if err := s.perms.UnassignRole(ctx, roleID, memberID); err != nil { + rlogger.Errorw("error removing member from role", "role.id", roleID, "member.id", memberID, "error", err) + + continue + } + + roleUnassignments++ + } + } + + 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) { diff --git a/internal/service/process_relationships.go b/internal/service/process_relationships.go index 3da1655..a46dfef 100644 --- a/internal/service/process_relationships.go +++ b/internal/service/process_relationships.go @@ -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 } diff --git a/internal/service/projects.go b/internal/service/projects.go index 496add4..7ad68f5 100644 --- a/internal/service/projects.go +++ b/internal/service/projects.go @@ -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, - Relation: RelateParent, - RelatedResource: project.Organization, - }, + Resource: project, + // Relate project to organization. + Parent: Relation{ + Resource: project.Organization, + Relation: RelateParent, }, } @@ -32,9 +30,8 @@ func (s *service) buildProjectRelationships(project *models.ProjectDetails) (Rel } relations.Memberships = append(relations.Memberships, ResourceMemberships{ - Resource: project, - Role: role, - Member: member.User, + 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 } diff --git a/internal/service/relationships.go b/internal/service/relationships.go index 199dc96..252daf8 100644 --- a/internal/service/relationships.go +++ b/internal/service/relationships.go @@ -16,47 +16,19 @@ type IDPrefixableResource interface { } type Relationships struct { - Relationships []Relationship - Memberships []ResourceMemberships + 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 - Relation RelationshipType - RelatedResource IDPrefixableResource +type Relation struct { + Relation RelationshipType + Resource IDPrefixableResource } type ResourceMemberships struct { - Resource IDPrefixableResource - Role string - Member IDPrefixableResource + Role string + Member IDPrefixableResource } diff --git a/internal/service/service.go b/internal/service/service.go index b5fccef..aa4b2ed 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -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 { diff --git a/internal/service/users.go b/internal/service/users.go index d712c5e..71834c0 100644 --- a/internal/service/users.go +++ b/internal/service/users.go @@ -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}, - Role: role, - Member: prefixedID{userID}, - }) + Memberships: []ResourceMemberships{ + { + Role: role, + 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 } From 70f17de14b1f1f7f059f7c7293495316121313dc Mon Sep 17 00:00:00 2001 From: Mike Mason Date: Mon, 17 Jul 2023 15:43:22 +0000 Subject: [PATCH 05/33] correct providers package name --- internal/metal/metal.go | 4 ++-- internal/metal/options.go | 7 ++++--- internal/metal/providers/emapi/client.go | 4 ++-- internal/metal/providers/emgql/client.go | 13 ++++++------- internal/metal/providers/provider.go | 2 +- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/internal/metal/metal.go b/internal/metal/metal.go index 1e567fc..b48c1c9 100644 --- a/internal/metal/metal.go +++ b/internal/metal/metal.go @@ -7,14 +7,14 @@ import ( "go.uber.org/zap" "go.equinixmetal.net/infra9-metal-bridge/internal/metal/models" - provider "go.equinixmetal.net/infra9-metal-bridge/internal/metal/providers" + "go.equinixmetal.net/infra9-metal-bridge/internal/metal/providers" ) // Client is the Equinix Metal API Client struct. type Client struct { logger *zap.Logger - provider provider.Provider + provider providers.Provider } func (c *Client) GetOrganizationDetails(ctx context.Context, id gidx.PrefixedID) (*models.OrganizationDetails, error) { diff --git a/internal/metal/options.go b/internal/metal/options.go index 9360840..4d17efd 100644 --- a/internal/metal/options.go +++ b/internal/metal/options.go @@ -1,17 +1,18 @@ package metal import ( - provider "go.equinixmetal.net/infra9-metal-bridge/internal/metal/providers" + "go.uber.org/zap" + + "go.equinixmetal.net/infra9-metal-bridge/internal/metal/providers" "go.equinixmetal.net/infra9-metal-bridge/internal/metal/providers/emapi" "go.equinixmetal.net/infra9-metal-bridge/internal/metal/providers/emgql" - "go.uber.org/zap" ) // Option is a Client configuration Option definition. type Option func(c *Client) error // WithProvider sets the provider on the client. -func WithProvider(provider provider.Provider) Option { +func WithProvider(provider providers.Provider) Option { return func(c *Client) error { c.provider = provider diff --git a/internal/metal/providers/emapi/client.go b/internal/metal/providers/emapi/client.go index b9372c7..98279b3 100644 --- a/internal/metal/providers/emapi/client.go +++ b/internal/metal/providers/emapi/client.go @@ -12,7 +12,7 @@ import ( "go.uber.org/zap" - provider "go.equinixmetal.net/infra9-metal-bridge/internal/metal/providers" + "go.equinixmetal.net/infra9-metal-bridge/internal/metal/providers" ) const ( @@ -29,7 +29,7 @@ var DefaultHTTPClient = &http.Client{ Timeout: defaultHTTPTimeout, } -var _ provider.Provider = &Client{} +var _ providers.Provider = &Client{} type Client struct { logger *zap.SugaredLogger diff --git a/internal/metal/providers/emgql/client.go b/internal/metal/providers/emgql/client.go index 965fb9a..2e992b3 100644 --- a/internal/metal/providers/emgql/client.go +++ b/internal/metal/providers/emgql/client.go @@ -5,21 +5,20 @@ import ( "net/url" "time" - provider "go.equinixmetal.net/infra9-metal-bridge/internal/metal/providers" "go.uber.org/zap" + + "go.equinixmetal.net/infra9-metal-bridge/internal/metal/providers" ) const ( defaultHTTPTimeout = 5 * time.Second ) -var ( - DefaultHTTPClient = &http.Client{ - Timeout: defaultHTTPTimeout, - } -) +var DefaultHTTPClient = &http.Client{ + Timeout: defaultHTTPTimeout, +} -var _ provider.Provider = &Client{} +var _ providers.Provider = &Client{} type Client struct { logger *zap.SugaredLogger diff --git a/internal/metal/providers/provider.go b/internal/metal/providers/provider.go index 0a99521..ddbc947 100644 --- a/internal/metal/providers/provider.go +++ b/internal/metal/providers/provider.go @@ -1,4 +1,4 @@ -package provider +package providers import ( "context" From cb0651b7bf7570d2502bc8fc9d54190e0d0821e2 Mon Sep 17 00:00:00 2001 From: Mike Mason Date: Mon, 17 Jul 2023 16:32:21 +0000 Subject: [PATCH 06/33] relation deletion must be done through the api as events delete all relationships --- internal/permissions/errors.go | 9 ++++--- internal/permissions/relationships.go | 33 +++++++++++++++++++++++ internal/service/process_relationships.go | 33 +++++++++++------------ 3 files changed, 54 insertions(+), 21 deletions(-) diff --git a/internal/permissions/errors.go b/internal/permissions/errors.go index bb51d49..877b74d 100644 --- a/internal/permissions/errors.go +++ b/internal/permissions/errors.go @@ -3,8 +3,9 @@ package permissions import "errors" var ( - ErrRoleNotFound = errors.New("role not found") - ErrAssignmentFailed = errors.New("assignment failed") - ErrUnassignmentFailed = errors.New("unassignment failed") - ErrUnexpectedRoleDeleteFailed = errors.New("unknown role delete error") + ErrRoleNotFound = errors.New("role not found") + ErrAssignmentFailed = errors.New("assignment failed") + ErrUnassignmentFailed = errors.New("unassignment failed") + ErrUnexpectedRoleDeleteFailed = errors.New("unknown role delete error") + ErrUnexpectedRelationshipDeleteFailed = errors.New("unknown relationship delete error") ) diff --git a/internal/permissions/relationships.go b/internal/permissions/relationships.go index 5381c9b..7da8e25 100644 --- a/internal/permissions/relationships.go +++ b/internal/permissions/relationships.go @@ -21,6 +21,39 @@ type ResourceRelationship struct { SubjectID gidx.PrefixedID } +type ResourceRelationshipRequest struct { + Relation string `json:"relation"` + SubjectID string `json:"subject_id"` +} + +type ResourceRelationshipDeleteResponse struct { + Success bool `json:"success"` +} + +func (c *Client) DeleteResourceRelationship(ctx context.Context, resourceID gidx.PrefixedID, relation string, relatedResourceID gidx.PrefixedID) error { + path := fmt.Sprintf("/api/v1/resources/%s/relationships", resourceID.String()) + + body, err := encodeJSON(ResourceRelationshipRequest{ + Relation: relation, + SubjectID: relatedResourceID.String(), + }) + if err != nil { + return err + } + + var response ResourceRelationshipDeleteResponse + + if _, err := c.DoRequest(ctx, http.MethodDelete, path, body, &response); err != nil { + return err + } + + if !response.Success { + return ErrUnexpectedRelationshipDeleteFailed + } + + return nil +} + func (c *Client) ListResourceRelationships(ctx context.Context, resourceID gidx.PrefixedID, relatedResourceType string) ([]ResourceRelationship, error) { query := url.Values{ "resourceType": []string{relatedResourceType}, diff --git a/internal/service/process_relationships.go b/internal/service/process_relationships.go index a46dfef..8b8e1d4 100644 --- a/internal/service/process_relationships.go +++ b/internal/service/process_relationships.go @@ -102,16 +102,6 @@ func (s *service) processRelationships(ctx context.Context, eventType string, re }) } - 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(), @@ -122,14 +112,23 @@ func (s *service) processRelationships(ctx context.Context, eventType string, re }) } + for _, relatedResourceID := range deleteParentRelationships { + err = s.perms.DeleteResourceRelationship(ctx, relationships.Resource.PrefixedID(), string(RelateParent), relatedResourceID) + if err != nil { + rlogger.Errorw("error deleting parent relationship", + "parent.resource.id", relatedResourceID.String(), + ) + } + } + for _, relation := range deleteSubjectRelationships { - processEvents = append(processEvents, events.ChangeMessage{ - SubjectID: relation.Resource.PrefixedID(), - EventType: string(events.DeleteChangeType), - AdditionalSubjectIDs: []gidx.PrefixedID{ - relationships.Resource.PrefixedID(), - }, - }) + err = s.perms.DeleteResourceRelationship(ctx, relation.Resource.PrefixedID(), string(relation.Relation), relationships.Resource.PrefixedID()) + if err != nil { + rlogger.Errorw("error deleting relationship", + "relation", relation.Relation, + "subject.id", relation.Resource.PrefixedID().String(), + ) + } } for _, event := range processEvents { From 02ac2870bc52e08ceff7029a8f0e788eb12dd20d Mon Sep 17 00:00:00 2001 From: Mike Mason Date: Mon, 17 Jul 2023 17:17:04 +0000 Subject: [PATCH 07/33] convert metlusr to idntusr --- internal/metal/models/idprefix.go | 3 +++ internal/metal/models/users.go | 45 +++++++++++++++++++++++++++---- internal/pubsub/subscriber.go | 24 +++++++++++++++-- 3 files changed, 65 insertions(+), 7 deletions(-) diff --git a/internal/metal/models/idprefix.go b/internal/metal/models/idprefix.go index 8dd3a59..39f99a7 100644 --- a/internal/metal/models/idprefix.go +++ b/internal/metal/models/idprefix.go @@ -9,4 +9,7 @@ const ( // IDPrefixUser defines the ID Prefix for a User. IDPrefixUser = "metlusr" + + // IdentityPrefixUser defines the ID Prefix for a User created with Identity API. + IdentityPrefixUser = "idntusr" ) diff --git a/internal/metal/models/users.go b/internal/metal/models/users.go index 5852cf2..8042c74 100644 --- a/internal/metal/models/users.go +++ b/internal/metal/models/users.go @@ -1,12 +1,19 @@ package models -import "go.infratographer.com/x/gidx" +import ( + "crypto/sha256" + "encoding/base64" + + "go.infratographer.com/x/gidx" +) const ( - MetalUserPrefix = "metlusr" + MetalUserIssuer = "https://auth.equinix.com/" + MetalUserIssuerIDPrefix = "auth|" ) type UserDetails struct { + id *gidx.PrefixedID ID string `json:"id"` FullName string `json:"full_name"` Organizations []*OrganizationDetails `json:"organizations"` @@ -15,9 +22,37 @@ type UserDetails struct { } func (d *UserDetails) PrefixedID() gidx.PrefixedID { - if d.ID == "" { - return gidx.NullPrefixedID + if d.id != nil { + return *d.id } - return gidx.PrefixedID(IDPrefixUser + "-" + d.ID) + nullID := gidx.NullPrefixedID + + d.id = &nullID + + if d.ID == "" { + return nullID + } + + id, err := GenerateSubjectID(IdentityPrefixUser, MetalUserIssuer, MetalUserIssuerIDPrefix+d.ID) + if err != nil { + return nullID + } + + d.id = &id + + return *d.id +} + +func GenerateSubjectID(prefix, iss, sub string) (gidx.PrefixedID, error) { + // Concatenate the iss and sub values, then hash them + issSub := iss + sub + issSubHash := sha256.Sum256([]byte(issSub)) + + digest := base64.RawURLEncoding.EncodeToString(issSubHash[:]) + + // Concatenate the prefix with the digest + out := prefix + "-" + digest + + return gidx.Parse(out) } diff --git a/internal/pubsub/subscriber.go b/internal/pubsub/subscriber.go index 8ae8164..bb2ec6f 100644 --- a/internal/pubsub/subscriber.go +++ b/internal/pubsub/subscriber.go @@ -6,11 +6,13 @@ import ( nc "github.com/nats-io/nats.go" "go.infratographer.com/x/events" + "go.infratographer.com/x/gidx" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" "go.uber.org/zap" + "go.equinixmetal.net/infra9-metal-bridge/internal/metal/models" "go.equinixmetal.net/infra9-metal-bridge/internal/service" "github.com/ThreeDotsLabs/watermill/message" @@ -173,7 +175,16 @@ func (s *Subscriber) handleTouchEvent(ctx context.Context, msg *message.Message, } if s.svc.IsUser(changeMsg.SubjectID) { - if err := s.svc.AssignUser(ctx, changeMsg.SubjectID, changeMsg.AdditionalSubjectIDs...); err != nil { + userUUID := changeMsg.SubjectID.String()[gidx.PrefixPartLength+1:] + + subjID, err := models.GenerateSubjectID(models.IdentityPrefixUser, models.MetalUserIssuer, models.MetalUserIssuerIDPrefix+userUUID) + if err != nil { + s.logger.Errorw("failed to convert user id to identity id", "user.id", changeMsg.SubjectID.String(), "error", err) + + return nil + } + + if err := s.svc.AssignUser(ctx, subjID, changeMsg.AdditionalSubjectIDs...); err != nil { // TODO: only return errors on retryable errors return err } @@ -206,7 +217,16 @@ func (s *Subscriber) handleDeleteEvent(ctx context.Context, msg *message.Message } if s.svc.IsUser(changeMsg.SubjectID) { - if err := s.svc.UnassignUser(ctx, changeMsg.SubjectID, changeMsg.AdditionalSubjectIDs...); err != nil { + userUUID := changeMsg.SubjectID.String()[gidx.PrefixPartLength+1:] + + subjID, err := models.GenerateSubjectID(models.IdentityPrefixUser, models.MetalUserIssuer, models.MetalUserIssuerIDPrefix+userUUID) + if err != nil { + s.logger.Errorw("failed to convert user id to identity id", "user.id", changeMsg.SubjectID.String(), "error", err) + + return nil + } + + if err := s.svc.UnassignUser(ctx, subjID, changeMsg.AdditionalSubjectIDs...); err != nil { // TODO: only return errors on retryable errors return err } From 2681b3d06492fd8a77d3f513ee1a773cf603c19c Mon Sep 17 00:00:00 2001 From: Mike Mason Date: Mon, 17 Jul 2023 17:19:27 +0000 Subject: [PATCH 08/33] remove debug log messages --- internal/pubsub/subscriber.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/internal/pubsub/subscriber.go b/internal/pubsub/subscriber.go index bb2ec6f..d4642fa 100644 --- a/internal/pubsub/subscriber.go +++ b/internal/pubsub/subscriber.go @@ -110,10 +110,8 @@ func (s Subscriber) listen(messages <-chan *message.Message, wg *sync.WaitGroup) if err := s.processEvent(msg); err != nil { s.logger.Warn("Failed to process msg: ", err) - s.logger.Infow("message nacked", "event.id", msg.UUID) msg.Nack() } else { - s.logger.Infow("message acked", "event.id", msg.UUID) msg.Ack() } } From bc87fa7726335218b08111c203fd32964c7935b6 Mon Sep 17 00:00:00 2001 From: Mike Mason Date: Mon, 17 Jul 2023 19:02:55 +0000 Subject: [PATCH 09/33] add variable and method comments --- internal/metal/metal.go | 5 +++++ internal/metal/models/doc.go | 2 ++ internal/metal/models/memberships.go | 1 + internal/metal/models/projects.go | 2 ++ internal/metal/models/users.go | 8 +++++++- internal/metal/providers/emapi/client.go | 5 +++++ internal/metal/providers/emapi/config.go | 1 + internal/metal/providers/emapi/doc.go | 2 ++ internal/metal/providers/emapi/errors.go | 1 + internal/metal/providers/emapi/memberships.go | 7 +++++++ internal/metal/providers/emapi/organizations.go | 6 ++++++ internal/metal/providers/emapi/projects.go | 6 ++++++ internal/metal/providers/emapi/users.go | 8 ++++++++ internal/metal/providers/emgql/client.go | 2 ++ internal/metal/providers/emgql/config.go | 2 +- internal/metal/providers/emgql/doc.go | 2 ++ internal/metal/providers/emgql/organizations.go | 4 +++- internal/metal/providers/emgql/projects.go | 4 +++- internal/metal/providers/emgql/users.go | 3 +++ internal/metal/providers/provider.go | 2 ++ internal/permissions/assignments.go | 7 +++++++ internal/permissions/client.go | 4 ++++ internal/permissions/config.go | 1 + internal/permissions/doc.go | 2 ++ internal/permissions/errors.go | 17 +++++++++++++---- internal/permissions/options.go | 2 ++ internal/permissions/relationships.go | 7 +++++++ internal/permissions/roles.go | 9 +++++++++ internal/service/organizations.go | 4 ++++ internal/service/process_memberships.go | 10 ++++++++++ internal/service/process_relationships.go | 14 +++++++------- internal/service/projects.go | 4 ++++ internal/service/relationships.go | 10 +++++++++- internal/service/service.go | 4 ++++ internal/service/users.go | 5 +++++ 35 files changed, 157 insertions(+), 16 deletions(-) create mode 100644 internal/metal/models/doc.go create mode 100644 internal/metal/providers/emapi/doc.go create mode 100644 internal/metal/providers/emgql/doc.go create mode 100644 internal/permissions/doc.go diff --git a/internal/metal/metal.go b/internal/metal/metal.go index b48c1c9..ceab2c9 100644 --- a/internal/metal/metal.go +++ b/internal/metal/metal.go @@ -17,22 +17,27 @@ type Client struct { provider providers.Provider } +// GetOrganizationDetails fetches the organization id provided with its memberships. func (c *Client) GetOrganizationDetails(ctx context.Context, id gidx.PrefixedID) (*models.OrganizationDetails, error) { return c.provider.GetOrganizationDetails(ctx, id) } +// GetProjectDetails fetchs the provided project id with membership information. func (c *Client) GetProjectDetails(ctx context.Context, id gidx.PrefixedID) (*models.ProjectDetails, error) { return c.provider.GetProjectDetails(ctx, id) } +// GetUserDetails fetches the provided user id. func (c *Client) GetUserDetails(ctx context.Context, id gidx.PrefixedID) (*models.UserDetails, error) { return c.provider.GetUserDetails(ctx, id) } +// GetUserOrganizationRole returns the role for the user in the organization. func (c *Client) GetUserOrganizationRole(ctx context.Context, userID, orgID gidx.PrefixedID) (string, error) { return c.provider.GetUserOrganizationRole(ctx, userID, orgID) } +// GetUserProjectRole returns the role for the user in the project. func (c *Client) GetUserProjectRole(ctx context.Context, userID, projID gidx.PrefixedID) (string, error) { return c.provider.GetUserProjectRole(ctx, userID, projID) } diff --git a/internal/metal/models/doc.go b/internal/metal/models/doc.go new file mode 100644 index 0000000..f03f4f7 --- /dev/null +++ b/internal/metal/models/doc.go @@ -0,0 +1,2 @@ +// Package models defines generic models each provider must be able to return. +package models diff --git a/internal/metal/models/memberships.go b/internal/metal/models/memberships.go index be61413..ad5446b 100644 --- a/internal/metal/models/memberships.go +++ b/internal/metal/models/memberships.go @@ -1,5 +1,6 @@ package models +// Membership contains metal membership details. type Membership[T any] struct { ID string `json:"id"` User *UserDetails `json:"user"` diff --git a/internal/metal/models/projects.go b/internal/metal/models/projects.go index f2a0869..656137f 100644 --- a/internal/metal/models/projects.go +++ b/internal/metal/models/projects.go @@ -2,6 +2,7 @@ package models import "go.infratographer.com/x/gidx" +// ProjectDetails contains project and membership information. type ProjectDetails struct { ID string `json:"id"` Name string `json:"name"` @@ -9,6 +10,7 @@ type ProjectDetails struct { Organization *OrganizationDetails `json:"organization"` } +// PrefixedID returns the prefixed id for the project. func (d *ProjectDetails) PrefixedID() gidx.PrefixedID { if d.ID == "" { return gidx.NullPrefixedID diff --git a/internal/metal/models/users.go b/internal/metal/models/users.go index 8042c74..ecb1c4d 100644 --- a/internal/metal/models/users.go +++ b/internal/metal/models/users.go @@ -8,10 +8,14 @@ import ( ) const ( - MetalUserIssuer = "https://auth.equinix.com/" + // MetalUserIssuer is the issuer that is used for metal api token users. + MetalUserIssuer = "https://auth.equinix.com/" + + // MetaluserIssuerIDPrefix is the issuer id prefix added by the issuer. MetalUserIssuerIDPrefix = "auth|" ) +// UserDetails contains the user information. type UserDetails struct { id *gidx.PrefixedID ID string `json:"id"` @@ -21,6 +25,7 @@ type UserDetails struct { Roles []string `json:"roles"` } +// PrefixedID returns the identity prefixed id for the user. func (d *UserDetails) PrefixedID() gidx.PrefixedID { if d.id != nil { return *d.id @@ -44,6 +49,7 @@ func (d *UserDetails) PrefixedID() gidx.PrefixedID { return *d.id } +// GenerateSubjectID builds a identity prefixed id with the provided prefix for the issuer and subject. func GenerateSubjectID(prefix, iss, sub string) (gidx.PrefixedID, error) { // Concatenate the iss and sub values, then hash them issSub := iss + sub diff --git a/internal/metal/providers/emapi/client.go b/internal/metal/providers/emapi/client.go index 98279b3..c8a14e1 100644 --- a/internal/metal/providers/emapi/client.go +++ b/internal/metal/providers/emapi/client.go @@ -25,12 +25,14 @@ const ( staffHeaderValue = "true" ) +// DefaultHTTPClient is the default http client used if no client is provided. var DefaultHTTPClient = &http.Client{ Timeout: defaultHTTPTimeout, } var _ providers.Provider = &Client{} +// Client is the client to interact with the equinix metal api. type Client struct { logger *zap.SugaredLogger httpClient *http.Client @@ -39,6 +41,8 @@ type Client struct { consumerToken string } +// Do executes the provided request. +// If the out value is provided, the response will attempt to be json decoded. func (c *Client) Do(req *http.Request, out any) (*http.Response, error) { if c.authToken != "" { req.Header.Set(authHeader, c.authToken) @@ -68,6 +72,7 @@ func (c *Client) Do(req *http.Request, out any) (*http.Response, error) { return resp, nil } +// DoRequest creates a new request from the provided parameters and executes the request. func (c *Client) DoRequest(ctx context.Context, method, path string, body io.Reader, out any) (*http.Response, error) { path = strings.TrimPrefix(path, c.baseURL.Path) diff --git a/internal/metal/providers/emapi/config.go b/internal/metal/providers/emapi/config.go index bbb939a..6627a41 100644 --- a/internal/metal/providers/emapi/config.go +++ b/internal/metal/providers/emapi/config.go @@ -17,6 +17,7 @@ type Config struct { ConsumerToken string } +// Populated checks if any field has been populated. func (c Config) Populated() bool { return c.AuthToken != "" || c.ConsumerToken != "" || c.BaseURL != "" } diff --git a/internal/metal/providers/emapi/doc.go b/internal/metal/providers/emapi/doc.go new file mode 100644 index 0000000..6b38602 --- /dev/null +++ b/internal/metal/providers/emapi/doc.go @@ -0,0 +1,2 @@ +// Package emapi implement a metal provider which fetches details from the Equinix Metal API. +package emapi diff --git a/internal/metal/providers/emapi/errors.go b/internal/metal/providers/emapi/errors.go index 5eb2931..ab6ded5 100644 --- a/internal/metal/providers/emapi/errors.go +++ b/internal/metal/providers/emapi/errors.go @@ -2,4 +2,5 @@ package emapi import "errors" +// ErrBaseURLRequired is returned if no base url is provided. var ErrBaseURLRequired = errors.New("emapi base url required") diff --git a/internal/metal/providers/emapi/memberships.go b/internal/metal/providers/emapi/memberships.go index 4a36662..857497b 100644 --- a/internal/metal/providers/emapi/memberships.go +++ b/internal/metal/providers/emapi/memberships.go @@ -4,10 +4,13 @@ import ( "go.equinixmetal.net/infra9-metal-bridge/internal/metal/models" ) +// Roles contains a list of roles. type Roles []string +// Memberships contains a list of memberships type Memberships []*Membership +// ToDetailsWithOrganizationDetails convers the memberships to generic membership models with organization details. func (m Memberships) ToDetailsWithOrganizationDetails(orgDetails *models.OrganizationDetails) []*models.Membership[models.OrganizationDetails] { memberships := make([]*models.Membership[models.OrganizationDetails], len(m)) @@ -28,6 +31,7 @@ func (m Memberships) ToDetailsWithOrganizationDetails(orgDetails *models.Organiz return memberships } +// ToDetailsWithProjectDetails convers the memberships to generic membership models with project details. func (m Memberships) ToDetailsWithProjectDetails(projDetails *models.ProjectDetails) []*models.Membership[models.ProjectDetails] { memberships := make([]*models.Membership[models.ProjectDetails], len(m)) @@ -48,6 +52,7 @@ func (m Memberships) ToDetailsWithProjectDetails(projDetails *models.ProjectDeta return memberships } +// Membership contains membership information. type Membership struct { client *Client @@ -57,6 +62,7 @@ type Membership struct { User *User `json:"user"` } +// ToDetailsWithOrganizationDetails convers the membership to generic membership model with organization details. func (m *Membership) ToDetailsWithOrganizationDetails(orgDetails *models.OrganizationDetails) *models.Membership[models.OrganizationDetails] { if m.ID == "" { return nil @@ -70,6 +76,7 @@ func (m *Membership) ToDetailsWithOrganizationDetails(orgDetails *models.Organiz } } +// ToDetailsWithOrganizationDetails convers the membership to generic membership model with organization details. func (m *Membership) ToDetailsWithProjectDetails(projDetails *models.ProjectDetails) *models.Membership[models.ProjectDetails] { if m.ID == "" { return nil diff --git a/internal/metal/providers/emapi/organizations.go b/internal/metal/providers/emapi/organizations.go index 639e44f..5f2577c 100644 --- a/internal/metal/providers/emapi/organizations.go +++ b/internal/metal/providers/emapi/organizations.go @@ -14,8 +14,10 @@ const ( organizationsPath = "/organizations" ) +// Organizations contains a list of organizations. type Organizations []*Organization +// ToDetails converts to a generic model organization details. func (o Organizations) ToDetails() []*models.OrganizationDetails { orgs := make([]*models.OrganizationDetails, len(o)) @@ -36,6 +38,7 @@ func (o Organizations) ToDetails() []*models.OrganizationDetails { return orgs } +// Organization contains organization information. type Organization struct { client *Client @@ -47,6 +50,7 @@ type Organization struct { Projects Projects `json:"projects"` } +// ToDetails converts the object to a generic orgnization details. func (o *Organization) ToDetails() *models.OrganizationDetails { var id string @@ -69,6 +73,7 @@ func (o *Organization) ToDetails() *models.OrganizationDetails { return details } +// getOrganizationWithMemberships fetches an organization from the equinix metal api with membership user information. func (c *Client) getOrganizationWithMemberships(ctx context.Context, id string) (*Organization, error) { var org Organization @@ -80,6 +85,7 @@ func (c *Client) getOrganizationWithMemberships(ctx context.Context, id string) return &org, nil } +// GetOrganizationDetails fetches the organization id provided with its memberships. func (c *Client) GetOrganizationDetails(ctx context.Context, id gidx.PrefixedID) (*models.OrganizationDetails, error) { org, err := c.getOrganizationWithMemberships(ctx, id.String()[gidx.PrefixPartLength+1:]) if err != nil { diff --git a/internal/metal/providers/emapi/projects.go b/internal/metal/providers/emapi/projects.go index 71639a8..42ad6a4 100644 --- a/internal/metal/providers/emapi/projects.go +++ b/internal/metal/providers/emapi/projects.go @@ -14,8 +14,10 @@ const ( projectsPath = "/projects" ) +// Projects contains a list of projects. type Projects []*Project +// ToDetails converts the objects to generic project details. func (p Projects) ToDetails() []*models.ProjectDetails { projects := make([]*models.ProjectDetails, len(p)) @@ -36,6 +38,7 @@ func (p Projects) ToDetails() []*models.ProjectDetails { return projects } +// Project contains project information. type Project struct { client *Client @@ -47,6 +50,7 @@ type Project struct { Organization *Organization `json:"organization"` } +// ToDetails converts the project to generic project details. func (p *Project) ToDetails() *models.ProjectDetails { var id string @@ -69,6 +73,7 @@ func (p *Project) ToDetails() *models.ProjectDetails { return details } +// getProjectWithMemberships fetches the provided project with membership information. func (c *Client) getProjectWithMemberships(ctx context.Context, id string) (*Project, error) { var project Project @@ -80,6 +85,7 @@ func (c *Client) getProjectWithMemberships(ctx context.Context, id string) (*Pro return &project, nil } +// GetProjectDetails fetchs the provided project id with membership information. func (c *Client) GetProjectDetails(ctx context.Context, id gidx.PrefixedID) (*models.ProjectDetails, error) { project, err := c.getProjectWithMemberships(ctx, id.String()[gidx.PrefixPartLength+1:]) if err != nil { diff --git a/internal/metal/providers/emapi/users.go b/internal/metal/providers/emapi/users.go index 966cbef..850db7b 100644 --- a/internal/metal/providers/emapi/users.go +++ b/internal/metal/providers/emapi/users.go @@ -14,8 +14,10 @@ const ( usersPath = "/users" ) +// Users contains a list of users. type Users []*User +// ToDetails converts the objects to generic user details. func (u Users) ToDetails() []*models.UserDetails { users := make([]*models.UserDetails, len(u)) @@ -36,6 +38,7 @@ func (u Users) ToDetails() []*models.UserDetails { return users } +// User contains user information. type User struct { client *Client @@ -46,6 +49,7 @@ type User struct { Projects Projects `json:"projects"` } +// ToDetails converts the user to generic user details. func (u *User) ToDetails() *models.UserDetails { var id string @@ -65,6 +69,7 @@ func (u *User) ToDetails() *models.UserDetails { } } +// getUser fetches the provided user. func (c *Client) getUser(ctx context.Context, id string) (*User, error) { var user User @@ -76,6 +81,7 @@ func (c *Client) getUser(ctx context.Context, id string) (*User, error) { return &user, nil } +// GetUserDetails fetches the provided user id. func (c *Client) GetUserDetails(ctx context.Context, id gidx.PrefixedID) (*models.UserDetails, error) { user, err := c.getUser(ctx, id.String()[gidx.PrefixPartLength+1:]) if err != nil { @@ -85,10 +91,12 @@ func (c *Client) GetUserDetails(ctx context.Context, id gidx.PrefixedID) (*model return user.ToDetails(), nil } +// GetUserOrganizationRole returns collaborator for all organizations. func (c *Client) GetUserOrganizationRole(ctx context.Context, userID, orgID gidx.PrefixedID) (string, error) { return "collaborator", nil } +// GetUserProjectRole returns collaborator for all projects. func (c *Client) GetUserProjectRole(ctx context.Context, userID, projectID gidx.PrefixedID) (string, error) { return "collaborator", nil } diff --git a/internal/metal/providers/emgql/client.go b/internal/metal/providers/emgql/client.go index 2e992b3..5b5fec3 100644 --- a/internal/metal/providers/emgql/client.go +++ b/internal/metal/providers/emgql/client.go @@ -14,12 +14,14 @@ const ( defaultHTTPTimeout = 5 * time.Second ) +// DefaultHTTPClient is the default http client used if no client is provided. var DefaultHTTPClient = &http.Client{ Timeout: defaultHTTPTimeout, } var _ providers.Provider = &Client{} +// Client is the client to interact with the equinix metal graphql service. type Client struct { logger *zap.SugaredLogger httpClient *http.Client diff --git a/internal/metal/providers/emgql/config.go b/internal/metal/providers/emgql/config.go index 86f8ba9..7a23789 100644 --- a/internal/metal/providers/emgql/config.go +++ b/internal/metal/providers/emgql/config.go @@ -2,11 +2,11 @@ package emgql // Config provides configuration for connecting to the Equinix Metal API provider. type Config struct { - // BaseURL is the baseurl to use when connecting to the Equinix Metal API Provider. BaseURL string } +// Populated checks if any field has been populated. func (c Config) Populated() bool { return c.BaseURL != "" } diff --git a/internal/metal/providers/emgql/doc.go b/internal/metal/providers/emgql/doc.go new file mode 100644 index 0000000..9dba58f --- /dev/null +++ b/internal/metal/providers/emgql/doc.go @@ -0,0 +1,2 @@ +// Package emgql implements a metal provider which fetches details from the Equinix Metal GraphQL. +package emgql diff --git a/internal/metal/providers/emgql/organizations.go b/internal/metal/providers/emgql/organizations.go index b3b354a..8338f21 100644 --- a/internal/metal/providers/emgql/organizations.go +++ b/internal/metal/providers/emgql/organizations.go @@ -3,10 +3,12 @@ package emgql import ( "context" - "go.equinixmetal.net/infra9-metal-bridge/internal/metal/models" "go.infratographer.com/x/gidx" + + "go.equinixmetal.net/infra9-metal-bridge/internal/metal/models" ) +// GetOrganizationDetails fetches the organization id provided with its memberships. func (c *Client) GetOrganizationDetails(ctx context.Context, id gidx.PrefixedID) (*models.OrganizationDetails, error) { return nil, nil } diff --git a/internal/metal/providers/emgql/projects.go b/internal/metal/providers/emgql/projects.go index f0caf29..9b449d2 100644 --- a/internal/metal/providers/emgql/projects.go +++ b/internal/metal/providers/emgql/projects.go @@ -3,10 +3,12 @@ package emgql import ( "context" - "go.equinixmetal.net/infra9-metal-bridge/internal/metal/models" "go.infratographer.com/x/gidx" + + "go.equinixmetal.net/infra9-metal-bridge/internal/metal/models" ) +// GetProjectDetails fetchs the provided project id with membership information. func (c *Client) GetProjectDetails(ctx context.Context, id gidx.PrefixedID) (*models.ProjectDetails, error) { return nil, nil } diff --git a/internal/metal/providers/emgql/users.go b/internal/metal/providers/emgql/users.go index 52e36c1..2f4ec4b 100644 --- a/internal/metal/providers/emgql/users.go +++ b/internal/metal/providers/emgql/users.go @@ -8,14 +8,17 @@ import ( "go.equinixmetal.net/infra9-metal-bridge/internal/metal/models" ) +// GetUserDetails fetches the provided user id. func (c *Client) GetUserDetails(ctx context.Context, id gidx.PrefixedID) (*models.UserDetails, error) { return nil, nil } +// GetUserOrganizationRole returns collaborator for all organizations. func (c *Client) GetUserOrganizationRole(ctx context.Context, userID, orgID gidx.PrefixedID) (string, error) { return "collaborator", nil } +// GetUserProjectRole returns collaborator for all projects. func (c *Client) GetUserProjectRole(ctx context.Context, userID, projID gidx.PrefixedID) (string, error) { return "collaborator", nil } diff --git a/internal/metal/providers/provider.go b/internal/metal/providers/provider.go index ddbc947..5b7b2d6 100644 --- a/internal/metal/providers/provider.go +++ b/internal/metal/providers/provider.go @@ -1,3 +1,4 @@ +// Package providers defines the provider interface for fetching metal resources. package providers import ( @@ -8,6 +9,7 @@ import ( "go.equinixmetal.net/infra9-metal-bridge/internal/metal/models" ) +// Provider defines the provider implementation. type Provider interface { GetOrganizationDetails(ctx context.Context, id gidx.PrefixedID) (*models.OrganizationDetails, error) GetProjectDetails(ctx context.Context, id gidx.PrefixedID) (*models.ProjectDetails, error) diff --git a/internal/permissions/assignments.go b/internal/permissions/assignments.go index ca23d21..f2dadd4 100644 --- a/internal/permissions/assignments.go +++ b/internal/permissions/assignments.go @@ -8,20 +8,24 @@ import ( "go.infratographer.com/x/gidx" ) +// RoleAssign is the role assignment request body. type RoleAssign struct { SubjectID string `json:"subject_id"` } +// RoleAssignResponse is the response from a role assignment. type RoleAssignResponse struct { Success bool `json:"success"` } +// roleAssignmentData is the response from listing a role assignment type roleAssignmentData struct { Data []struct { SubjectID string `json:"subject_id"` } `json:"data"` } +// AssignRole assigns the provided member ID to the given role ID. func (c *Client) AssignRole(ctx context.Context, roleID gidx.PrefixedID, memberID gidx.PrefixedID) error { path := fmt.Sprintf("/api/v1/roles/%s/assignments", roleID.String()) @@ -45,6 +49,7 @@ func (c *Client) AssignRole(ctx context.Context, roleID gidx.PrefixedID, memberI return nil } +// UnassignRole removes the provided member ID from the given role ID. func (c *Client) UnassignRole(ctx context.Context, roleID gidx.PrefixedID, memberID gidx.PrefixedID) error { path := fmt.Sprintf("/api/v1/roles/%s/assignments", roleID.String()) @@ -68,6 +73,7 @@ func (c *Client) UnassignRole(ctx context.Context, roleID gidx.PrefixedID, membe return nil } +// ListRoleAssignments lists all assignments for the given role. func (c *Client) ListRoleAssignments(ctx context.Context, roleID gidx.PrefixedID) ([]gidx.PrefixedID, error) { path := fmt.Sprintf("/api/v1/roles/%s/assignments", roleID.String()) @@ -91,6 +97,7 @@ func (c *Client) ListRoleAssignments(ctx context.Context, roleID gidx.PrefixedID return assignments, nil } +// RoleHasAssignment gets the assignments for the given role and check for the provided member id. func (c *Client) RoleHasAssignment(ctx context.Context, roleID gidx.PrefixedID, memberID gidx.PrefixedID) (bool, error) { assignments, err := c.ListRoleAssignments(ctx, roleID) if err != nil { diff --git a/internal/permissions/client.go b/internal/permissions/client.go index 38d3106..1ae0f20 100644 --- a/internal/permissions/client.go +++ b/internal/permissions/client.go @@ -21,6 +21,7 @@ var defaultHTTPClient = &http.Client{ Timeout: 5 * time.Second, } +// Client is the permissions client. type Client struct { logger *zap.SugaredLogger @@ -32,6 +33,8 @@ type Client struct { allowURL *url.URL } +// Do executes the provided request. +// If the out value is provided, the response will attempt to be json decoded. func (c *Client) Do(req *http.Request, out any) (*http.Response, error) { if c.token != "" { req.Header.Set(echo.HeaderAuthorization, "Bearer "+c.token) @@ -55,6 +58,7 @@ func (c *Client) Do(req *http.Request, out any) (*http.Response, error) { return resp, nil } +// DoRequest creates a new request from the provided parameters and executes the request. func (c *Client) DoRequest(ctx context.Context, method, path string, body io.Reader, out any) (*http.Response, error) { path = strings.TrimPrefix(path, c.baseURL.Path) diff --git a/internal/permissions/config.go b/internal/permissions/config.go index ccc21d7..f41d9cc 100644 --- a/internal/permissions/config.go +++ b/internal/permissions/config.go @@ -18,6 +18,7 @@ type Config struct { BearerToken string } +// MustViperFlags registers command flags along with the viper bindings. func MustViperFlags(v *viper.Viper, flags *pflag.FlagSet) { flags.String("permissions-baseurl", "", "permissions base url") viperx.MustBindFlag(v, "permissions.baseurl", flags.Lookup("permissions-baseurl")) diff --git a/internal/permissions/doc.go b/internal/permissions/doc.go new file mode 100644 index 0000000..5a481e1 --- /dev/null +++ b/internal/permissions/doc.go @@ -0,0 +1,2 @@ +// Package permissions implements a Permissions API client for fetching and manipulating relationships and role assignments. +package permissions diff --git a/internal/permissions/errors.go b/internal/permissions/errors.go index 877b74d..0a179eb 100644 --- a/internal/permissions/errors.go +++ b/internal/permissions/errors.go @@ -3,9 +3,18 @@ package permissions import "errors" var ( - ErrRoleNotFound = errors.New("role not found") - ErrAssignmentFailed = errors.New("assignment failed") - ErrUnassignmentFailed = errors.New("unassignment failed") - ErrUnexpectedRoleDeleteFailed = errors.New("unknown role delete error") + // ErrRoleNotFound is returned when no role is found for a given list of actions. + ErrRoleNotFound = errors.New("role not found") + + // ErrAssignmentFailed is returned when a user assignment to a role fails. + ErrAssignmentFailed = errors.New("assignment failed") + + // ErrUnassignmentFailed is returned when a user assignment is removed from a role fails. + ErrUnassignmentFailed = errors.New("unassignment failed") + + // ErrUnexpectedRoleDeleteFailed is returned when an unknown error is returned when deleting a role. + ErrUnexpectedRoleDeleteFailed = errors.New("unknown role delete error") + + // ErrUnexpectedRelationshipDeleteFailed is returned when an unknown error is returned when deleting a relationship. ErrUnexpectedRelationshipDeleteFailed = errors.New("unknown relationship delete error") ) diff --git a/internal/permissions/options.go b/internal/permissions/options.go index 30942f5..61a3b2f 100644 --- a/internal/permissions/options.go +++ b/internal/permissions/options.go @@ -4,8 +4,10 @@ import ( "go.uber.org/zap" ) +// Option is a client configuration option definition. type Option func(*Client) error +// WithLogger sets the logger for the client. func WithLogger(logger *zap.SugaredLogger) Option { return func(c *Client) error { c.logger = logger diff --git a/internal/permissions/relationships.go b/internal/permissions/relationships.go index 7da8e25..50cf314 100644 --- a/internal/permissions/relationships.go +++ b/internal/permissions/relationships.go @@ -15,21 +15,25 @@ type resourceRelationship struct { SubjectID string `json:"subject_id"` } +// ResourceRelationship defines the resource to subject relationship. type ResourceRelationship struct { ResourceID gidx.PrefixedID Relation string SubjectID gidx.PrefixedID } +// ResourceRelationshipRequest defines the request to relate to a subject. type ResourceRelationshipRequest struct { Relation string `json:"relation"` SubjectID string `json:"subject_id"` } +// ResourceRelationshipDeleteResponse defines the response for a delete of a relationship. type ResourceRelationshipDeleteResponse struct { Success bool `json:"success"` } +// DeleteResourceRelationship deletes the provided resources relationship to the given subject id. func (c *Client) DeleteResourceRelationship(ctx context.Context, resourceID gidx.PrefixedID, relation string, relatedResourceID gidx.PrefixedID) error { path := fmt.Sprintf("/api/v1/resources/%s/relationships", resourceID.String()) @@ -54,6 +58,9 @@ func (c *Client) DeleteResourceRelationship(ctx context.Context, resourceID gidx return nil } +// ListResourceRelationships returns resources related to the given id. +// If relatedResourceType is not provied, relations to subjects are returned. +// If relatedResourceType is provided, relations to the given resource are returned which match the given type. func (c *Client) ListResourceRelationships(ctx context.Context, resourceID gidx.PrefixedID, relatedResourceType string) ([]ResourceRelationship, error) { query := url.Values{ "resourceType": []string{relatedResourceType}, diff --git a/internal/permissions/roles.go b/internal/permissions/roles.go index 695eae9..0a1e4fa 100644 --- a/internal/permissions/roles.go +++ b/internal/permissions/roles.go @@ -9,25 +9,31 @@ import ( "golang.org/x/exp/slices" ) +// ResourceRoleCreate is the role create request. type ResourceRoleCreate struct { Actions []string `json:"actions"` } +// ResourceRoleCreateResponse is the role creation response. type ResourceRoleCreateResponse struct { ID string `json:"id"` } +// ResourceRoleDeleteResponse is the role deletion response. type ResourceRoleDeleteResponse struct { Success bool `json:"success"` } +// ResourceRoles is a listg of resource roles. type ResourceRoles []ResourceRole +// ResourceRole contains the role id and its actions. type ResourceRole struct { ID gidx.PrefixedID `json:"id"` Actions []string `json:"actions"` } +// CreateRole creates a role on the given resource id with the provided actions. func (c *Client) CreateRole(ctx context.Context, resourceID gidx.PrefixedID, actions []string) (gidx.PrefixedID, error) { path := fmt.Sprintf("/api/v1/resources/%s/roles", resourceID.String()) @@ -52,6 +58,7 @@ func (c *Client) CreateRole(ctx context.Context, resourceID gidx.PrefixedID, act return roleID, nil } +// DeleteRole deletes the provided role. func (c *Client) DeleteRole(ctx context.Context, roleID gidx.PrefixedID) error { path := fmt.Sprintf("/api/v1/roles/%s", roleID.String()) @@ -68,6 +75,7 @@ func (c *Client) DeleteRole(ctx context.Context, roleID gidx.PrefixedID) error { return nil } +// ListResourceRoles fetches all roles assigned to the provided resource. func (c *Client) ListResourceRoles(ctx context.Context, resourceID gidx.PrefixedID) (ResourceRoles, error) { path := fmt.Sprintf("/api/v1/resources/%s/roles", resourceID.String()) @@ -82,6 +90,7 @@ func (c *Client) ListResourceRoles(ctx context.Context, resourceID gidx.Prefixed return response.Data, nil } +// FindResourceRoleByActions fetches roles assigned to the provided resource and finds the first role where the actions match the provided actions. func (c *Client) FindResourceRoleByActions(ctx context.Context, resourceID gidx.PrefixedID, actions []string) (ResourceRole, error) { roles, err := c.ListResourceRoles(ctx, resourceID) if err != nil { diff --git a/internal/service/organizations.go b/internal/service/organizations.go index 01ad064..1f5ba80 100644 --- a/internal/service/organizations.go +++ b/internal/service/organizations.go @@ -11,6 +11,7 @@ import ( const organizationEvent = "metalorganization" +// buildOrganizationRelationships compiles all relations into a relationships object to be processed by the processors. func (s *service) buildOrganizationRelationships(org *models.OrganizationDetails) (Relationships, error) { relations := Relationships{ Resource: org, @@ -46,6 +47,7 @@ func (s *service) buildOrganizationRelationships(org *models.OrganizationDetails return relations, nil } +// IsOrganizationID checks if the provided id has the metal organization prefix. func (s *service) IsOrganizationID(id gidx.PrefixedID) bool { if idType, ok := s.idPrefixMap[id.Prefix()]; ok { return idType == TypeOrganization @@ -54,6 +56,7 @@ func (s *service) IsOrganizationID(id gidx.PrefixedID) bool { return false } +// TouchOrganization initializes a sync for the provided organization id for relationships and memberships. func (s *service) TouchOrganization(ctx context.Context, id gidx.PrefixedID) error { logger := s.logger.With("organization.id", id.String()) @@ -84,6 +87,7 @@ func (s *service) TouchOrganization(ctx context.Context, id gidx.PrefixedID) err return nil } +// DeleteOrganization deletes the provided organization id. func (s *service) DeleteOrganization(ctx context.Context, id gidx.PrefixedID) error { err := s.publisher.PublishChange(ctx, organizationEvent, events.ChangeMessage{ SubjectID: id, diff --git a/internal/service/process_memberships.go b/internal/service/process_memberships.go index 543480b..8be2902 100644 --- a/internal/service/process_memberships.go +++ b/internal/service/process_memberships.go @@ -10,6 +10,8 @@ import ( "go.equinixmetal.net/infra9-metal-bridge/internal/permissions" ) +// syncMemberships determines the changes between what is wanted and what is live and executes on the differences. +// If skipDeletions is true, no deletes will be executed. func (s *service) syncMemberships(ctx context.Context, relationships Relationships, skipDeletions bool) (int, int) { if len(relationships.Memberships) == 0 { return 0, 0 @@ -170,6 +172,10 @@ func (s *service) syncMemberships(ctx context.Context, relationships Relationshi return rolesCreated + rolesDeleted, roleAssignments + roleUnassignments } +// mapResourceWants processes the provided memberships and returns two maps. +// A Role Key is computed based on a sorted slice of actions for each role. +// The first map is of Role Key -> list of actions +// The second map is of Role Key -> Member ID -> true func (s *service) mapResourceWants(memberships []ResourceMemberships) (map[string][]string, map[string]map[gidx.PrefixedID]bool) { roleActionsKey := make(map[string]string) @@ -196,6 +202,10 @@ func (s *service) mapResourceWants(memberships []ResourceMemberships) (map[strin return wantRoles, wantAssignments } +// mapResourceDetails fetches the provided ResourceID's live state and returns two maps and an error. +// A Role Key is computed based on a sorted slice of actions for each role. +// The first map is of Role Key -> Permissions Resource Role +// The second map is of Role Key -> Member ID -> true func (s *service) mapResourceDetails(ctx context.Context, resourceID gidx.PrefixedID) (map[string]permissions.ResourceRole, map[string]map[gidx.PrefixedID]bool, error) { roles := make(map[string]permissions.ResourceRole) assignments := make(map[string]map[gidx.PrefixedID]bool) diff --git a/internal/service/process_relationships.go b/internal/service/process_relationships.go index 8b8e1d4..ebee2cd 100644 --- a/internal/service/process_relationships.go +++ b/internal/service/process_relationships.go @@ -9,13 +9,9 @@ import ( "go.equinixmetal.net/infra9-metal-bridge/internal/permissions" ) -type relationshipStats struct { - parentCreated bool - parentsDeleted int - subjectRelationshipsCreated int - subjectRelationshipsDeleted int -} - +// processRelationships determines the changes between what is wanted and what is live and executes on the differences. +// Relationship creations use events. +// Relationship deletions use the api, as delete events delete all related resources and not just the provided ones. func (s *service) processRelationships(ctx context.Context, eventType string, relationships Relationships) int { rlogger := s.logger.With("resource.id", relationships.Resource.PrefixedID()) @@ -161,6 +157,7 @@ func (s *service) processRelationships(ctx context.Context, eventType string, re return changes } +// mapRelationWants returns the parent relation if provided and a map of Subjects -> relation. func (s *service) mapRelationWants(relationships Relationships) (*Relation, map[gidx.PrefixedID]RelationshipType) { var wantParent *Relation @@ -177,6 +174,9 @@ func (s *service) mapRelationWants(relationships Relationships) (*Relation, map[ return wantParent, wantSubject } +// getRelationshipMap fetches the provided resources relationships, as the source resource and the destination subject. +// Returned are two maps, the first maps Subject IDs -> Relationship +// The second map, maps Resource IDs -> relationship 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 { diff --git a/internal/service/projects.go b/internal/service/projects.go index 7ad68f5..beb5ce1 100644 --- a/internal/service/projects.go +++ b/internal/service/projects.go @@ -11,6 +11,7 @@ import ( const projectEvent = "metalproject" +// buildProjectRelationships compiles all relations into a relationships object to be processed by the processors. func (s *service) buildProjectRelationships(project *models.ProjectDetails) (Relationships, error) { relations := Relationships{ Resource: project, @@ -39,6 +40,7 @@ func (s *service) buildProjectRelationships(project *models.ProjectDetails) (Rel return relations, nil } +// IsProjectID checks if the provided id has the metal project prefix. func (s *service) IsProjectID(id gidx.PrefixedID) bool { if idType, ok := s.idPrefixMap[id.Prefix()]; ok { return idType == TypeProject @@ -47,6 +49,7 @@ func (s *service) IsProjectID(id gidx.PrefixedID) bool { return false } +// TouchProject initializes a sync for the provided project id for relationships and memberships. func (s *service) TouchProject(ctx context.Context, id gidx.PrefixedID) error { logger := s.logger.With("project.id", id.String()) @@ -77,6 +80,7 @@ func (s *service) TouchProject(ctx context.Context, id gidx.PrefixedID) error { return nil } +// DeleteProject deletes the provided project id. func (s *service) DeleteProject(ctx context.Context, id gidx.PrefixedID) error { err := s.publisher.PublishChange(ctx, projectEvent, events.ChangeMessage{ SubjectID: id, diff --git a/internal/service/relationships.go b/internal/service/relationships.go index 252daf8..b06ee31 100644 --- a/internal/service/relationships.go +++ b/internal/service/relationships.go @@ -5,16 +5,22 @@ import ( ) const ( - RelateOwner RelationshipType = "owner" + // RelateOwner is the owner relationship type. + RelateOwner RelationshipType = "owner" + + // RelateParent is the parent relationship type. RelateParent RelationshipType = "parent" ) +// RelationshipType are relationship types. type RelationshipType string +// IDPrefixableResource ensures the the interface passed provides prefixed ids. type IDPrefixableResource interface { PrefixedID() gidx.PrefixedID } +// Relationships defines a resource and all possible relationships and memberships. type Relationships struct { Resource IDPrefixableResource Parent Relation @@ -23,11 +29,13 @@ type Relationships struct { Memberships []ResourceMemberships } +// Relation defines a relation to a resource. type Relation struct { Relation RelationshipType Resource IDPrefixableResource } +// ResourceMemberships defines a member and role. type ResourceMemberships struct { Role string Member IDPrefixableResource diff --git a/internal/service/service.go b/internal/service/service.go index aa4b2ed..a3f4a9c 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -29,8 +29,10 @@ var DefaultPrefixMap = map[string]ObjectType{ TypeUser.Prefix(): TypeUser, } +// ObjectType defines a type of object. type ObjectType string +// Prefix returns the objects id prefix. func (t ObjectType) Prefix() string { switch t { case TypeOrganization: @@ -44,6 +46,7 @@ func (t ObjectType) Prefix() string { } } +// String returns a string fo the object type. func (t ObjectType) String() string { return string(t) } @@ -96,6 +99,7 @@ func (r prefixedID) PrefixedID() gidx.PrefixedID { return r.id } +// New creates a new service. func New(publisher *events.Publisher, metal *metal.Client, perms *permissions.Client, options ...Option) (Service, error) { svc := &service{ publisher: publisher, diff --git a/internal/service/users.go b/internal/service/users.go index 71834c0..02ff9cd 100644 --- a/internal/service/users.go +++ b/internal/service/users.go @@ -6,6 +6,7 @@ import ( "go.infratographer.com/x/gidx" ) +// IsUser checks the provided id has the metal user prefix. func (s *service) IsUser(id gidx.PrefixedID) bool { if idType, ok := s.idPrefixMap[id.Prefix()]; ok { return idType == TypeUser @@ -14,6 +15,7 @@ func (s *service) IsUser(id gidx.PrefixedID) bool { return false } +// IsAssignableResource checks that the provided id is an id which can have memberships assignments. func (s *service) IsAssignableResource(id gidx.PrefixedID) bool { if idType, ok := s.idPrefixMap[id.Prefix()]; ok { switch idType { @@ -27,6 +29,7 @@ func (s *service) IsAssignableResource(id gidx.PrefixedID) bool { return false } +// Assignuser assigns the provided users to the given resource ids. func (s *service) AssignUser(ctx context.Context, userID gidx.PrefixedID, resourceIDs ...gidx.PrefixedID) error { var totalResources, rolesChanged, assignmentsChanged int @@ -68,6 +71,7 @@ func (s *service) AssignUser(ctx context.Context, userID gidx.PrefixedID, resour return nil } +// UnassignUser removes the assignment for the provided user id to the given resources. func (s *service) UnassignUser(ctx context.Context, userID gidx.PrefixedID, resourceIDs ...gidx.PrefixedID) error { for _, resourceID := range resourceIDs { rlogger := s.logger.With("user.id", userID, "resource.id", resourceID) @@ -119,6 +123,7 @@ func (s *service) UnassignUser(ctx context.Context, userID gidx.PrefixedID, reso return nil } +// getuserResourceRole fetches the appropriate object types user role for the given resource. func (s *service) getUserResourceRole(ctx context.Context, userID, resourceID gidx.PrefixedID) (string, error) { var ( role string From 1f67d3c805cae7f3921ac5482bd0aad5fd67c898 Mon Sep 17 00:00:00 2001 From: Mike Mason Date: Mon, 17 Jul 2023 19:14:23 +0000 Subject: [PATCH 10/33] remove unused variables --- internal/metal/providers/emapi/memberships.go | 2 -- internal/metal/providers/emapi/organizations.go | 2 -- internal/metal/providers/emapi/projects.go | 2 -- internal/metal/providers/emapi/users.go | 2 -- 4 files changed, 8 deletions(-) diff --git a/internal/metal/providers/emapi/memberships.go b/internal/metal/providers/emapi/memberships.go index 857497b..5ea6c48 100644 --- a/internal/metal/providers/emapi/memberships.go +++ b/internal/metal/providers/emapi/memberships.go @@ -54,8 +54,6 @@ func (m Memberships) ToDetailsWithProjectDetails(projDetails *models.ProjectDeta // Membership contains membership information. type Membership struct { - client *Client - HREF string `json:"href"` ID string `json:"id"` Roles Roles `json:"roles"` diff --git a/internal/metal/providers/emapi/organizations.go b/internal/metal/providers/emapi/organizations.go index 5f2577c..5c695a8 100644 --- a/internal/metal/providers/emapi/organizations.go +++ b/internal/metal/providers/emapi/organizations.go @@ -40,8 +40,6 @@ func (o Organizations) ToDetails() []*models.OrganizationDetails { // Organization contains organization information. type Organization struct { - client *Client - HREF string `json:"href"` ID string `json:"id"` Name string `json:"name"` diff --git a/internal/metal/providers/emapi/projects.go b/internal/metal/providers/emapi/projects.go index 42ad6a4..080b8fe 100644 --- a/internal/metal/providers/emapi/projects.go +++ b/internal/metal/providers/emapi/projects.go @@ -40,8 +40,6 @@ func (p Projects) ToDetails() []*models.ProjectDetails { // Project contains project information. type Project struct { - client *Client - HREF string `json:"href"` ID string `json:"id"` Name string `json:"name"` diff --git a/internal/metal/providers/emapi/users.go b/internal/metal/providers/emapi/users.go index 850db7b..b8f8090 100644 --- a/internal/metal/providers/emapi/users.go +++ b/internal/metal/providers/emapi/users.go @@ -40,8 +40,6 @@ func (u Users) ToDetails() []*models.UserDetails { // User contains user information. type User struct { - client *Client - HREF string `json:"href"` ID string `json:"id"` FullName string `json:"full_name"` From 4d1e9717e525d019d813ff79dfc3529f322682e5 Mon Sep 17 00:00:00 2001 From: Mike Mason Date: Mon, 17 Jul 2023 19:24:54 +0000 Subject: [PATCH 11/33] fix linting --- .golangci.yml | 56 +++++++++++++++++++ internal/metal/models/organizations.go | 2 + internal/metal/models/users.go | 2 +- internal/metal/providers/emapi/memberships.go | 8 +-- .../metal/providers/emapi/organizations.go | 2 +- internal/metal/providers/emapi/projects.go | 2 +- internal/metal/providers/emapi/users.go | 2 +- internal/permissions/assignments.go | 6 +- internal/permissions/client.go | 3 +- internal/permissions/relationships.go | 5 +- internal/permissions/roles.go | 6 +- internal/pubsub/subscriber.go | 1 + internal/service/process_relationships.go | 2 + internal/service/users.go | 1 + 14 files changed, 81 insertions(+), 17 deletions(-) create mode 100644 .golangci.yml diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..9875d33 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,56 @@ +linters-settings: + gofumpt: + extra-rules: true + +linters: + enable: + # default linters + - errcheck + - gosimple + - govet + - ineffassign + - staticcheck + - typecheck + - unused + + # additional linters + - bodyclose + - gocritic + - gocyclo + # - goerr113 + - gofmt + - goimports + - revive + - gomnd + - govet + - misspell + - noctx + - stylecheck + - whitespace + - wsl + + # - bod +issues: + exclude: + # Default excludes from `golangci-lint run --help` with EXC0002 removed + # EXC0001 errcheck: Almost all programs ignore errors on these functions and in most cases it's ok + - Error return value of .((os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*print(f|ln)?|os\.(Un)?Setenv). is not checked + # EXC0002 golint: Annoying issue about not having a comment. The rare codebase has such comments + # - (comment on exported (method|function|type|const)|should have( a package)? comment|comment should be of the form) + # EXC0003 golint: False positive when tests are defined in package 'test' + - func name will be used as test\.Test.* by other packages, and that stutters; consider calling this + # EXC0004 govet: Common false positives + - (possible misuse of unsafe.Pointer|should have signature) + # EXC0005 staticcheck: Developers tend to write in C-style with an explicit 'break' in a 'switch', so it's ok to ignore + - ineffective break statement. Did you mean to break out of the outer loop + # EXC0006 gosec: Too many false-positives on 'unsafe' usage + - Use of unsafe calls should be audited + # EXC0007 gosec: Too many false-positives for parametrized shell calls + - Subprocess launch(ed with variable|ing should be audited) + # EXC0008 gosec: Duplicated errcheck checks + - (G104|G307) + # EXC0009 gosec: Too many issues in popular repos + - (Expect directory permissions to be 0750 or less|Expect file permissions to be 0600 or less) + # EXC0010 gosec: False positive is triggered by 'src, err := ioutil.ReadFile(filename)' + - Potential file inclusion via variable + exclude-use-default: false diff --git a/internal/metal/models/organizations.go b/internal/metal/models/organizations.go index c2cd095..e99055b 100644 --- a/internal/metal/models/organizations.go +++ b/internal/metal/models/organizations.go @@ -2,6 +2,7 @@ package models import "go.infratographer.com/x/gidx" +// OrganizationDetails contains the organization and membership information. type OrganizationDetails struct { ID string `json:"id"` Name string `json:"name"` @@ -9,6 +10,7 @@ type OrganizationDetails struct { Projects []*ProjectDetails `json:"projects"` } +// PrefixedID returns the prefixed id for the organization. func (d *OrganizationDetails) PrefixedID() gidx.PrefixedID { if d.ID == "" { return gidx.NullPrefixedID diff --git a/internal/metal/models/users.go b/internal/metal/models/users.go index ecb1c4d..8c75d7c 100644 --- a/internal/metal/models/users.go +++ b/internal/metal/models/users.go @@ -11,7 +11,7 @@ const ( // MetalUserIssuer is the issuer that is used for metal api token users. MetalUserIssuer = "https://auth.equinix.com/" - // MetaluserIssuerIDPrefix is the issuer id prefix added by the issuer. + // MetalUserIssuerIDPrefix is the issuer id prefix added by the issuer. MetalUserIssuerIDPrefix = "auth|" ) diff --git a/internal/metal/providers/emapi/memberships.go b/internal/metal/providers/emapi/memberships.go index 5ea6c48..6e2b565 100644 --- a/internal/metal/providers/emapi/memberships.go +++ b/internal/metal/providers/emapi/memberships.go @@ -10,7 +10,7 @@ type Roles []string // Memberships contains a list of memberships type Memberships []*Membership -// ToDetailsWithOrganizationDetails convers the memberships to generic membership models with organization details. +// ToDetailsWithOrganizationDetails converts the memberships to generic membership models with organization details. func (m Memberships) ToDetailsWithOrganizationDetails(orgDetails *models.OrganizationDetails) []*models.Membership[models.OrganizationDetails] { memberships := make([]*models.Membership[models.OrganizationDetails], len(m)) @@ -31,7 +31,7 @@ func (m Memberships) ToDetailsWithOrganizationDetails(orgDetails *models.Organiz return memberships } -// ToDetailsWithProjectDetails convers the memberships to generic membership models with project details. +// ToDetailsWithProjectDetails converts the memberships to generic membership models with project details. func (m Memberships) ToDetailsWithProjectDetails(projDetails *models.ProjectDetails) []*models.Membership[models.ProjectDetails] { memberships := make([]*models.Membership[models.ProjectDetails], len(m)) @@ -60,7 +60,7 @@ type Membership struct { User *User `json:"user"` } -// ToDetailsWithOrganizationDetails convers the membership to generic membership model with organization details. +// ToDetailsWithOrganizationDetails converts the membership to generic membership model with organization details. func (m *Membership) ToDetailsWithOrganizationDetails(orgDetails *models.OrganizationDetails) *models.Membership[models.OrganizationDetails] { if m.ID == "" { return nil @@ -74,7 +74,7 @@ func (m *Membership) ToDetailsWithOrganizationDetails(orgDetails *models.Organiz } } -// ToDetailsWithOrganizationDetails convers the membership to generic membership model with organization details. +// ToDetailsWithProjectDetails converts the membership to generic membership model with organization details. func (m *Membership) ToDetailsWithProjectDetails(projDetails *models.ProjectDetails) *models.Membership[models.ProjectDetails] { if m.ID == "" { return nil diff --git a/internal/metal/providers/emapi/organizations.go b/internal/metal/providers/emapi/organizations.go index 5c695a8..35c1124 100644 --- a/internal/metal/providers/emapi/organizations.go +++ b/internal/metal/providers/emapi/organizations.go @@ -75,7 +75,7 @@ func (o *Organization) ToDetails() *models.OrganizationDetails { func (c *Client) getOrganizationWithMemberships(ctx context.Context, id string) (*Organization, error) { var org Organization - _, err := c.DoRequest(ctx, http.MethodGet, organizationsPath+"/"+id+"?include=memberships.user", nil, &org) + _, err := c.DoRequest(ctx, http.MethodGet, organizationsPath+"/"+id+"?include=memberships.user", nil, &org) // nolint:bodyclose // body is closed by Do json decode. if err != nil { return nil, fmt.Errorf("error loading organization: %w", err) } diff --git a/internal/metal/providers/emapi/projects.go b/internal/metal/providers/emapi/projects.go index 080b8fe..5dc27c5 100644 --- a/internal/metal/providers/emapi/projects.go +++ b/internal/metal/providers/emapi/projects.go @@ -75,7 +75,7 @@ func (p *Project) ToDetails() *models.ProjectDetails { func (c *Client) getProjectWithMemberships(ctx context.Context, id string) (*Project, error) { var project Project - _, err := c.DoRequest(ctx, http.MethodGet, projectsPath+"/"+id+"?include=memberships.user", nil, &project) + _, err := c.DoRequest(ctx, http.MethodGet, projectsPath+"/"+id+"?include=memberships.user", nil, &project) // nolint:bodyclose // body is closed by Do json decode. if err != nil { return nil, fmt.Errorf("error loading organization: %w", err) } diff --git a/internal/metal/providers/emapi/users.go b/internal/metal/providers/emapi/users.go index b8f8090..96742f5 100644 --- a/internal/metal/providers/emapi/users.go +++ b/internal/metal/providers/emapi/users.go @@ -71,7 +71,7 @@ func (u *User) ToDetails() *models.UserDetails { func (c *Client) getUser(ctx context.Context, id string) (*User, error) { var user User - _, err := c.DoRequest(ctx, http.MethodGet, usersPath+"/"+id, nil, &user) + _, err := c.DoRequest(ctx, http.MethodGet, usersPath+"/"+id, nil, &user) // nolint:bodyclose // body is closed by Do json decode. if err != nil { return nil, fmt.Errorf("error loading organization: %w", err) } diff --git a/internal/permissions/assignments.go b/internal/permissions/assignments.go index f2dadd4..895a4e2 100644 --- a/internal/permissions/assignments.go +++ b/internal/permissions/assignments.go @@ -38,7 +38,7 @@ func (c *Client) AssignRole(ctx context.Context, roleID gidx.PrefixedID, memberI var response RoleAssignResponse - if _, err = c.DoRequest(ctx, http.MethodPost, path, body, &response); err != nil { + if _, err = c.DoRequest(ctx, http.MethodPost, path, body, &response); err != nil { // nolint:bodyclose // body is closed by Do json decode. return err } @@ -62,7 +62,7 @@ func (c *Client) UnassignRole(ctx context.Context, roleID gidx.PrefixedID, membe var response RoleAssignResponse - if _, err = c.DoRequest(ctx, http.MethodDelete, path, body, &response); err != nil { + if _, err = c.DoRequest(ctx, http.MethodDelete, path, body, &response); err != nil { // nolint:bodyclose // body is closed by Do json decode. return err } @@ -79,7 +79,7 @@ func (c *Client) ListRoleAssignments(ctx context.Context, roleID gidx.PrefixedID var response roleAssignmentData - if _, err := c.DoRequest(ctx, http.MethodGet, path, nil, &response); err != nil { + if _, err := c.DoRequest(ctx, http.MethodGet, path, nil, &response); err != nil { // nolint:bodyclose // body is closed by Do json decode. return nil, err } diff --git a/internal/permissions/client.go b/internal/permissions/client.go index 1ae0f20..c6ab9ad 100644 --- a/internal/permissions/client.go +++ b/internal/permissions/client.go @@ -18,7 +18,7 @@ import ( const defaultPermissionsURL = "https://permissions-api.hollow-a.sv15.metalkube.net" var defaultHTTPClient = &http.Client{ - Timeout: 5 * time.Second, + Timeout: 5 * time.Second, // nolint:gomnd // unexported } // Client is the permissions client. @@ -104,6 +104,7 @@ func encodeJSON(v any) (*bytes.Buffer, error) { return &buff, nil } +// NewClient creats a new permissions client. func NewClient(token string, options ...Option) (*Client, error) { client := &Client{ logger: zap.NewNop().Sugar(), diff --git a/internal/permissions/relationships.go b/internal/permissions/relationships.go index 50cf314..a9e8f8d 100644 --- a/internal/permissions/relationships.go +++ b/internal/permissions/relationships.go @@ -47,7 +47,7 @@ func (c *Client) DeleteResourceRelationship(ctx context.Context, resourceID gidx var response ResourceRelationshipDeleteResponse - if _, err := c.DoRequest(ctx, http.MethodDelete, path, body, &response); err != nil { + if _, err := c.DoRequest(ctx, http.MethodDelete, path, body, &response); err != nil { // nolint:bodyclose // closed by Do on json decode. return err } @@ -75,11 +75,12 @@ func (c *Client) ListResourceRelationships(ctx context.Context, resourceID gidx. Data []resourceRelationship `json:"data"` } - if _, err := c.DoRequest(ctx, http.MethodGet, url.String(), nil, &response); err != nil { + if _, err := c.DoRequest(ctx, http.MethodGet, url.String(), nil, &response); err != nil { // nolint:bodyclose // closed by Do on json decode. return nil, err } data := make([]ResourceRelationship, len(response.Data)) + for i, entry := range response.Data { var ( resID, subID gidx.PrefixedID diff --git a/internal/permissions/roles.go b/internal/permissions/roles.go index 0a1e4fa..cf1a884 100644 --- a/internal/permissions/roles.go +++ b/internal/permissions/roles.go @@ -46,7 +46,7 @@ func (c *Client) CreateRole(ctx context.Context, resourceID gidx.PrefixedID, act var response ResourceRoleCreateResponse - if _, err = c.DoRequest(ctx, http.MethodPost, path, body, &response); err != nil { + if _, err = c.DoRequest(ctx, http.MethodPost, path, body, &response); err != nil { // nolint:bodyclose // closed by Do on json decode. return gidx.NullPrefixedID, err } @@ -64,7 +64,7 @@ func (c *Client) DeleteRole(ctx context.Context, roleID gidx.PrefixedID) error { var response ResourceRoleDeleteResponse - if _, err := c.DoRequest(ctx, http.MethodDelete, path, nil, &response); err != nil { + if _, err := c.DoRequest(ctx, http.MethodDelete, path, nil, &response); err != nil { // nolint:bodyclose // closed by Do on json decode. return err } @@ -83,7 +83,7 @@ func (c *Client) ListResourceRoles(ctx context.Context, resourceID gidx.Prefixed Data ResourceRoles `json:"data"` } - if _, err := c.DoRequest(ctx, http.MethodGet, path, nil, &response); err != nil { + if _, err := c.DoRequest(ctx, http.MethodGet, path, nil, &response); err != nil { // nolint:bodyclose // closed by Do on json decode. return nil, err } diff --git a/internal/pubsub/subscriber.go b/internal/pubsub/subscriber.go index d4642fa..eefb034 100644 --- a/internal/pubsub/subscriber.go +++ b/internal/pubsub/subscriber.go @@ -107,6 +107,7 @@ func (s Subscriber) listen(messages <-chan *message.Message, wg *sync.WaitGroup) for msg := range messages { s.logger.Infow("processing event", "event.id", msg.UUID) + if err := s.processEvent(msg); err != nil { s.logger.Warn("Failed to process msg: ", err) diff --git a/internal/service/process_relationships.go b/internal/service/process_relationships.go index ebee2cd..6e13a52 100644 --- a/internal/service/process_relationships.go +++ b/internal/service/process_relationships.go @@ -193,6 +193,7 @@ func (s *service) getRelationshipMap(ctx context.Context, resource IDPrefixableR } parents := make(map[gidx.PrefixedID]RelationshipType, len(liveResource)) + for _, relationship := range liveResource { if relationship.Relation != string(RelateParent) { continue @@ -202,6 +203,7 @@ func (s *service) getRelationshipMap(ctx context.Context, resource IDPrefixableR } subject := make(map[gidx.PrefixedID]RelationshipType, len(liveSubject)) + for _, relationship := range liveSubject { subject[relationship.ResourceID] = RelationshipType(relationship.Relation) } diff --git a/internal/service/users.go b/internal/service/users.go index 02ff9cd..66c08b5 100644 --- a/internal/service/users.go +++ b/internal/service/users.go @@ -59,6 +59,7 @@ func (s *service) AssignUser(ctx context.Context, userID gidx.PrefixedID, resour }, true) totalResources++ + rolesChanged += roles assignmentsChanged += assignments } From 544273cd0b331aa4bcf998d6092ee11ccce09fad Mon Sep 17 00:00:00 2001 From: Mike Mason Date: Mon, 17 Jul 2023 19:36:19 +0000 Subject: [PATCH 12/33] remove unused routes --- cmd/serve.go | 11 ----------- internal/routes/errors.go | 22 ---------------------- internal/routes/options.go | 27 --------------------------- internal/routes/routes.go | 32 -------------------------------- 4 files changed, 92 deletions(-) delete mode 100644 internal/routes/errors.go delete mode 100644 internal/routes/options.go delete mode 100644 internal/routes/routes.go diff --git a/cmd/serve.go b/cmd/serve.go index 5fb707a..4e05b71 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -13,7 +13,6 @@ import ( "go.equinixmetal.net/infra9-metal-bridge/internal/metal" "go.equinixmetal.net/infra9-metal-bridge/internal/permissions" "go.equinixmetal.net/infra9-metal-bridge/internal/pubsub" - "go.equinixmetal.net/infra9-metal-bridge/internal/routes" "go.equinixmetal.net/infra9-metal-bridge/internal/service" ) @@ -87,14 +86,6 @@ func serve(cmd *cobra.Command, _ []string) { } } - router, err := routes.NewRouter( - routes.WithLogger(logger.Desugar()), - routes.WithService(service), - ) - if err != nil { - logger.Fatalw("error initializing router", "error", err) - } - srv, err := echox.NewServer( logger.Desugar(), echox.ConfigFromViper(viper.GetViper()), @@ -104,8 +95,6 @@ func serve(cmd *cobra.Command, _ []string) { logger.Fatalw("failed to initialize new server", "error", err) } - srv.AddHandler(router) - defer subscriber.Close() logger.Info("Listening for events") diff --git a/internal/routes/errors.go b/internal/routes/errors.go deleted file mode 100644 index 007c34f..0000000 --- a/internal/routes/errors.go +++ /dev/null @@ -1,22 +0,0 @@ -package routes - -import ( - "errors" - "net/http" - - "github.com/labstack/echo/v4" -) - -var ( - // ErrInvalidJWTPrivateKeyType is returned when the private key type is not of an expected value. - ErrInvalidJWTPrivateKeyType = errors.New("invalid JWT private key provided") - - // ErrAuthTokenHeaderRequired is returned when a token check request is made, but the Authorization header is missing. - ErrAuthTokenHeaderRequired = echo.NewHTTPError(http.StatusBadRequest, "header Authorization missing or invalid") - - // ErrInvalidSigningMethod is returned when defined jwt signing method is not recognized. - ErrInvalidSigningMethod = errors.New("unrecognized jwt signing method provided") - - // ErrMissingIssuer is returned when the jwt issuer is not defined in the config. - ErrMissingIssuer = errors.New("jwt issuer required") -) diff --git a/internal/routes/options.go b/internal/routes/options.go deleted file mode 100644 index 817ec29..0000000 --- a/internal/routes/options.go +++ /dev/null @@ -1,27 +0,0 @@ -package routes - -import ( - "go.equinixmetal.net/infra9-metal-bridge/internal/service" - "go.uber.org/zap" -) - -// Option is a functional configuration option for the router. -type Option func(r *Router) error - -// WithLogger sets the logger for the router. -func WithLogger(logger *zap.Logger) Option { - return func(r *Router) error { - r.logger = logger - - return nil - } -} - -// WithService sets the service handler. -func WithService(svc service.Service) Option { - return func(r *Router) error { - r.svc = svc - - return nil - } -} diff --git a/internal/routes/routes.go b/internal/routes/routes.go deleted file mode 100644 index d297525..0000000 --- a/internal/routes/routes.go +++ /dev/null @@ -1,32 +0,0 @@ -// Package routes provides the routes for the application. -package routes - -import ( - "github.com/labstack/echo/v4" - "go.equinixmetal.net/infra9-metal-bridge/internal/service" - "go.uber.org/zap" -) - -// Router is the router for the application. -type Router struct { - logger *zap.Logger - svc service.Service -} - -// Routes registers the routes for the application. -func (r *Router) Routes(g *echo.Group) {} - -// NewRouter creates a new router -func NewRouter(opts ...Option) (*Router, error) { - router := Router{ - logger: zap.NewNop(), - } - - for _, opt := range opts { - if err := opt(&router); err != nil { - return nil, err - } - } - - return &router, nil -} From 5e1df7b29bf6b8185fc638d3940dd5101ae20d63 Mon Sep 17 00:00:00 2001 From: Mike Mason Date: Mon, 17 Jul 2023 19:36:37 +0000 Subject: [PATCH 13/33] add makefile --- Makefile | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..dd87810 --- /dev/null +++ b/Makefile @@ -0,0 +1,57 @@ +GOPKG=go.equinixmetal.net/infra9-metal-bridge + +ROOT_DIR := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) +TOOLS_DIR := .tools + +GO_FILES=$(shell git ls-files '*.go') + +GOTOOLS_REPO = golang.org/x/tools +GOTOOLS_VERSION = v0.9.3 + +GOLANGCI_LINT_REPO = github.com/golangci/golangci-lint +GOLANGCI_LINT_VERSION = v1.51.2 + +.PHONY: all help lint test golint fix-lint unit-test coverage privkey + + +help: Makefile ## Print help. + @grep -h "##" $(MAKEFILE_LIST) | grep -v grep | sed -e 's/:.*##/#/' | column -c 2 -t -s# + +all: lint test ## Lints and tests. + +lint: golint ## Runs all lint checks. + +golint: | $(TOOLS_DIR)/golangci-lint ## Runs Go lint checks. + @echo Linting Go files... + @$(TOOLS_DIR)/golangci-lint run --timeout=5m + +fix-lint: $(GO_FILES) | $(TOOLS_DIR)/goimports ## Runs goimports on all go files. + @echo Linting go files... + @$(TOOLS_DIR)/goimports -w -local $(GOPKG) $^ + +test: | unit-test ## Regenerate files and run unit tests. + +unit-test: ## Runs unit tests. + @echo Running unit tests... + @go test -timeout 30s -cover -short ./... + +.PHONY: coverage +coverage: ## Generates a test coverage report. + @echo Generating coverage report... + @go test -timeout 30s ./... -coverprofile=coverage.out -covermode=atomic + @go tool cover -func=coverage.out + @go tool cover -html=coverage.out + +# Tools setup +$(TOOLS_DIR): + mkdir -p $(TOOLS_DIR) + +$(TOOLS_DIR)/goimports: | $(TOOLS_DIR) + @echo "Installing $(GOTOOLS_REPO)/cmd/goimports@$(GOTOOLS_VERSION)" + @GOBIN=$(ROOT_DIR)/$(TOOLS_DIR) go install $(GOTOOLS_REPO)/cmd/goimports@$(GOTOOLS_VERSION) + +$(TOOLS_DIR)/golangci-lint: | $(TOOLS_DIR) + @echo "Installing $(GOLANGCI_LINT_REPO)/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION)" + @GOBIN=$(ROOT_DIR)/$(TOOLS_DIR) go install $(GOLANGCI_LINT_REPO)/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION) + $@ version + $@ linters From 8b93a128e44067c6d95e7726d23089dc9db130af Mon Sep 17 00:00:00 2001 From: Mike Mason Date: Mon, 17 Jul 2023 19:37:10 +0000 Subject: [PATCH 14/33] tidy go.mod --- go.mod | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 0efda62..a2b1dfe 100644 --- a/go.mod +++ b/go.mod @@ -7,11 +7,13 @@ require ( github.com/labstack/echo/v4 v4.10.2 github.com/nats-io/nats.go v1.26.0 github.com/spf13/cobra v1.7.0 + github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.16.0 go.infratographer.com/x v0.3.2 go.opentelemetry.io/otel v1.16.0 go.opentelemetry.io/otel/trace v1.16.0 go.uber.org/zap v1.24.0 + golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 ) require ( @@ -57,7 +59,6 @@ require ( github.com/spf13/afero v1.9.5 // indirect github.com/spf13/cast v1.5.1 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.4.2 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect @@ -74,7 +75,6 @@ require ( go.uber.org/atomic v1.10.0 // indirect go.uber.org/multierr v1.9.0 // indirect golang.org/x/crypto v0.9.0 // indirect - golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect golang.org/x/net v0.10.0 // indirect golang.org/x/sys v0.8.0 // indirect golang.org/x/text v0.9.0 // indirect From df0cc4aa58838d29877fcf7d750f9ade7d5d9c46 Mon Sep 17 00:00:00 2001 From: Mike Mason Date: Mon, 17 Jul 2023 19:47:21 +0000 Subject: [PATCH 15/33] update makefile --- Makefile | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index dd87810..b7b13c6 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ GOTOOLS_VERSION = v0.9.3 GOLANGCI_LINT_REPO = github.com/golangci/golangci-lint GOLANGCI_LINT_VERSION = v1.51.2 -.PHONY: all help lint test golint fix-lint unit-test coverage privkey +.PHONY: all help lint test golint fix-lint unit-test coverage help: Makefile ## Print help. @@ -35,13 +35,16 @@ unit-test: ## Runs unit tests. @echo Running unit tests... @go test -timeout 30s -cover -short ./... -.PHONY: coverage coverage: ## Generates a test coverage report. @echo Generating coverage report... @go test -timeout 30s ./... -coverprofile=coverage.out -covermode=atomic @go tool cover -func=coverage.out @go tool cover -html=coverage.out +bin: ## Builds the binary + @echo Building binary + @go build -o infra9-metal-bridge ./main.go + # Tools setup $(TOOLS_DIR): mkdir -p $(TOOLS_DIR) From fee4cf94ab7e6297ac2b80a1bb77d7e939ff89b6 Mon Sep 17 00:00:00 2001 From: Mike Mason Date: Mon, 17 Jul 2023 22:05:23 +0000 Subject: [PATCH 16/33] support client credentials --- cmd/serve.go | 14 ++++++++++ go.mod | 24 +++++++++------- go.sum | 49 +++++++++++++++++---------------- internal/config/config.go | 7 +++++ internal/permissions/client.go | 12 +++++--- internal/permissions/options.go | 11 ++++++++ 6 files changed, 79 insertions(+), 38 deletions(-) diff --git a/cmd/serve.go b/cmd/serve.go index 4e05b71..5afef9f 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -5,6 +5,7 @@ import ( "github.com/spf13/viper" "go.infratographer.com/x/echox" "go.infratographer.com/x/events" + "go.infratographer.com/x/oauth2x" "go.infratographer.com/x/otelx" "go.infratographer.com/x/versionx" "go.infratographer.com/x/viperx" @@ -34,6 +35,7 @@ func init() { serveCmd.PersistentFlags().StringSlice("events-topics", []string{}, "event topics to subscribe to") viperx.MustBindFlag(viper.GetViper(), "events.topics", serveCmd.PersistentFlags().Lookup("events-topics")) + oauth2x.MustViperFlags(viper.GetViper(), serveCmd.Flags()) permissions.MustViperFlags(viper.GetViper(), serveCmd.Flags()) } @@ -56,9 +58,21 @@ func serve(cmd *cobra.Command, _ []string) { logger.Fatalw("error initializing Metal client", "error", err) } + permHTTPClient := permissions.DefaultHTTPClient + + if config.AppConfig.OIDC.Client.ID != "" { + tokenSrc, err := oauth2x.NewClientCredentialsTokenSrc(cmd.Context(), config.AppConfig.OIDC.Client) + if err != nil { + logger.Fatalw("error initializing oauth2 client", "error", err) + } + + permHTTPClient = oauth2x.NewClient(cmd.Context(), tokenSrc) + } + perms, err := permissions.NewClient("", permissions.WithLogger(logger), permissions.WithConfig(config.AppConfig.Permissions), + permissions.WithHTTPClient(permHTTPClient), ) if err != nil { logger.Fatalw("error initializing Permissions client", "error", err) diff --git a/go.mod b/go.mod index a2b1dfe..74e7ed9 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.20 require ( github.com/ThreeDotsLabs/watermill v1.2.0 github.com/labstack/echo/v4 v4.10.2 - github.com/nats-io/nats.go v1.26.0 + github.com/nats-io/nats.go v1.27.1 github.com/spf13/cobra v1.7.0 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.16.0 @@ -13,7 +13,7 @@ require ( go.opentelemetry.io/otel v1.16.0 go.opentelemetry.io/otel/trace v1.16.0 go.uber.org/zap v1.24.0 - golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 + golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 ) require ( @@ -35,14 +35,14 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jaevor/go-nanoid v1.3.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.16.5 // indirect + github.com/klauspost/compress v1.16.7 // indirect github.com/labstack/echo-contrib v0.15.0 // indirect github.com/labstack/echo-jwt/v4 v4.2.0 // indirect github.com/labstack/gommon v0.4.0 // indirect github.com/lithammer/shortuuid/v3 v3.0.7 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.18 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect @@ -55,7 +55,7 @@ require ( github.com/prometheus/client_golang v1.14.0 // indirect github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/common v0.40.0 // indirect - github.com/prometheus/procfs v0.9.0 // indirect + github.com/prometheus/procfs v0.11.0 // indirect github.com/spf13/afero v1.9.5 // indirect github.com/spf13/cast v1.5.1 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect @@ -74,14 +74,18 @@ require ( go.opentelemetry.io/proto/otlp v0.19.0 // indirect go.uber.org/atomic v1.10.0 // indirect go.uber.org/multierr v1.9.0 // indirect - golang.org/x/crypto v0.9.0 // indirect - golang.org/x/net v0.10.0 // indirect - golang.org/x/sys v0.8.0 // indirect - golang.org/x/text v0.9.0 // indirect + golang.org/x/crypto v0.11.0 // indirect + golang.org/x/net v0.12.0 // indirect + golang.org/x/oauth2 v0.10.0 // indirect + golang.org/x/sys v0.10.0 // indirect + golang.org/x/text v0.11.0 // indirect golang.org/x/time v0.3.0 // indirect + google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect google.golang.org/grpc v1.55.0 // indirect - google.golang.org/protobuf v1.30.0 // indirect + google.golang.org/protobuf v1.31.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) + +replace go.infratographer.com/x => github.com/mikemrm/infratographer-x v0.0.0-20230717210423-5110152cd32d diff --git a/go.sum b/go.sum index 608e23d..6b85e60 100644 --- a/go.sum +++ b/go.sum @@ -49,7 +49,7 @@ github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kd github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/brianvoe/gofakeit/v6 v6.21.0 h1:tNkm9yxEbpuPK8Bx39tT4sSc5i9SUGiciLdNix+VDQY= +github.com/brianvoe/gofakeit/v6 v6.23.0 h1:pgVhyWpYq4e0GEVCh2gdZnS/nBX+8SnyTBliHg5xjks= github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -187,8 +187,8 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI= -github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= +github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -212,10 +212,12 @@ github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxec github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= -github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/mikemrm/infratographer-x v0.0.0-20230717210423-5110152cd32d h1:Ib2XipuNiPypUd70QsWm7y6eI11TShXnzGMV7fFc+3M= +github.com/mikemrm/infratographer-x v0.0.0-20230717210423-5110152cd32d/go.mod h1:pXXSdeJBisAK3AdED5EFj7Yo8z8td7fOWDkNl4Dkp0s= github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= @@ -226,8 +228,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/nats-io/jwt/v2 v2.4.1 h1:Y35W1dgbbz2SQUYDPCaclXcuqleVmpbRa7646Jf2EX4= github.com/nats-io/nats-server/v2 v2.9.17 h1:gFpUQ3hqIDJrnqog+Bl5vaXg+RhhYEZIElasEuRn2tw= -github.com/nats-io/nats.go v1.26.0 h1:fWJTYPnZ8DzxIaqIHOAMfColuznchnd5Ab5dbJpgPIE= -github.com/nats-io/nats.go v1.26.0/go.mod h1:XpbWUlOElGwTYbMR7imivs7jJj9GtK7ypv321Wp6pjc= +github.com/nats-io/nats.go v1.27.1 h1:OuYnal9aKVSnOzLQIzf7554OXMCG7KbaTkCSBHRcSoo= +github.com/nats-io/nats.go v1.27.1/go.mod h1:XpbWUlOElGwTYbMR7imivs7jJj9GtK7ypv321Wp6pjc= github.com/nats-io/nkeys v0.4.4 h1:xvBJ8d69TznjcQl9t6//Q5xXuVhyYiSos6RPtvQNTwA= github.com/nats-io/nkeys v0.4.4/go.mod h1:XUkxdLPTufzlihbamfzQ7mw/VGx6ObUs+0bN5sNvt64= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= @@ -248,8 +250,8 @@ github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvq github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= github.com/prometheus/common v0.40.0 h1:Afz7EVRqGg2Mqqf4JuF9vdvp1pi220m55Pi9T2JnO4Q= github.com/prometheus/common v0.40.0/go.mod h1:L65ZJPSmfn/UBWLQIHV7dBrKFidB/wPlF1y5TlSt9OE= -github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= -github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= +github.com/prometheus/procfs v0.11.0 h1:5EAgkfkMl659uZPbe9AS2N68a7Cc1TJbPEuGzFuRbyk= +github.com/prometheus/procfs v0.11.0/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= @@ -291,8 +293,6 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.infratographer.com/x v0.3.2 h1:AxHY77AGhWcRNcO7ENP/4Cj0xg6KGKpxFn/yZn+rPOs= -go.infratographer.com/x v0.3.2/go.mod h1:GvOhGwi/1Dp5qAQSudHUdLfFmiXzzc27KBfkH0nxnEQ= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= @@ -339,8 +339,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= -golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= +golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= +golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -351,8 +351,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc= -golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= +golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw= +golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -409,8 +409,8 @@ golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= +golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -421,7 +421,8 @@ golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8= +golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8= +golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -475,8 +476,8 @@ golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -487,8 +488,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= +golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -647,8 +648,8 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= -google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= diff --git a/internal/config/config.go b/internal/config/config.go index 9acbab2..7967d2b 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -5,6 +5,7 @@ import ( "go.infratographer.com/x/echox" "go.infratographer.com/x/events" "go.infratographer.com/x/loggingx" + "go.infratographer.com/x/oauth2x" "go.infratographer.com/x/otelx" "go.equinixmetal.net/infra9-metal-bridge/internal/metal" @@ -15,6 +16,7 @@ import ( // AppConfig is the main application configuration. var AppConfig struct { Logging loggingx.Config + OIDC OIDCClientConfig EquinixMetal metal.Config OTel otelx.Config Server echox.Config @@ -29,3 +31,8 @@ type EventsConfig struct { Publisher events.PublisherConfig Subscriber events.SubscriberConfig } + +// OIDCClientConfig defines the configuration for OIDC Client Credentials. +type OIDCClientConfig struct { + Client oauth2x.Config +} diff --git a/internal/permissions/client.go b/internal/permissions/client.go index c6ab9ad..fcc8e83 100644 --- a/internal/permissions/client.go +++ b/internal/permissions/client.go @@ -15,10 +15,14 @@ import ( "go.uber.org/zap" ) -const defaultPermissionsURL = "https://permissions-api.hollow-a.sv15.metalkube.net" +const ( + defaultPermissionsURL = "https://permissions-api.hollow-a.sv15.metalkube.net" -var defaultHTTPClient = &http.Client{ - Timeout: 5 * time.Second, // nolint:gomnd // unexported + DefaultHTTPClientTimeout = 5 * time.Second +) + +var DefaultHTTPClient = &http.Client{ + Timeout: DefaultHTTPClientTimeout, } // Client is the permissions client. @@ -108,7 +112,7 @@ func encodeJSON(v any) (*bytes.Buffer, error) { func NewClient(token string, options ...Option) (*Client, error) { client := &Client{ logger: zap.NewNop().Sugar(), - httpClient: defaultHTTPClient, + httpClient: DefaultHTTPClient, token: token, } diff --git a/internal/permissions/options.go b/internal/permissions/options.go index 61a3b2f..f3e0608 100644 --- a/internal/permissions/options.go +++ b/internal/permissions/options.go @@ -1,6 +1,8 @@ package permissions import ( + "net/http" + "go.uber.org/zap" ) @@ -15,3 +17,12 @@ func WithLogger(logger *zap.SugaredLogger) Option { return nil } } + +// WithHTTPClient sets the http client to be used by the client. +func WithHTTPClient(client *http.Client) Option { + return func(c *Client) error { + c.httpClient = client + + return nil + } +} From 99baf40c20bfceb58543ef45a913f0c8c526670e Mon Sep 17 00:00:00 2001 From: Mike Mason Date: Tue, 18 Jul 2023 13:18:24 +0000 Subject: [PATCH 17/33] move topics to be defined under events.subscriber --- cmd/serve.go | 6 +++--- internal/config/config.go | 8 +++++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/cmd/serve.go b/cmd/serve.go index 5afef9f..717e50e 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -33,7 +33,7 @@ func init() { events.MustViperFlagsForSubscriber(viper.GetViper(), serveCmd.Flags()) serveCmd.PersistentFlags().StringSlice("events-topics", []string{}, "event topics to subscribe to") - viperx.MustBindFlag(viper.GetViper(), "events.topics", serveCmd.PersistentFlags().Lookup("events-topics")) + viperx.MustBindFlag(viper.GetViper(), "events.subscriber.topics", serveCmd.PersistentFlags().Lookup("events-topics")) oauth2x.MustViperFlags(viper.GetViper(), serveCmd.Flags()) permissions.MustViperFlags(viper.GetViper(), serveCmd.Flags()) @@ -87,14 +87,14 @@ func serve(cmd *cobra.Command, _ []string) { logger.Fatalw("error initializing service", "error", err) } - subscriber, err := pubsub.NewSubscriber(cmd.Context(), config.AppConfig.Events.Subscriber, service, + subscriber, err := pubsub.NewSubscriber(cmd.Context(), config.AppConfig.Events.Subscriber.SubscriberConfig, service, pubsub.WithLogger(logger), ) if err != nil { logger.Fatalw("unable to initialize event subscriber", "error", err) } - for _, topic := range viper.GetStringSlice("events.topics") { + for _, topic := range config.AppConfig.Events.Subscriber.Topics { if err := subscriber.Subscribe(topic); err != nil { logger.Fatalw("error subscribing to topic: "+topic, "topic", topic, "error", err) } diff --git a/internal/config/config.go b/internal/config/config.go index 7967d2b..cb7ee37 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -29,10 +29,16 @@ var AppConfig struct { // EventsConfig defines the configuration setting up both subscriptions and publishing type EventsConfig struct { Publisher events.PublisherConfig - Subscriber events.SubscriberConfig + Subscriber SubscriberConfig } // OIDCClientConfig defines the configuration for OIDC Client Credentials. type OIDCClientConfig struct { Client oauth2x.Config } + +// SubscriberConfig extends events SubscriberConfig by adding topics. +type SubscriberConfig struct { + events.SubscriberConfig `mapstructure:",squash"` + Topics []string +} From 05762f5d75a5d09d09f30c9e1c03650a59520074 Mon Sep 17 00:00:00 2001 From: Mike Mason Date: Tue, 18 Jul 2023 13:23:49 +0000 Subject: [PATCH 18/33] rename sync back to process --- internal/service/organizations.go | 2 +- internal/service/process_memberships.go | 4 ++-- internal/service/projects.go | 2 +- internal/service/users.go | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/service/organizations.go b/internal/service/organizations.go index 1f5ba80..b396af4 100644 --- a/internal/service/organizations.go +++ b/internal/service/organizations.go @@ -75,7 +75,7 @@ func (s *service) TouchOrganization(ctx context.Context, id gidx.PrefixedID) err } relationshipChanges := s.processRelationships(ctx, organizationEvent, relationships) - rolesChanged, assignmentsChanged := s.syncMemberships(ctx, relationships, false) + rolesChanged, assignmentsChanged := s.processMemberships(ctx, relationships, false) s.logger.Infow("organization sync complete", "resource.id", org.PrefixedID(), diff --git a/internal/service/process_memberships.go b/internal/service/process_memberships.go index 8be2902..cbe4ff4 100644 --- a/internal/service/process_memberships.go +++ b/internal/service/process_memberships.go @@ -10,9 +10,9 @@ import ( "go.equinixmetal.net/infra9-metal-bridge/internal/permissions" ) -// syncMemberships determines the changes between what is wanted and what is live and executes on the differences. +// processMemberships determines the changes between what is wanted and what is live and executes on the differences. // If skipDeletions is true, no deletes will be executed. -func (s *service) syncMemberships(ctx context.Context, relationships Relationships, skipDeletions bool) (int, int) { +func (s *service) processMemberships(ctx context.Context, relationships Relationships, skipDeletions bool) (int, int) { if len(relationships.Memberships) == 0 { return 0, 0 } diff --git a/internal/service/projects.go b/internal/service/projects.go index beb5ce1..2400423 100644 --- a/internal/service/projects.go +++ b/internal/service/projects.go @@ -68,7 +68,7 @@ func (s *service) TouchProject(ctx context.Context, id gidx.PrefixedID) error { } relationshipChanges := s.processRelationships(ctx, projectEvent, relationships) - rolesChanged, assignmentsChanged := s.syncMemberships(ctx, relationships, false) + rolesChanged, assignmentsChanged := s.processMemberships(ctx, relationships, false) s.logger.Infow("project sync complete", "resource.id", project.PrefixedID(), diff --git a/internal/service/users.go b/internal/service/users.go index 66c08b5..a46e50b 100644 --- a/internal/service/users.go +++ b/internal/service/users.go @@ -48,7 +48,7 @@ func (s *service) AssignUser(ctx context.Context, userID gidx.PrefixedID, resour continue } - roles, assignments := s.syncMemberships(ctx, Relationships{ + roles, assignments := s.processMemberships(ctx, Relationships{ Resource: prefixedID{resourceID}, Memberships: []ResourceMemberships{ { From f828546cfeb59e25c22e0d25d4c48f01faa5b2ad Mon Sep 17 00:00:00 2001 From: Mike Mason Date: Tue, 18 Jul 2023 13:36:41 +0000 Subject: [PATCH 19/33] make client interface --- internal/metal/metal.go | 25 ++++++++++++--------- internal/metal/options.go | 8 +++---- internal/permissions/assignments.go | 8 +++---- internal/permissions/client.go | 32 ++++++++++++++++++++------- internal/permissions/config.go | 6 ++--- internal/permissions/options.go | 10 ++++----- internal/permissions/relationships.go | 4 ++-- internal/permissions/roles.go | 8 +++---- internal/service/options.go | 4 ++-- internal/service/service.go | 13 +++++++---- 10 files changed, 72 insertions(+), 46 deletions(-) diff --git a/internal/metal/metal.go b/internal/metal/metal.go index ceab2c9..779958f 100644 --- a/internal/metal/metal.go +++ b/internal/metal/metal.go @@ -10,41 +10,46 @@ import ( "go.equinixmetal.net/infra9-metal-bridge/internal/metal/providers" ) -// Client is the Equinix Metal API Client struct. -type Client struct { +// Client is the Equinix Metal Client Interface. +type Client interface { + providers.Provider +} + +// client is the Equinix Metal Client struct. +type client struct { logger *zap.Logger provider providers.Provider } // GetOrganizationDetails fetches the organization id provided with its memberships. -func (c *Client) GetOrganizationDetails(ctx context.Context, id gidx.PrefixedID) (*models.OrganizationDetails, error) { +func (c *client) GetOrganizationDetails(ctx context.Context, id gidx.PrefixedID) (*models.OrganizationDetails, error) { return c.provider.GetOrganizationDetails(ctx, id) } // GetProjectDetails fetchs the provided project id with membership information. -func (c *Client) GetProjectDetails(ctx context.Context, id gidx.PrefixedID) (*models.ProjectDetails, error) { +func (c *client) GetProjectDetails(ctx context.Context, id gidx.PrefixedID) (*models.ProjectDetails, error) { return c.provider.GetProjectDetails(ctx, id) } // GetUserDetails fetches the provided user id. -func (c *Client) GetUserDetails(ctx context.Context, id gidx.PrefixedID) (*models.UserDetails, error) { +func (c *client) GetUserDetails(ctx context.Context, id gidx.PrefixedID) (*models.UserDetails, error) { return c.provider.GetUserDetails(ctx, id) } // GetUserOrganizationRole returns the role for the user in the organization. -func (c *Client) GetUserOrganizationRole(ctx context.Context, userID, orgID gidx.PrefixedID) (string, error) { +func (c *client) GetUserOrganizationRole(ctx context.Context, userID, orgID gidx.PrefixedID) (string, error) { return c.provider.GetUserOrganizationRole(ctx, userID, orgID) } // GetUserProjectRole returns the role for the user in the project. -func (c *Client) GetUserProjectRole(ctx context.Context, userID, projID gidx.PrefixedID) (string, error) { +func (c *client) GetUserProjectRole(ctx context.Context, userID, projID gidx.PrefixedID) (string, error) { return c.provider.GetUserProjectRole(ctx, userID, projID) } -// New creates a new Client. -func New(options ...Option) (*Client, error) { - client := new(Client) +// New creates a new Equinix Metal Client. +func New(options ...Option) (Client, error) { + client := new(client) for _, opt := range options { if err := opt(client); err != nil { diff --git a/internal/metal/options.go b/internal/metal/options.go index 4d17efd..2b78af9 100644 --- a/internal/metal/options.go +++ b/internal/metal/options.go @@ -9,11 +9,11 @@ import ( ) // Option is a Client configuration Option definition. -type Option func(c *Client) error +type Option func(c *client) error // WithProvider sets the provider on the client. func WithProvider(provider providers.Provider) Option { - return func(c *Client) error { + return func(c *client) error { c.provider = provider return nil @@ -22,7 +22,7 @@ func WithProvider(provider providers.Provider) Option { // WithLogger sets the logger for the client. func WithLogger(logger *zap.Logger) Option { - return func(c *Client) error { + return func(c *client) error { c.logger = logger return nil @@ -31,7 +31,7 @@ func WithLogger(logger *zap.Logger) Option { // WithConfig applies all configurations defined in the config. func WithConfig(config Config) Option { - return func(c *Client) error { + return func(c *client) error { var options []Option if config.EMGQL.Populated() { diff --git a/internal/permissions/assignments.go b/internal/permissions/assignments.go index 895a4e2..9958e92 100644 --- a/internal/permissions/assignments.go +++ b/internal/permissions/assignments.go @@ -26,7 +26,7 @@ type roleAssignmentData struct { } // AssignRole assigns the provided member ID to the given role ID. -func (c *Client) AssignRole(ctx context.Context, roleID gidx.PrefixedID, memberID gidx.PrefixedID) error { +func (c *client) AssignRole(ctx context.Context, roleID gidx.PrefixedID, memberID gidx.PrefixedID) error { path := fmt.Sprintf("/api/v1/roles/%s/assignments", roleID.String()) body, err := encodeJSON(RoleAssign{ @@ -50,7 +50,7 @@ func (c *Client) AssignRole(ctx context.Context, roleID gidx.PrefixedID, memberI } // UnassignRole removes the provided member ID from the given role ID. -func (c *Client) UnassignRole(ctx context.Context, roleID gidx.PrefixedID, memberID gidx.PrefixedID) error { +func (c *client) UnassignRole(ctx context.Context, roleID gidx.PrefixedID, memberID gidx.PrefixedID) error { path := fmt.Sprintf("/api/v1/roles/%s/assignments", roleID.String()) body, err := encodeJSON(RoleAssign{ @@ -74,7 +74,7 @@ func (c *Client) UnassignRole(ctx context.Context, roleID gidx.PrefixedID, membe } // ListRoleAssignments lists all assignments for the given role. -func (c *Client) ListRoleAssignments(ctx context.Context, roleID gidx.PrefixedID) ([]gidx.PrefixedID, error) { +func (c *client) ListRoleAssignments(ctx context.Context, roleID gidx.PrefixedID) ([]gidx.PrefixedID, error) { path := fmt.Sprintf("/api/v1/roles/%s/assignments", roleID.String()) var response roleAssignmentData @@ -98,7 +98,7 @@ func (c *Client) ListRoleAssignments(ctx context.Context, roleID gidx.PrefixedID } // RoleHasAssignment gets the assignments for the given role and check for the provided member id. -func (c *Client) RoleHasAssignment(ctx context.Context, roleID gidx.PrefixedID, memberID gidx.PrefixedID) (bool, error) { +func (c *client) RoleHasAssignment(ctx context.Context, roleID gidx.PrefixedID, memberID gidx.PrefixedID) (bool, error) { assignments, err := c.ListRoleAssignments(ctx, roleID) if err != nil { return false, err diff --git a/internal/permissions/client.go b/internal/permissions/client.go index fcc8e83..d2eff88 100644 --- a/internal/permissions/client.go +++ b/internal/permissions/client.go @@ -12,21 +12,37 @@ import ( "time" "github.com/labstack/echo/v4" + "go.infratographer.com/x/gidx" "go.uber.org/zap" ) const ( defaultPermissionsURL = "https://permissions-api.hollow-a.sv15.metalkube.net" - DefaultHTTPClientTimeout = 5 * time.Second + defaultHTTPClientTimeout = 5 * time.Second ) +// DefaultHTTPClient is the default HTTP client for the Permissions Client. var DefaultHTTPClient = &http.Client{ - Timeout: DefaultHTTPClientTimeout, + Timeout: defaultHTTPClientTimeout, } -// Client is the permissions client. -type Client struct { +// Client defines the Permissions API client interface. +type Client interface { + AssignRole(ctx context.Context, roleID gidx.PrefixedID, memberID gidx.PrefixedID) error + CreateRole(ctx context.Context, resourceID gidx.PrefixedID, actions []string) (gidx.PrefixedID, error) + DeleteResourceRelationship(ctx context.Context, resourceID gidx.PrefixedID, relation string, relatedResourceID gidx.PrefixedID) error + DeleteRole(ctx context.Context, roleID gidx.PrefixedID) error + FindResourceRoleByActions(ctx context.Context, resourceID gidx.PrefixedID, actions []string) (ResourceRole, error) + ListResourceRelationships(ctx context.Context, resourceID gidx.PrefixedID, relatedResourceType string) ([]ResourceRelationship, error) + ListResourceRoles(ctx context.Context, resourceID gidx.PrefixedID) (ResourceRoles, error) + ListRoleAssignments(ctx context.Context, roleID gidx.PrefixedID) ([]gidx.PrefixedID, error) + RoleHasAssignment(ctx context.Context, roleID gidx.PrefixedID, memberID gidx.PrefixedID) (bool, error) + UnassignRole(ctx context.Context, roleID gidx.PrefixedID, memberID gidx.PrefixedID) error +} + +// client is the permissions client. +type client struct { logger *zap.SugaredLogger httpClient *http.Client @@ -39,7 +55,7 @@ type Client struct { // Do executes the provided request. // If the out value is provided, the response will attempt to be json decoded. -func (c *Client) Do(req *http.Request, out any) (*http.Response, error) { +func (c *client) Do(req *http.Request, out any) (*http.Response, error) { if c.token != "" { req.Header.Set(echo.HeaderAuthorization, "Bearer "+c.token) } @@ -63,7 +79,7 @@ func (c *Client) Do(req *http.Request, out any) (*http.Response, error) { } // DoRequest creates a new request from the provided parameters and executes the request. -func (c *Client) DoRequest(ctx context.Context, method, path string, body io.Reader, out any) (*http.Response, error) { +func (c *client) DoRequest(ctx context.Context, method, path string, body io.Reader, out any) (*http.Response, error) { path = strings.TrimPrefix(path, c.baseURL.Path) pathURL, err := url.Parse(path) @@ -109,8 +125,8 @@ func encodeJSON(v any) (*bytes.Buffer, error) { } // NewClient creats a new permissions client. -func NewClient(token string, options ...Option) (*Client, error) { - client := &Client{ +func NewClient(token string, options ...Option) (Client, error) { + client := &client{ logger: zap.NewNop().Sugar(), httpClient: DefaultHTTPClient, token: token, diff --git a/internal/permissions/config.go b/internal/permissions/config.go index f41d9cc..7a3e1bd 100644 --- a/internal/permissions/config.go +++ b/internal/permissions/config.go @@ -29,7 +29,7 @@ func MustViperFlags(v *viper.Viper, flags *pflag.FlagSet) { // WithConfig applies all configurations defined in the config. func WithConfig(config Config) Option { - return func(c *Client) error { + return func(c *client) error { var options []Option if config.BaseURL != "" { @@ -52,7 +52,7 @@ func WithConfig(config Config) Option { // WithBaseURL updates the baseurl used by the client. func WithBaseURL(baseURL string) Option { - return func(c *Client) error { + return func(c *client) error { u, err := url.Parse(baseURL) if err != nil { return fmt.Errorf("failed to parse emapi base url %s: %w", baseURL, err) @@ -66,7 +66,7 @@ func WithBaseURL(baseURL string) Option { // WithBearerToken sets the bearer token to authenticate the request with. func WithBearerToken(token string) Option { - return func(c *Client) error { + return func(c *client) error { c.token = token return nil diff --git a/internal/permissions/options.go b/internal/permissions/options.go index f3e0608..3b8a3b2 100644 --- a/internal/permissions/options.go +++ b/internal/permissions/options.go @@ -7,11 +7,11 @@ import ( ) // Option is a client configuration option definition. -type Option func(*Client) error +type Option func(*client) error // WithLogger sets the logger for the client. func WithLogger(logger *zap.SugaredLogger) Option { - return func(c *Client) error { + return func(c *client) error { c.logger = logger return nil @@ -19,9 +19,9 @@ func WithLogger(logger *zap.SugaredLogger) Option { } // WithHTTPClient sets the http client to be used by the client. -func WithHTTPClient(client *http.Client) Option { - return func(c *Client) error { - c.httpClient = client +func WithHTTPClient(httpClient *http.Client) Option { + return func(c *client) error { + c.httpClient = httpClient return nil } diff --git a/internal/permissions/relationships.go b/internal/permissions/relationships.go index a9e8f8d..5d79a1d 100644 --- a/internal/permissions/relationships.go +++ b/internal/permissions/relationships.go @@ -34,7 +34,7 @@ type ResourceRelationshipDeleteResponse struct { } // DeleteResourceRelationship deletes the provided resources relationship to the given subject id. -func (c *Client) DeleteResourceRelationship(ctx context.Context, resourceID gidx.PrefixedID, relation string, relatedResourceID gidx.PrefixedID) error { +func (c *client) DeleteResourceRelationship(ctx context.Context, resourceID gidx.PrefixedID, relation string, relatedResourceID gidx.PrefixedID) error { path := fmt.Sprintf("/api/v1/resources/%s/relationships", resourceID.String()) body, err := encodeJSON(ResourceRelationshipRequest{ @@ -61,7 +61,7 @@ func (c *Client) DeleteResourceRelationship(ctx context.Context, resourceID gidx // ListResourceRelationships returns resources related to the given id. // If relatedResourceType is not provied, relations to subjects are returned. // If relatedResourceType is provided, relations to the given resource are returned which match the given type. -func (c *Client) ListResourceRelationships(ctx context.Context, resourceID gidx.PrefixedID, relatedResourceType string) ([]ResourceRelationship, error) { +func (c *client) ListResourceRelationships(ctx context.Context, resourceID gidx.PrefixedID, relatedResourceType string) ([]ResourceRelationship, error) { query := url.Values{ "resourceType": []string{relatedResourceType}, } diff --git a/internal/permissions/roles.go b/internal/permissions/roles.go index cf1a884..08ad258 100644 --- a/internal/permissions/roles.go +++ b/internal/permissions/roles.go @@ -34,7 +34,7 @@ type ResourceRole struct { } // CreateRole creates a role on the given resource id with the provided actions. -func (c *Client) CreateRole(ctx context.Context, resourceID gidx.PrefixedID, actions []string) (gidx.PrefixedID, error) { +func (c *client) CreateRole(ctx context.Context, resourceID gidx.PrefixedID, actions []string) (gidx.PrefixedID, error) { path := fmt.Sprintf("/api/v1/resources/%s/roles", resourceID.String()) body, err := encodeJSON(ResourceRoleCreate{ @@ -59,7 +59,7 @@ func (c *Client) CreateRole(ctx context.Context, resourceID gidx.PrefixedID, act } // DeleteRole deletes the provided role. -func (c *Client) DeleteRole(ctx context.Context, roleID gidx.PrefixedID) error { +func (c *client) DeleteRole(ctx context.Context, roleID gidx.PrefixedID) error { path := fmt.Sprintf("/api/v1/roles/%s", roleID.String()) var response ResourceRoleDeleteResponse @@ -76,7 +76,7 @@ func (c *Client) DeleteRole(ctx context.Context, roleID gidx.PrefixedID) error { } // ListResourceRoles fetches all roles assigned to the provided resource. -func (c *Client) ListResourceRoles(ctx context.Context, resourceID gidx.PrefixedID) (ResourceRoles, error) { +func (c *client) ListResourceRoles(ctx context.Context, resourceID gidx.PrefixedID) (ResourceRoles, error) { path := fmt.Sprintf("/api/v1/resources/%s/roles", resourceID.String()) var response struct { @@ -91,7 +91,7 @@ func (c *Client) ListResourceRoles(ctx context.Context, resourceID gidx.Prefixed } // FindResourceRoleByActions fetches roles assigned to the provided resource and finds the first role where the actions match the provided actions. -func (c *Client) FindResourceRoleByActions(ctx context.Context, resourceID gidx.PrefixedID, actions []string) (ResourceRole, error) { +func (c *client) FindResourceRoleByActions(ctx context.Context, resourceID gidx.PrefixedID, actions []string) (ResourceRole, error) { roles, err := c.ListResourceRoles(ctx, resourceID) if err != nil { return ResourceRole{}, err diff --git a/internal/service/options.go b/internal/service/options.go index f1f7e6f..fc7b8a3 100644 --- a/internal/service/options.go +++ b/internal/service/options.go @@ -21,7 +21,7 @@ func WithLogger(logger *zap.SugaredLogger) Option { } // WithMetalClient sets the Equinix Metal client used by the service. -func WithMetalClient(client *metal.Client) Option { +func WithMetalClient(client metal.Client) Option { return func(s *service) error { s.metal = client @@ -30,7 +30,7 @@ func WithMetalClient(client *metal.Client) Option { } // WithPermissionsClient sets the permissions client used by the service. -func WithPermissionsClient(client *permissions.Client) Option { +func WithPermissionsClient(client permissions.Client) Option { return func(s *service) error { s.perms = client diff --git a/internal/service/service.go b/internal/service/service.go index a3f4a9c..5ea1ab7 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -78,13 +78,18 @@ type Service interface { IsAssignableResource(id gidx.PrefixedID) bool } +// EventPublisher defines the required methods to publish events. +type EventPublisher interface { + PublishChange(ctx context.Context, subjectType string, change events.ChangeMessage) error +} + var _ Service = &service{} type service struct { logger *zap.SugaredLogger - publisher *events.Publisher - metal *metal.Client - perms *permissions.Client + publisher EventPublisher + metal metal.Client + perms permissions.Client idPrefixMap map[string]ObjectType rootResource prefixedID @@ -100,7 +105,7 @@ func (r prefixedID) PrefixedID() gidx.PrefixedID { } // New creates a new service. -func New(publisher *events.Publisher, metal *metal.Client, perms *permissions.Client, options ...Option) (Service, error) { +func New(publisher EventPublisher, metal metal.Client, perms permissions.Client, options ...Option) (Service, error) { svc := &service{ publisher: publisher, metal: metal, From 4fbd69e170e51635a077c3c5ddb40b302115b637 Mon Sep 17 00:00:00 2001 From: Mike Mason Date: Tue, 18 Jul 2023 13:40:44 +0000 Subject: [PATCH 20/33] update config-example with client credentials --- config-example.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/config-example.yaml b/config-example.yaml index fdc4060..219aaf6 100644 --- a/config-example.yaml +++ b/config-example.yaml @@ -1,3 +1,9 @@ +oidc: + client: + id: idntcli-abc123 + secret: somesecret + issuer: http://mock-oauth2-server:8081/default + events: topics: - '*.*' From 321e872b465679849f16b36277f7b837c5df70e7 Mon Sep 17 00:00:00 2001 From: Mike Mason Date: Tue, 18 Jul 2023 13:41:03 +0000 Subject: [PATCH 21/33] update config-example move topics --- config-example.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config-example.yaml b/config-example.yaml index 219aaf6..2e0cf0a 100644 --- a/config-example.yaml +++ b/config-example.yaml @@ -5,9 +5,9 @@ oidc: issuer: http://mock-oauth2-server:8081/default events: - topics: - - '*.*' subscriber: + topics: + - '*.*' url: nats://nats:4222 prefix: com.equinixmetal queueGroup: metal-bridge From 44fd32567b5fdb01262ef8f44f5b9f2f2acf51b5 Mon Sep 17 00:00:00 2001 From: Mike Mason Date: Tue, 18 Jul 2023 19:48:52 +0000 Subject: [PATCH 22/33] update permissions relationships calls to use from/to --- internal/permissions/client.go | 3 +- internal/permissions/relationships.go | 70 +++++++++++------------ internal/service/organizations.go | 2 +- internal/service/process_relationships.go | 16 ++++-- internal/service/relationships.go | 2 +- 5 files changed, 49 insertions(+), 44 deletions(-) diff --git a/internal/permissions/client.go b/internal/permissions/client.go index d2eff88..e449d1b 100644 --- a/internal/permissions/client.go +++ b/internal/permissions/client.go @@ -34,7 +34,8 @@ type Client interface { DeleteResourceRelationship(ctx context.Context, resourceID gidx.PrefixedID, relation string, relatedResourceID gidx.PrefixedID) error DeleteRole(ctx context.Context, roleID gidx.PrefixedID) error FindResourceRoleByActions(ctx context.Context, resourceID gidx.PrefixedID, actions []string) (ResourceRole, error) - ListResourceRelationships(ctx context.Context, resourceID gidx.PrefixedID, relatedResourceType string) ([]ResourceRelationship, error) + ListResourceRelationshipsFrom(ctx context.Context, resourceID gidx.PrefixedID) ([]ResourceRelationship, error) + ListResourceRelationshipsTo(ctx context.Context, resourceID gidx.PrefixedID) ([]ResourceRelationship, error) ListResourceRoles(ctx context.Context, resourceID gidx.PrefixedID) (ResourceRoles, error) ListRoleAssignments(ctx context.Context, roleID gidx.PrefixedID) ([]gidx.PrefixedID, error) RoleHasAssignment(ctx context.Context, roleID gidx.PrefixedID, memberID gidx.PrefixedID) (bool, error) diff --git a/internal/permissions/relationships.go b/internal/permissions/relationships.go index 5d79a1d..fe7d744 100644 --- a/internal/permissions/relationships.go +++ b/internal/permissions/relationships.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "net/http" - "net/url" "go.infratographer.com/x/gidx" ) @@ -58,53 +57,54 @@ func (c *client) DeleteResourceRelationship(ctx context.Context, resourceID gidx return nil } -// ListResourceRelationships returns resources related to the given id. -// If relatedResourceType is not provied, relations to subjects are returned. -// If relatedResourceType is provided, relations to the given resource are returned which match the given type. -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(), - } - +// ListResourceRelationshipsFrom returns resources related to the given id. +func (c *client) ListResourceRelationshipsFrom(ctx context.Context, resourceID gidx.PrefixedID) ([]ResourceRelationship, error) { var response struct { Data []resourceRelationship `json:"data"` } - if _, err := c.DoRequest(ctx, http.MethodGet, url.String(), nil, &response); err != nil { // nolint:bodyclose // closed by Do on json decode. + if _, err := c.DoRequest(ctx, http.MethodGet, fmt.Sprintf("/api/v1/relationships/from/%s", resourceID.String()), nil, &response); err != nil { // nolint:bodyclose // closed by Do on json decode. 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 - } + subID, err := gidx.Parse(entry.SubjectID) + if err != nil { + return nil, err } data[i] = ResourceRelationship{ - ResourceID: resID, - Relation: entry.Relation, - SubjectID: subID, + Relation: entry.Relation, + SubjectID: subID, + } + } + + return data, nil +} + +// ListResourceRelationshipsTo returns resources related to the given id. +func (c *client) ListResourceRelationshipsTo(ctx context.Context, resourceID gidx.PrefixedID) ([]ResourceRelationship, error) { + var response struct { + Data []resourceRelationship `json:"data"` + } + + if _, err := c.DoRequest(ctx, http.MethodGet, fmt.Sprintf("/api/v1/relationships/to/%s", resourceID.String()), nil, &response); err != nil { // nolint:bodyclose // closed by Do on json decode. + return nil, err + } + + data := make([]ResourceRelationship, len(response.Data)) + + for i, entry := range response.Data { + resID, err := gidx.Parse(entry.ResourceID) + if err != nil { + return nil, err + } + + data[i] = ResourceRelationship{ + ResourceID: resID, + Relation: entry.Relation, } } diff --git a/internal/service/organizations.go b/internal/service/organizations.go index b396af4..d91136e 100644 --- a/internal/service/organizations.go +++ b/internal/service/organizations.go @@ -19,7 +19,7 @@ func (s *service) buildOrganizationRelationships(org *models.OrganizationDetails Relation: RelateParent, Resource: s.rootResource, }, - SubjectType: TypeProject, + SubjectRelation: RelateParent, } for _, member := range org.Memberships { diff --git a/internal/service/process_relationships.go b/internal/service/process_relationships.go index 6e13a52..e0a7ec2 100644 --- a/internal/service/process_relationships.go +++ b/internal/service/process_relationships.go @@ -17,10 +17,10 @@ func (s *service) processRelationships(ctx context.Context, eventType string, re wantParentRelationship, wantSubjectRelationships := s.mapRelationWants(relationships) - liveParentRelationships, liveSubjectRelationships, err := s.getRelationshipMap(ctx, relationships.Resource, relationships.SubjectType) + liveParentRelationships, liveSubjectRelationships, err := s.getRelationshipMap(ctx, relationships.Resource, relationships.SubjectRelation) if err != nil { rlogger.Errorw("failed to get relationship map", - "relationships.subject_type", relationships.SubjectType, + "relationships.subject_relation", relationships.SubjectRelation, "error", err, ) @@ -177,16 +177,16 @@ func (s *service) mapRelationWants(relationships Relationships) (*Relation, map[ // getRelationshipMap fetches the provided resources relationships, as the source resource and the destination subject. // Returned are two maps, the first maps Subject IDs -> Relationship // The second map, maps Resource IDs -> relationship -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(), "") +func (s *service) getRelationshipMap(ctx context.Context, resource IDPrefixableResource, relation RelationshipType) (map[gidx.PrefixedID]RelationshipType, map[gidx.PrefixedID]RelationshipType, error) { + liveResource, err := s.perms.ListResourceRelationshipsFrom(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 relation != "" { + liveSubject, err = s.perms.ListResourceRelationshipsTo(ctx, resource.PrefixedID()) if err != nil { return nil, nil, err } @@ -205,6 +205,10 @@ func (s *service) getRelationshipMap(ctx context.Context, resource IDPrefixableR subject := make(map[gidx.PrefixedID]RelationshipType, len(liveSubject)) for _, relationship := range liveSubject { + if relationship.Relation != string(relation) { + continue + } + subject[relationship.ResourceID] = RelationshipType(relationship.Relation) } diff --git a/internal/service/relationships.go b/internal/service/relationships.go index b06ee31..bb665b6 100644 --- a/internal/service/relationships.go +++ b/internal/service/relationships.go @@ -24,7 +24,7 @@ type IDPrefixableResource interface { type Relationships struct { Resource IDPrefixableResource Parent Relation - SubjectType ObjectType + SubjectRelation RelationshipType SubjectRelationships []Relation Memberships []ResourceMemberships } From f5103fb3fa90810b605c24ec536e4edfad1d47ac Mon Sep 17 00:00:00 2001 From: Mike Mason Date: Tue, 18 Jul 2023 19:50:27 +0000 Subject: [PATCH 23/33] update to latest infratographer/x with oauth2 client credentials --- go.mod | 4 +--- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index 74e7ed9..78ee24a 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/spf13/cobra v1.7.0 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.16.0 - go.infratographer.com/x v0.3.2 + go.infratographer.com/x v0.3.3 go.opentelemetry.io/otel v1.16.0 go.opentelemetry.io/otel/trace v1.16.0 go.uber.org/zap v1.24.0 @@ -87,5 +87,3 @@ require ( gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) - -replace go.infratographer.com/x => github.com/mikemrm/infratographer-x v0.0.0-20230717210423-5110152cd32d diff --git a/go.sum b/go.sum index 6b85e60..2b2765e 100644 --- a/go.sum +++ b/go.sum @@ -216,8 +216,6 @@ github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APP github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= -github.com/mikemrm/infratographer-x v0.0.0-20230717210423-5110152cd32d h1:Ib2XipuNiPypUd70QsWm7y6eI11TShXnzGMV7fFc+3M= -github.com/mikemrm/infratographer-x v0.0.0-20230717210423-5110152cd32d/go.mod h1:pXXSdeJBisAK3AdED5EFj7Yo8z8td7fOWDkNl4Dkp0s= github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= @@ -293,6 +291,8 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.infratographer.com/x v0.3.3 h1:dTaLEp75RgL0JxKJhrcuQTP4a2x/MrevvZ3OdtkEhCs= +go.infratographer.com/x v0.3.3/go.mod h1:pXXSdeJBisAK3AdED5EFj7Yo8z8td7fOWDkNl4Dkp0s= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= From 8186433cfb964ec02b263336f767225cf191a40f Mon Sep 17 00:00:00 2001 From: Mike Mason Date: Tue, 18 Jul 2023 23:05:57 +0000 Subject: [PATCH 24/33] add buildkite pipline --- .buildkite/pipeline.yml | 82 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 .buildkite/pipeline.yml diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml new file mode 100644 index 0000000..69006fe --- /dev/null +++ b/.buildkite/pipeline.yml @@ -0,0 +1,82 @@ +env: + ARTIFACT_NAME: bk-infra9-metal-bridge + BIN_NAME: infra9-metal-bridge + DEPLOYMENT_REPO: k8s-infra9-metal-bridge + GOPRIVATE: go.equinixmetal.net/* + IMAGE_TAG: ${BUILDKITE_BUILD_NUMBER}-${BUILDKITE_COMMIT:0:8} + QUAY_REPO: quay.io/equinixmetal/infra9-metal-bridge + +steps: + - label: ":golangci-lint: lint" + key: "lint" + command: | + make lint + plugins: + - docker#v5.3.0: + image: "golangci/golangci-lint:v1.51.2" + volumes: + - "/var/lib/buildkite-agent/.gitconfig/:/root/.gitconfig/" + + - label: ":test_tube: test" + key: "test" + command: | + make test + plugins: + - docker#v5.3.0: + image: "golang:1.20" + volumes: + - "/var/lib/buildkite-agent/.gitconfig/:/root/.gitconfig/" + + - label: ":golang: build" + key: "gobuild" + commands: | + CGO_ENABLED=0 go build -o ${ARTIFACT_NAME} -buildvcs=false ./main.go + artifact_paths: "${ARTIFACT_NAME}" + plugins: + - docker#v5.6.0: + image: "golang:1.20" + volumes: + - "/var/lib/buildkite-agent/.gitconfig:/root/.gitconfig" + + - label: ":whale: docker build" + key: "build" + depends_on: ["lint", "test", "gobuild"] + commands: | + #!/bin/bash + echo --- Retrieve artifacts + buildkite-agent artifact download "${ARTIFACT_NAME}" . + mv "${ARTIFACT_NAME}" "${BIN_NAME}" + + # make sure it is executable + chmod +x ${BIN_NAME} + + echo --- Build Docker Image + docker build . -t "$QUAY_REPO:$IMAGE_TAG" + + echo --- Push Docker Image + docker push "$QUAY_REPO:$IMAGE_TAG" + + buildkite-agent annotate --style "success" "Image pushed to quay [$QUAY_REPO:$IMAGE_TAG](https://$QUAY_REPO:$IMAGE_TAG)" + + # For main commits, pull-requests will be created to bump the image in the deployment manifest + - label: "Bump image tag for main branch builds" + depends_on: + - "build" + if: build.branch == 'main' + plugins: + - first-aml/git-clone: + repository: git@github.com:equinixmetal/$DEPLOYMENT_REPO.git + - ssh://git@github.com/equinixmetal/ssm-buildkite-plugin#v1.0.3: + parameters: + GITHUB_TOKEN: /buildkite/github/personal-access-token/v1 + - ssh://git@github.com/packethost/yaml-update-buildkite-plugin#v1.0.1: + dir: $DEPLOYMENT_REPO + file: values.yaml + values: + - .bridge.image.tag=$IMAGE_TAG + - ssh://git@github.com/equinixmetal/github-pr-template-buildkite-plugin#v0.2.0: {} + # Create Pull Request to main using commit from previous step + - envato/github-pull-request#v0.4.0: + title: '[buildkite] bump image tag to $IMAGE_TAG' + head: buildkite-yaml-update-$BUILDKITE_BUILD_NUMBER + base: main From 7c22b97a7cd4adf2e26bdfae9dc396690155f890 Mon Sep 17 00:00:00 2001 From: Mike Mason Date: Wed, 19 Jul 2023 14:32:24 +0000 Subject: [PATCH 25/33] add testing of org and user processing --- go.mod | 4 + go.sum | 1 + internal/metal/providers/mock.go | 52 ++++ internal/permissions/mock.go | 90 +++++++ internal/service/organizations_test.go | 357 +++++++++++++++++++++++++ internal/service/projects_test.go | 303 +++++++++++++++++++++ 6 files changed, 807 insertions(+) create mode 100644 internal/metal/providers/mock.go create mode 100644 internal/permissions/mock.go create mode 100644 internal/service/organizations_test.go create mode 100644 internal/service/projects_test.go diff --git a/go.mod b/go.mod index 78ee24a..cfb8dce 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/spf13/cobra v1.7.0 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.16.0 + github.com/stretchr/testify v1.8.4 go.infratographer.com/x v0.3.3 go.opentelemetry.io/otel v1.16.0 go.opentelemetry.io/otel/trace v1.16.0 @@ -22,6 +23,7 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/garsue/watermillzap v1.2.0 // indirect github.com/go-logr/logr v1.2.4 // indirect @@ -52,6 +54,7 @@ require ( github.com/oklog/ulid v1.3.1 // indirect github.com/pelletier/go-toml/v2 v2.0.8 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.14.0 // indirect github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/common v0.40.0 // indirect @@ -59,6 +62,7 @@ require ( github.com/spf13/afero v1.9.5 // indirect github.com/spf13/cast v1.5.1 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/stretchr/objx v0.5.0 // indirect github.com/subosito/gotenv v1.4.2 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect diff --git a/go.sum b/go.sum index 2b2765e..b7826d7 100644 --- a/go.sum +++ b/go.sum @@ -280,6 +280,7 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= diff --git a/internal/metal/providers/mock.go b/internal/metal/providers/mock.go new file mode 100644 index 0000000..91ee7a9 --- /dev/null +++ b/internal/metal/providers/mock.go @@ -0,0 +1,52 @@ +package providers + +import ( + "context" + + "github.com/stretchr/testify/mock" + "go.infratographer.com/x/gidx" + + "go.equinixmetal.net/infra9-metal-bridge/internal/metal/models" +) + +var _ Provider = &MockProvider{} + +// MockProvider implements Provider used for testing. +type MockProvider struct { + mock.Mock +} + +// GetOrganizationDetails implements Provider used for testing. +func (p *MockProvider) GetOrganizationDetails(ctx context.Context, id gidx.PrefixedID) (*models.OrganizationDetails, error) { + args := p.Called(id) + + return args.Get(0).(*models.OrganizationDetails), args.Error(1) +} + +// GetProjectDetails implements Provider used for testing. +func (p *MockProvider) GetProjectDetails(ctx context.Context, id gidx.PrefixedID) (*models.ProjectDetails, error) { + args := p.Called(id) + + return args.Get(0).(*models.ProjectDetails), args.Error(1) +} + +// GetUserDetails implements Provider used for testing. +func (p *MockProvider) GetUserDetails(ctx context.Context, id gidx.PrefixedID) (*models.UserDetails, error) { + args := p.Called(id) + + return args.Get(0).(*models.UserDetails), args.Error(1) +} + +// GetUserOrganizationRole implements Provider used for testing. +func (p *MockProvider) GetUserOrganizationRole(ctx context.Context, userID, orgID gidx.PrefixedID) (string, error) { + args := p.Called(userID, orgID) + + return args.String(0), args.Error(1) +} + +// GetUserProjectRole implements Provider used for testing. +func (p *MockProvider) GetUserProjectRole(ctx context.Context, userID, projID gidx.PrefixedID) (string, error) { + args := p.Called(userID, projID) + + return args.String(0), args.Error(1) +} diff --git a/internal/permissions/mock.go b/internal/permissions/mock.go new file mode 100644 index 0000000..8dac744 --- /dev/null +++ b/internal/permissions/mock.go @@ -0,0 +1,90 @@ +package permissions + +import ( + "context" + + "github.com/stretchr/testify/mock" + "go.infratographer.com/x/gidx" +) + +// MockClient implements Client for testing. +type MockClient struct { + mock.Mock +} + +// AssignRole implements Client for testing. +func (c *MockClient) AssignRole(ctx context.Context, roleID gidx.PrefixedID, memberID gidx.PrefixedID) error { + args := c.Called(roleID, memberID) + + return args.Error(0) +} + +// CreateRole implements Client for testing. +func (c *MockClient) CreateRole(ctx context.Context, resourceID gidx.PrefixedID, actions []string) (gidx.PrefixedID, error) { + args := c.Called(resourceID, actions) + + return args.Get(0).(gidx.PrefixedID), args.Error(1) +} + +// DeleteResourceRelationship implements Client for testing. +func (c *MockClient) DeleteResourceRelationship(ctx context.Context, resourceID gidx.PrefixedID, relation string, relatedResourceID gidx.PrefixedID) error { + args := c.Called(resourceID, relation, relatedResourceID) + + return args.Error(0) +} + +// DeleteRole implements Client for testing. +func (c *MockClient) DeleteRole(ctx context.Context, roleID gidx.PrefixedID) error { + args := c.Called(roleID) + + return args.Error(0) +} + +// FindResourceRoleByActions implements Client for testing. +func (c *MockClient) FindResourceRoleByActions(ctx context.Context, resourceID gidx.PrefixedID, actions []string) (ResourceRole, error) { + args := c.Called(resourceID, actions) + + return args.Get(0).(ResourceRole), args.Error(1) +} + +// ListResourceRelationshipsFrom implements Client for testing. +func (c *MockClient) ListResourceRelationshipsFrom(ctx context.Context, resourceID gidx.PrefixedID) ([]ResourceRelationship, error) { + args := c.Called(resourceID) + + return args.Get(0).([]ResourceRelationship), args.Error(1) +} + +// ListResourceRelationshipsTo implements Client for testing. +func (c *MockClient) ListResourceRelationshipsTo(ctx context.Context, resourceID gidx.PrefixedID) ([]ResourceRelationship, error) { + args := c.Called(resourceID) + + return args.Get(0).([]ResourceRelationship), args.Error(1) +} + +// ListResourceRoles implements Client for testing. +func (c *MockClient) ListResourceRoles(ctx context.Context, resourceID gidx.PrefixedID) (ResourceRoles, error) { + args := c.Called(resourceID) + + return args.Get(0).(ResourceRoles), args.Error(1) +} + +// ListRoleAssignments implements Client for testing. +func (c *MockClient) ListRoleAssignments(ctx context.Context, roleID gidx.PrefixedID) ([]gidx.PrefixedID, error) { + args := c.Called(roleID) + + return args.Get(0).([]gidx.PrefixedID), args.Error(1) +} + +// RoleHasAssignment implements Client for testing. +func (c *MockClient) RoleHasAssignment(ctx context.Context, roleID gidx.PrefixedID, memberID gidx.PrefixedID) (bool, error) { + args := c.Called(roleID, memberID) + + return args.Bool(0), args.Error(1) +} + +// UnassignRole implements Client for testing. +func (c *MockClient) UnassignRole(ctx context.Context, roleID gidx.PrefixedID, memberID gidx.PrefixedID) error { + args := c.Called(roleID, memberID) + + return args.Error(0) +} diff --git a/internal/service/organizations_test.go b/internal/service/organizations_test.go new file mode 100644 index 0000000..ead7efa --- /dev/null +++ b/internal/service/organizations_test.go @@ -0,0 +1,357 @@ +package service_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "go.infratographer.com/x/events" + "go.infratographer.com/x/gidx" + + "go.equinixmetal.net/infra9-metal-bridge/internal/metal/models" + "go.equinixmetal.net/infra9-metal-bridge/internal/metal/providers" + "go.equinixmetal.net/infra9-metal-bridge/internal/permissions" + "go.equinixmetal.net/infra9-metal-bridge/internal/service" +) + +type mockPublisher struct { + mock.Mock +} + +func (p *mockPublisher) PublishChange(ctx context.Context, subjectType string, change events.ChangeMessage) error { + args := p.Called(subjectType, change) + + return args.Error(0) +} + +func TestTouchOrganizationEmpty(t *testing.T) { + rootTenantID := gidx.PrefixedID("tnntten-root1") + + roleMap := map[string][]string{ + "owner": { + "action1", + "action2", + }, + } + + userID := gidx.PrefixedID("idntusr-2s-9kVNPJBaInlHpRs3lAMsvU_kVkLaSlD4R_RhavDw") + user := &models.UserDetails{ + ID: "usr1", + } + + projectID := gidx.PrefixedID("metlprj-prj1") + project := &models.ProjectDetails{ + ID: "prj1", + } + + orgID := gidx.PrefixedID("metlorg-org1") + org := &models.OrganizationDetails{ + ID: "org1", + Projects: []*models.ProjectDetails{ + project, + }, + Memberships: []*models.Membership[models.OrganizationDetails]{ + { + User: user, + Roles: []string{ + "owner", + }, + }, + }, + } + + var ( + mMetal = new(providers.MockProvider) + mPerms = new(permissions.MockClient) + mPublisher = new(mockPublisher) + ) + + // Relationships + + mMetal.On("GetOrganizationDetails", orgID).Return(org, nil) + + mPerms.On("ListResourceRelationshipsFrom", orgID).Return([]permissions.ResourceRelationship{}, nil) + mPerms.On("ListResourceRelationshipsTo", orgID).Return([]permissions.ResourceRelationship{}, nil) + + orgTenantChangeMessage := events.ChangeMessage{ + SubjectID: orgID, + EventType: string(events.CreateChangeType), + AdditionalSubjectIDs: []gidx.PrefixedID{ + rootTenantID, + }, + } + relateProjectChangeMessage := events.ChangeMessage{ + SubjectID: projectID, + EventType: string(events.CreateChangeType), + AdditionalSubjectIDs: []gidx.PrefixedID{ + orgID, + }, + } + + mPublisher.On("PublishChange", "metalorganization", orgTenantChangeMessage).Return(nil) + mPublisher.On("PublishChange", "metalorganization", relateProjectChangeMessage).Return(nil) + + // Memberships + + newRoleID := gidx.PrefixedID("permrol-role1") + + mPerms.On("ListResourceRoles", orgID).Return(permissions.ResourceRoles{}, nil) + mPerms.On("CreateRole", orgID, roleMap["owner"]).Return(newRoleID, nil) + + mPerms.On("AssignRole", newRoleID, userID).Return(nil) + + // Run scenario + + svc, err := service.New(mPublisher, mMetal, mPerms, + service.WithRootTenant(rootTenantID.String()), + service.WithRoles(roleMap), + ) + + require.NoError(t, err) + + err = svc.TouchOrganization(context.Background(), orgID) + + require.NoError(t, err) + + require.True(t, mMetal.AssertExpectations(t), "unexpected calls to metal provider") + require.True(t, mPerms.AssertExpectations(t), "unexpected calls to permissions client") + require.True(t, mPublisher.AssertExpectations(t), "unexpected calls to events publisher") +} + +func TestTouchOrganizationCleanup(t *testing.T) { + oldRootTenantID := gidx.PrefixedID("tnntten-root1") + newRootTenantID := gidx.PrefixedID("tnntten-root2") + + roleMap := map[string][]string{ + "owner": { + "action1", + "action2", + }, + "collaborator": { + "action1", + }, + } + + oldUserID := gidx.PrefixedID("idntusr-2s-9kVNPJBaInlHpRs3lAMsvU_kVkLaSlD4R_RhavDw") + deadProjectID := gidx.PrefixedID("metlprj-prj1") + + userID := gidx.PrefixedID("idntusr-RnKvdujrwqm4o1dBDgfgaqeCpKFMaGeOtGnNbZky0Kg") + user := &models.UserDetails{ + ID: "usr2", + } + + projectID := gidx.PrefixedID("metlprj-prj2") + project := &models.ProjectDetails{ + ID: "prj2", + } + + orgID := gidx.PrefixedID("metlorg-org1") + org := &models.OrganizationDetails{ + ID: "org1", + Projects: []*models.ProjectDetails{ + project, + }, + Memberships: []*models.Membership[models.OrganizationDetails]{ + { + User: user, + Roles: []string{ + "owner", + }, + }, + }, + } + + var ( + mMetal = new(providers.MockProvider) + mPerms = new(permissions.MockClient) + mPublisher = new(mockPublisher) + ) + + // Relationships + + mMetal.On("GetOrganizationDetails", orgID).Return(org, nil) + + existingRelsFrom := []permissions.ResourceRelationship{ + { + Relation: string(service.RelateParent), + SubjectID: oldRootTenantID, + }, + } + + mPerms.On("ListResourceRelationshipsFrom", orgID).Return(existingRelsFrom, nil) + + existingRelsTo := []permissions.ResourceRelationship{ + { + ResourceID: deadProjectID, + Relation: string(service.RelateParent), + }, + } + + mPerms.On("ListResourceRelationshipsTo", orgID).Return(existingRelsTo, nil) + + mPerms.On("DeleteResourceRelationship", orgID, string(service.RelateParent), oldRootTenantID).Return(nil) + mPerms.On("DeleteResourceRelationship", deadProjectID, string(service.RelateParent), orgID).Return(nil) + + orgTenantChangeMessage := events.ChangeMessage{ + SubjectID: orgID, + EventType: string(events.CreateChangeType), + AdditionalSubjectIDs: []gidx.PrefixedID{ + newRootTenantID, + }, + } + relateProjectChangeMessage := events.ChangeMessage{ + SubjectID: projectID, + EventType: string(events.CreateChangeType), + AdditionalSubjectIDs: []gidx.PrefixedID{ + orgID, + }, + } + + mPublisher.On("PublishChange", "metalorganization", orgTenantChangeMessage).Return(nil) + mPublisher.On("PublishChange", "metalorganization", relateProjectChangeMessage).Return(nil) + + // Memberships + + oldRoleID := gidx.PrefixedID("permrol-role1") + newRoleID := gidx.PrefixedID("permrol-role2") + + existingRoles := permissions.ResourceRoles{ + { + ID: oldRoleID, + Actions: roleMap["collaborator"], + }, + } + + mPerms.On("ListResourceRoles", orgID).Return(existingRoles, nil) + + existingRoleAssignments := []gidx.PrefixedID{ + oldUserID, + } + + mPerms.On("ListRoleAssignments", oldRoleID).Return(existingRoleAssignments, nil) + + mPerms.On("CreateRole", orgID, roleMap["owner"]).Return(newRoleID, nil) + mPerms.On("DeleteRole", oldRoleID).Return(nil) + + mPerms.On("AssignRole", newRoleID, userID).Return(nil) + mPerms.On("UnassignRole", oldRoleID, oldUserID).Return(nil) + + // Run scenario + + svc, err := service.New(mPublisher, mMetal, mPerms, + service.WithRootTenant(newRootTenantID.String()), + service.WithRoles(roleMap), + ) + + require.NoError(t, err) + + err = svc.TouchOrganization(context.Background(), orgID) + + require.NoError(t, err) + + require.True(t, mMetal.AssertExpectations(t), "unexpected calls to metal provider") + require.True(t, mPerms.AssertExpectations(t), "unexpected calls to permissions client") + require.True(t, mPublisher.AssertExpectations(t), "unexpected calls to events publisher") +} + +func TestTouchOrganizationNoChange(t *testing.T) { + rootTenantID := gidx.PrefixedID("tnntten-root1") + + roleMap := map[string][]string{ + "owner": { + "action1", + "action2", + }, + } + + userID := gidx.PrefixedID("idntusr-2s-9kVNPJBaInlHpRs3lAMsvU_kVkLaSlD4R_RhavDw") + user := &models.UserDetails{ + ID: "usr1", + } + + projectID := gidx.PrefixedID("metlprj-prj1") + project := &models.ProjectDetails{ + ID: "prj1", + } + + orgID := gidx.PrefixedID("metlorg-org1") + org := &models.OrganizationDetails{ + ID: "org1", + Projects: []*models.ProjectDetails{ + project, + }, + Memberships: []*models.Membership[models.OrganizationDetails]{ + { + User: user, + Roles: []string{ + "owner", + }, + }, + }, + } + + var ( + mMetal = new(providers.MockProvider) + mPerms = new(permissions.MockClient) + mPublisher = new(mockPublisher) + ) + + // Relationships + + mMetal.On("GetOrganizationDetails", orgID).Return(org, nil) + + existingRelsFrom := []permissions.ResourceRelationship{ + { + Relation: string(service.RelateParent), + SubjectID: rootTenantID, + }, + } + + mPerms.On("ListResourceRelationshipsFrom", orgID).Return(existingRelsFrom, nil) + + existingRelsTo := []permissions.ResourceRelationship{ + { + ResourceID: projectID, + Relation: string(service.RelateParent), + }, + } + + mPerms.On("ListResourceRelationshipsTo", orgID).Return(existingRelsTo, nil) + + // Memberships + + roleID := gidx.PrefixedID("permrol-role1") + + existingRoles := permissions.ResourceRoles{ + { + ID: roleID, + Actions: roleMap["owner"], + }, + } + + mPerms.On("ListResourceRoles", orgID).Return(existingRoles, nil) + + existingRoleAssignments := []gidx.PrefixedID{ + userID, + } + + mPerms.On("ListRoleAssignments", roleID).Return(existingRoleAssignments, nil) + + // Run scenario + + svc, err := service.New(mPublisher, mMetal, mPerms, + service.WithRootTenant(rootTenantID.String()), + service.WithRoles(roleMap), + ) + + require.NoError(t, err) + + err = svc.TouchOrganization(context.Background(), orgID) + + require.NoError(t, err) + + require.True(t, mMetal.AssertExpectations(t), "unexpected calls to metal provider") + require.True(t, mPerms.AssertExpectations(t), "unexpected calls to permissions client") + require.True(t, mPublisher.AssertExpectations(t), "unexpected calls to events publisher") +} diff --git a/internal/service/projects_test.go b/internal/service/projects_test.go new file mode 100644 index 0000000..e3de5ec --- /dev/null +++ b/internal/service/projects_test.go @@ -0,0 +1,303 @@ +package service_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + "go.infratographer.com/x/events" + "go.infratographer.com/x/gidx" + + "go.equinixmetal.net/infra9-metal-bridge/internal/metal/models" + "go.equinixmetal.net/infra9-metal-bridge/internal/metal/providers" + "go.equinixmetal.net/infra9-metal-bridge/internal/permissions" + "go.equinixmetal.net/infra9-metal-bridge/internal/service" +) + +func TestTouchProjectEmpty(t *testing.T) { + rootTenantID := gidx.PrefixedID("tnntten-root1") + + roleMap := map[string][]string{ + "owner": { + "action1", + "action2", + }, + } + + userID := gidx.PrefixedID("idntusr-2s-9kVNPJBaInlHpRs3lAMsvU_kVkLaSlD4R_RhavDw") + user := &models.UserDetails{ + ID: "usr1", + } + + orgID := gidx.PrefixedID("metlorg-org1") + org := &models.OrganizationDetails{ + ID: "org1", + } + + projectID := gidx.PrefixedID("metlprj-prj1") + project := &models.ProjectDetails{ + ID: "prj1", + Organization: org, + Memberships: []*models.Membership[models.ProjectDetails]{ + { + User: user, + Roles: []string{ + "owner", + }, + }, + }, + } + + var ( + mMetal = new(providers.MockProvider) + mPerms = new(permissions.MockClient) + mPublisher = new(mockPublisher) + ) + + // Relationships + + mMetal.On("GetProjectDetails", projectID).Return(project, nil) + + mPerms.On("ListResourceRelationshipsFrom", projectID).Return([]permissions.ResourceRelationship{}, nil) + + relateParentChangeMessage := events.ChangeMessage{ + SubjectID: projectID, + EventType: string(events.CreateChangeType), + AdditionalSubjectIDs: []gidx.PrefixedID{ + orgID, + }, + } + + mPublisher.On("PublishChange", "metalproject", relateParentChangeMessage).Return(nil) + + // Memberships + + newRoleID := gidx.PrefixedID("permrol-role1") + + mPerms.On("ListResourceRoles", projectID).Return(permissions.ResourceRoles{}, nil) + mPerms.On("CreateRole", projectID, roleMap["owner"]).Return(newRoleID, nil) + + mPerms.On("AssignRole", newRoleID, userID).Return(nil) + + // Run scenario + + svc, err := service.New(mPublisher, mMetal, mPerms, + service.WithRootTenant(rootTenantID.String()), + service.WithRoles(roleMap), + ) + + require.NoError(t, err) + + err = svc.TouchProject(context.Background(), projectID) + + require.NoError(t, err) + + require.True(t, mMetal.AssertExpectations(t), "unexpected calls to metal provider") + require.True(t, mPerms.AssertExpectations(t), "unexpected calls to permissions client") + require.True(t, mPublisher.AssertExpectations(t), "unexpected calls to events publisher") +} + +func TestTouchProjectCleanup(t *testing.T) { + rootTenantID := gidx.PrefixedID("tnntten-root1") + + roleMap := map[string][]string{ + "owner": { + "action1", + "action2", + }, + "collaborator": { + "action1", + }, + } + + oldUserID := gidx.PrefixedID("idntusr-2s-9kVNPJBaInlHpRs3lAMsvU_kVkLaSlD4R_RhavDw") + oldOrgID := gidx.PrefixedID("metlorg-org1") + + userID := gidx.PrefixedID("idntusr-RnKvdujrwqm4o1dBDgfgaqeCpKFMaGeOtGnNbZky0Kg") + user := &models.UserDetails{ + ID: "usr2", + } + + orgID := gidx.PrefixedID("metlorg-org2") + org := &models.OrganizationDetails{ + ID: "org2", + } + + projectID := gidx.PrefixedID("metlprj-prj2") + project := &models.ProjectDetails{ + ID: "prj2", + Organization: org, + Memberships: []*models.Membership[models.ProjectDetails]{ + { + User: user, + Roles: []string{ + "owner", + }, + }, + }, + } + + var ( + mMetal = new(providers.MockProvider) + mPerms = new(permissions.MockClient) + mPublisher = new(mockPublisher) + ) + + // Relationships + + mMetal.On("GetProjectDetails", projectID).Return(project, nil) + + existingRelsFrom := []permissions.ResourceRelationship{ + { + Relation: string(service.RelateParent), + SubjectID: oldOrgID, + }, + } + + mPerms.On("ListResourceRelationshipsFrom", projectID).Return(existingRelsFrom, nil) + + mPerms.On("DeleteResourceRelationship", projectID, string(service.RelateParent), oldOrgID).Return(nil) + + relateParentChangeMessage := events.ChangeMessage{ + SubjectID: projectID, + EventType: string(events.CreateChangeType), + AdditionalSubjectIDs: []gidx.PrefixedID{ + orgID, + }, + } + + mPublisher.On("PublishChange", "metalproject", relateParentChangeMessage).Return(nil) + + // Memberships + + oldRoleID := gidx.PrefixedID("permrol-role1") + newRoleID := gidx.PrefixedID("permrol-role2") + + existingRoles := permissions.ResourceRoles{ + { + ID: oldRoleID, + Actions: roleMap["collaborator"], + }, + } + + mPerms.On("ListResourceRoles", projectID).Return(existingRoles, nil) + + existingRoleAssignments := []gidx.PrefixedID{ + oldUserID, + } + + mPerms.On("ListRoleAssignments", oldRoleID).Return(existingRoleAssignments, nil) + + mPerms.On("CreateRole", projectID, roleMap["owner"]).Return(newRoleID, nil) + mPerms.On("DeleteRole", oldRoleID).Return(nil) + + mPerms.On("AssignRole", newRoleID, userID).Return(nil) + mPerms.On("UnassignRole", oldRoleID, oldUserID).Return(nil) + + // Run scenario + + svc, err := service.New(mPublisher, mMetal, mPerms, + service.WithRootTenant(rootTenantID.String()), + service.WithRoles(roleMap), + ) + + require.NoError(t, err) + + err = svc.TouchProject(context.Background(), projectID) + + require.NoError(t, err) + + require.True(t, mMetal.AssertExpectations(t), "unexpected calls to metal provider") + require.True(t, mPerms.AssertExpectations(t), "unexpected calls to permissions client") + require.True(t, mPublisher.AssertExpectations(t), "unexpected calls to events publisher") +} + +func TestTouchProjectNoChange(t *testing.T) { + rootTenantID := gidx.PrefixedID("tnntten-root1") + + roleMap := map[string][]string{ + "owner": { + "action1", + "action2", + }, + } + + userID := gidx.PrefixedID("idntusr-2s-9kVNPJBaInlHpRs3lAMsvU_kVkLaSlD4R_RhavDw") + user := &models.UserDetails{ + ID: "usr1", + } + + orgID := gidx.PrefixedID("metlorg-org1") + org := &models.OrganizationDetails{ + ID: "org1", + } + + projectID := gidx.PrefixedID("metlprj-prj1") + project := &models.ProjectDetails{ + ID: "prj1", + Organization: org, + Memberships: []*models.Membership[models.ProjectDetails]{ + { + User: user, + Roles: []string{ + "owner", + }, + }, + }, + } + + var ( + mMetal = new(providers.MockProvider) + mPerms = new(permissions.MockClient) + mPublisher = new(mockPublisher) + ) + + // Relationships + + mMetal.On("GetProjectDetails", projectID).Return(project, nil) + + existingRelsFrom := []permissions.ResourceRelationship{ + { + Relation: string(service.RelateParent), + SubjectID: orgID, + }, + } + + mPerms.On("ListResourceRelationshipsFrom", projectID).Return(existingRelsFrom, nil) + + // Memberships + + roleID := gidx.PrefixedID("permrol-role1") + + existingRoles := permissions.ResourceRoles{ + { + ID: roleID, + Actions: roleMap["owner"], + }, + } + + mPerms.On("ListResourceRoles", projectID).Return(existingRoles, nil) + + existingRoleAssignments := []gidx.PrefixedID{ + userID, + } + + mPerms.On("ListRoleAssignments", roleID).Return(existingRoleAssignments, nil) + + // Run scenario + + svc, err := service.New(mPublisher, mMetal, mPerms, + service.WithRootTenant(rootTenantID.String()), + service.WithRoles(roleMap), + ) + + require.NoError(t, err) + + err = svc.TouchProject(context.Background(), projectID) + + require.NoError(t, err) + + require.True(t, mMetal.AssertExpectations(t), "unexpected calls to metal provider") + require.True(t, mPerms.AssertExpectations(t), "unexpected calls to permissions client") + require.True(t, mPublisher.AssertExpectations(t), "unexpected calls to events publisher") +} From 6c5073ba9ba9bbfc0f7e1b5cdadd9ae65c22d1fb Mon Sep 17 00:00:00 2001 From: Mike Mason Date: Wed, 19 Jul 2023 17:14:25 +0000 Subject: [PATCH 26/33] switch subscriptions to not use events package due to watermill wrapping --- internal/pubsub/nats.go | 131 ++++++++++++++++++++++++++++++++++ internal/pubsub/subscriber.go | 116 ++++++++++++++++++------------ 2 files changed, 201 insertions(+), 46 deletions(-) create mode 100644 internal/pubsub/nats.go diff --git a/internal/pubsub/nats.go b/internal/pubsub/nats.go new file mode 100644 index 0000000..3c642c7 --- /dev/null +++ b/internal/pubsub/nats.go @@ -0,0 +1,131 @@ +package pubsub + +import ( + "bytes" + "context" + "crypto/md5" + "encoding/hex" + "encoding/json" + "strings" + + "github.com/nats-io/nats.go" + "github.com/pkg/errors" + "go.infratographer.com/x/events" + "go.uber.org/zap" +) + +const subscriptionBufferSize = 10 + +type changeEvent struct { + *nats.Msg + events.ChangeMessage + + Error error +} + +type subscriber struct { + logger *zap.SugaredLogger + + nats *nats.Conn + jetstream nats.JetStreamContext + + topicPrefix string + queueGroup string + subscriptionOptions []nats.SubOpt + + subscriptions []*nats.Subscription + + context context.Context + cancelCtx func() +} + +func (s *subscriber) durableName(topic string) string { + hash := md5.Sum([]byte(topic)) + + return s.queueGroup + hex.EncodeToString(hash[:]) +} + +// SubscribeChanges subscribes to a topic, returning a channel which change events will be sent to. +func (s *subscriber) SubscribeChanges(ctx context.Context, topic string) (<-chan *changeEvent, error) { + subject := strings.Join([]string{s.topicPrefix, "changes", topic}, ".") + + opts := s.subscriptionOptions + + opts = append(opts, nats.Durable(s.durableName(subject))) + + msgCh := make(chan *changeEvent, subscriptionBufferSize) + + sub, err := s.jetstream.QueueSubscribe(subject, s.queueGroup, func(msg *nats.Msg) { + event := &changeEvent{ + Msg: msg, + } + + if err := json.NewDecoder(bytes.NewBuffer(msg.Data)).Decode(&event.ChangeMessage); err != nil { + event.Error = err + } + + msgCh <- event + }, opts...) + if err != nil { + return nil, err + } + + s.subscriptions = append(s.subscriptions, sub) + + go func(subscriber *nats.Subscription) { + select { + case <-ctx.Done(): + case <-s.context.Done(): + } + + if err := sub.Unsubscribe(); err != nil { + s.logger.Errorw("unable to unsubscribe", "error", err, "subject", subject) + } + }(sub) + + return msgCh, nil +} + +// Close closes the underlying nats connection. +func (s *subscriber) Close() { + s.cancelCtx() + + s.nats.Close() +} + +func newSubscriber(ctx context.Context, config events.SubscriberConfig, logger *zap.SugaredLogger, subOptions ...nats.SubOpt) (*subscriber, error) { + options := []nats.Option{ + nats.Timeout(config.Timeout), + } + + switch { + case config.NATSConfig.CredsFile != "": + options = append(options, nats.UserCredentials(config.NATSConfig.CredsFile)) + case config.NATSConfig.Token != "": + options = append(options, nats.Token(config.NATSConfig.Token)) + } + + conn, err := nats.Connect(config.URL, options...) + if err != nil { + return nil, errors.Wrap(err, "cannot connect to NATS") + } + + js, err := conn.JetStream() + if err != nil { + conn.Close() + + return nil, errors.Wrap(err, "cannot initialize JetStream") + } + + ctx, cancel := context.WithCancel(ctx) + + return &subscriber{ + logger: logger, + nats: conn, + jetstream: js, + topicPrefix: config.Prefix, + subscriptionOptions: subOptions, + context: ctx, + cancelCtx: cancel, + }, nil +} diff --git a/internal/pubsub/subscriber.go b/internal/pubsub/subscriber.go index eefb034..1095516 100644 --- a/internal/pubsub/subscriber.go +++ b/internal/pubsub/subscriber.go @@ -3,6 +3,7 @@ package pubsub import ( "context" "sync" + "time" nc "github.com/nats-io/nats.go" "go.infratographer.com/x/events" @@ -14,18 +15,18 @@ import ( "go.equinixmetal.net/infra9-metal-bridge/internal/metal/models" "go.equinixmetal.net/infra9-metal-bridge/internal/service" - - "github.com/ThreeDotsLabs/watermill/message" ) -var tracer = otel.Tracer("go.infratographer.com/permissions-api/internal/pubsub") +const defaultNakDelay = 10 * time.Second + +var tracer = otel.Tracer("go.equinixmetal.net/infra9-metal-bridge") // Subscriber is the subscriber client type Subscriber struct { ctx context.Context - changeChannels []<-chan *message.Message + changeChannels []<-chan *changeEvent logger *zap.SugaredLogger - subscriber *events.Subscriber + subscriber *subscriber subOpts []nc.SubOpt svc service.Service } @@ -59,7 +60,7 @@ func NewSubscriber(ctx context.Context, cfg events.SubscriberConfig, service ser opt(s) } - sub, err := events.NewSubscriber(cfg, s.subOpts...) + sub, err := newSubscriber(ctx, cfg, s.logger, s.subOpts...) if err != nil { return nil, err } @@ -102,49 +103,60 @@ func (s Subscriber) Listen() error { } // listen listens for messages on a channel and calls the registered message handler -func (s Subscriber) listen(messages <-chan *message.Message, wg *sync.WaitGroup) { +func (s Subscriber) listen(messages <-chan *changeEvent, wg *sync.WaitGroup) { defer wg.Done() for msg := range messages { - s.logger.Infow("processing event", "event.id", msg.UUID) + mlogger := s.logger.With( + "nats.subject", msg.Subject, + "event.subject.id", msg.SubjectID, + "event.type", msg.EventType, + ) + + mlogger.Infow("processing event") if err := s.processEvent(msg); err != nil { - s.logger.Warn("Failed to process msg: ", err) + mlogger.Errorw("Failed to process msg: ", "error", err) - msg.Nack() + if err = msg.NakWithDelay(defaultNakDelay); err != nil { + mlogger.Errorw("error naking failed message", "error", err) + } } else { - msg.Ack() + if err = msg.Ack(); err != nil { + mlogger.Warnw("error acking message", "error", err) + } } } } // Close closes the subscriber connection and unsubscribes from all subscriptions -func (s *Subscriber) Close() error { - return s.subscriber.Close() +func (s *Subscriber) Close() { + s.subscriber.Close() } // processEvent event message handler -func (s *Subscriber) processEvent(msg *message.Message) error { - changeMsg, err := events.UnmarshalChangeMessage(msg.Payload) - if err != nil { - s.logger.Errorw("failed to process data in msg", zap.Error(err)) +func (s *Subscriber) processEvent(msg *changeEvent) error { + mlogger := s.logger.With( + "nats.subject", msg.Subject, + "event.subject.id", msg.SubjectID, + "event.type", msg.EventType, + ) - return err - } - - ctx, span := tracer.Start(context.Background(), "pubsub.receive", trace.WithAttributes(attribute.String("pubsub.subject", changeMsg.SubjectID.String()))) + ctx, span := tracer.Start(context.Background(), "pubsub.receive", trace.WithAttributes(attribute.String("pubsub.subject", msg.SubjectID.String()))) defer span.End() - switch events.ChangeType(changeMsg.EventType) { + var err error + + switch events.ChangeType(msg.EventType) { case events.CreateChangeType: - err = s.handleTouchEvent(ctx, msg, changeMsg) + err = s.handleTouchEvent(ctx, msg) case events.UpdateChangeType: - err = s.handleTouchEvent(ctx, msg, changeMsg) + err = s.handleTouchEvent(ctx, msg) case events.DeleteChangeType: - err = s.handleDeleteEvent(ctx, msg, changeMsg) + err = s.handleDeleteEvent(ctx, msg) default: - s.logger.Warnw("ignoring msg, not a create, update or delete event", "event_type", changeMsg.EventType) + mlogger.Warn("ignoring msg, not a create, update or delete event") } if err != nil { @@ -154,9 +166,15 @@ func (s *Subscriber) processEvent(msg *message.Message) error { return nil } -func (s *Subscriber) handleTouchEvent(ctx context.Context, msg *message.Message, changeMsg events.ChangeMessage) error { - if s.svc.IsOrganizationID(changeMsg.SubjectID) { - if err := s.svc.TouchOrganization(ctx, changeMsg.SubjectID); err != nil { +func (s *Subscriber) handleTouchEvent(ctx context.Context, msg *changeEvent) error { + mlogger := s.logger.With( + "nats.subject", msg.Subject, + "event.subject.id", msg.SubjectID, + "event.type", msg.EventType, + ) + + if s.svc.IsOrganizationID(msg.SubjectID) { + if err := s.svc.TouchOrganization(ctx, msg.SubjectID); err != nil { // TODO: only return errors on retryable errors return err } @@ -164,8 +182,8 @@ func (s *Subscriber) handleTouchEvent(ctx context.Context, msg *message.Message, return nil } - if s.svc.IsProjectID(changeMsg.SubjectID) { - if err := s.svc.TouchProject(ctx, changeMsg.SubjectID); err != nil { + if s.svc.IsProjectID(msg.SubjectID) { + if err := s.svc.TouchProject(ctx, msg.SubjectID); err != nil { // TODO: only return errors on retryable errors return err } @@ -173,17 +191,17 @@ func (s *Subscriber) handleTouchEvent(ctx context.Context, msg *message.Message, return nil } - if s.svc.IsUser(changeMsg.SubjectID) { - userUUID := changeMsg.SubjectID.String()[gidx.PrefixPartLength+1:] + if s.svc.IsUser(msg.SubjectID) { + userUUID := msg.SubjectID.String()[gidx.PrefixPartLength+1:] subjID, err := models.GenerateSubjectID(models.IdentityPrefixUser, models.MetalUserIssuer, models.MetalUserIssuerIDPrefix+userUUID) if err != nil { - s.logger.Errorw("failed to convert user id to identity id", "user.id", changeMsg.SubjectID.String(), "error", err) + mlogger.Errorw("failed to convert user id to identity id", "error", err) return nil } - if err := s.svc.AssignUser(ctx, subjID, changeMsg.AdditionalSubjectIDs...); err != nil { + if err := s.svc.AssignUser(ctx, subjID, msg.AdditionalSubjectIDs...); err != nil { // TODO: only return errors on retryable errors return err } @@ -191,14 +209,20 @@ func (s *Subscriber) handleTouchEvent(ctx context.Context, msg *message.Message, return nil } - s.logger.Warnw("unknown subject id", "subject.id", changeMsg.SubjectID) + mlogger.Warnw("unknown subject id") return nil } -func (s *Subscriber) handleDeleteEvent(ctx context.Context, msg *message.Message, changeMsg events.ChangeMessage) error { - if s.svc.IsOrganizationID(changeMsg.SubjectID) { - if err := s.svc.DeleteOrganization(ctx, changeMsg.SubjectID); err != nil { +func (s *Subscriber) handleDeleteEvent(ctx context.Context, msg *changeEvent) error { + mlogger := s.logger.With( + "nats.subject", msg.Subject, + "event.subject.id", msg.SubjectID, + "event.type", msg.EventType, + ) + + if s.svc.IsOrganizationID(msg.SubjectID) { + if err := s.svc.DeleteOrganization(ctx, msg.SubjectID); err != nil { // TODO: only return errors on retryable errors return err } @@ -206,8 +230,8 @@ func (s *Subscriber) handleDeleteEvent(ctx context.Context, msg *message.Message return nil } - if s.svc.IsProjectID(changeMsg.SubjectID) { - if err := s.svc.DeleteProject(ctx, changeMsg.SubjectID); err != nil { + if s.svc.IsProjectID(msg.SubjectID) { + if err := s.svc.DeleteProject(ctx, msg.SubjectID); err != nil { // TODO: only return errors on retryable errors return err } @@ -215,17 +239,17 @@ func (s *Subscriber) handleDeleteEvent(ctx context.Context, msg *message.Message return nil } - if s.svc.IsUser(changeMsg.SubjectID) { - userUUID := changeMsg.SubjectID.String()[gidx.PrefixPartLength+1:] + if s.svc.IsUser(msg.SubjectID) { + userUUID := msg.SubjectID.String()[gidx.PrefixPartLength+1:] subjID, err := models.GenerateSubjectID(models.IdentityPrefixUser, models.MetalUserIssuer, models.MetalUserIssuerIDPrefix+userUUID) if err != nil { - s.logger.Errorw("failed to convert user id to identity id", "user.id", changeMsg.SubjectID.String(), "error", err) + mlogger.Errorw("failed to convert user id to identity id", "error", err) return nil } - if err := s.svc.UnassignUser(ctx, subjID, changeMsg.AdditionalSubjectIDs...); err != nil { + if err := s.svc.UnassignUser(ctx, subjID, msg.AdditionalSubjectIDs...); err != nil { // TODO: only return errors on retryable errors return err } @@ -233,7 +257,7 @@ func (s *Subscriber) handleDeleteEvent(ctx context.Context, msg *message.Message return nil } - s.logger.Warnw("unknown subject id", "subject.id", changeMsg.SubjectID) + mlogger.Warnw("unknown subject id") return nil } From 02c7895449e4b1f9241fc603b755d26f563729a6 Mon Sep 17 00:00:00 2001 From: Mike Mason Date: Wed, 19 Jul 2023 17:48:22 +0000 Subject: [PATCH 27/33] fix metal provider flag and env loading, and return error if provider not defined --- cmd/serve.go | 1 + internal/metal/config.go | 9 +++++++++ internal/metal/errors.go | 3 +++ internal/metal/metal.go | 4 ++++ internal/metal/providers/emapi/config.go | 16 ++++++++++++++++ internal/metal/providers/emgql/config.go | 12 ++++++++++++ 6 files changed, 45 insertions(+) diff --git a/cmd/serve.go b/cmd/serve.go index 717e50e..fc9b058 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -37,6 +37,7 @@ func init() { oauth2x.MustViperFlags(viper.GetViper(), serveCmd.Flags()) permissions.MustViperFlags(viper.GetViper(), serveCmd.Flags()) + metal.MustViperFlags(viper.GetViper(), serveCmd.Flags()) } func serve(cmd *cobra.Command, _ []string) { diff --git a/internal/metal/config.go b/internal/metal/config.go index 9f1d89d..1464abc 100644 --- a/internal/metal/config.go +++ b/internal/metal/config.go @@ -1,6 +1,9 @@ package metal import ( + "github.com/spf13/pflag" + "github.com/spf13/viper" + "go.equinixmetal.net/infra9-metal-bridge/internal/metal/providers/emapi" "go.equinixmetal.net/infra9-metal-bridge/internal/metal/providers/emgql" ) @@ -13,3 +16,9 @@ type Config struct { // EMAPI sets the provider to Equinix Metal API. EMAPI emapi.Config } + +// MustViperFlags registers command flags along with the viper bindings. +func MustViperFlags(v *viper.Viper, flags *pflag.FlagSet) { + emgql.MustViperFlags(v, flags) + emapi.MustViperFlags(v, flags) +} diff --git a/internal/metal/errors.go b/internal/metal/errors.go index 106ca78..f368680 100644 --- a/internal/metal/errors.go +++ b/internal/metal/errors.go @@ -5,4 +5,7 @@ import "errors" var ( // ErrUnauthorized is returned when the token provided did not validate to a user. ErrUnauthorized = errors.New("unauthorized key") + + // ErrMetalProviderRequired is returned when no provider has been configured for the metal client. + ErrMetalProviderRequired = errors.New("metal provider required") ) diff --git a/internal/metal/metal.go b/internal/metal/metal.go index 779958f..81786ae 100644 --- a/internal/metal/metal.go +++ b/internal/metal/metal.go @@ -61,5 +61,9 @@ func New(options ...Option) (Client, error) { client.logger = zap.NewNop() } + if client.provider == nil { + return nil, ErrMetalProviderRequired + } + return client, nil } diff --git a/internal/metal/providers/emapi/config.go b/internal/metal/providers/emapi/config.go index 6627a41..cfffe30 100644 --- a/internal/metal/providers/emapi/config.go +++ b/internal/metal/providers/emapi/config.go @@ -3,6 +3,10 @@ package emapi import ( "fmt" "net/url" + + "github.com/spf13/pflag" + "github.com/spf13/viper" + "go.infratographer.com/x/viperx" ) // Config provides configuration for connecting to the Equinix Metal API provider. @@ -17,6 +21,18 @@ type Config struct { ConsumerToken string } +// MustViperFlags registers command flags along with the viper bindings. +func MustViperFlags(v *viper.Viper, flags *pflag.FlagSet) { + flags.String("emapi-base-url", "", "Equinix Metal Rest API Base URL") + viperx.MustBindFlag(v, "equinixmetal.emapi.baseurl", flags.Lookup("emapi-base-url")) + + flags.String("emapi-auth-token", "", "Equinix Metal Rest Auth Token") + viperx.MustBindFlag(v, "equinixmetal.emapi.authtoken", flags.Lookup("emapi-auth-token")) + + flags.String("emapi-consumer-token", "", "Equinix Metal Rest Consumer Token") + viperx.MustBindFlag(v, "equinixmetal.emapi.consumertoken", flags.Lookup("emapi-consumer-token")) +} + // Populated checks if any field has been populated. func (c Config) Populated() bool { return c.AuthToken != "" || c.ConsumerToken != "" || c.BaseURL != "" diff --git a/internal/metal/providers/emgql/config.go b/internal/metal/providers/emgql/config.go index 7a23789..de207dc 100644 --- a/internal/metal/providers/emgql/config.go +++ b/internal/metal/providers/emgql/config.go @@ -1,11 +1,23 @@ package emgql +import ( + "github.com/spf13/pflag" + "github.com/spf13/viper" + "go.infratographer.com/x/viperx" +) + // Config provides configuration for connecting to the Equinix Metal API provider. type Config struct { // BaseURL is the baseurl to use when connecting to the Equinix Metal API Provider. BaseURL string } +// MustViperFlags registers command flags along with the viper bindings. +func MustViperFlags(v *viper.Viper, flags *pflag.FlagSet) { + flags.String("emgql-base-url", "", "Equinix Metal GraphQL Base URL") + viperx.MustBindFlag(v, "equinixmetal.emgql.baseurl", flags.Lookup("emgql-base-url")) +} + // Populated checks if any field has been populated. func (c Config) Populated() bool { return c.BaseURL != "" From be7ec3cd0ee36b672e7b22f9cf2cc7258524cce3 Mon Sep 17 00:00:00 2001 From: Mike Mason Date: Wed, 19 Jul 2023 18:10:02 +0000 Subject: [PATCH 28/33] whoopsied the queue group --- internal/pubsub/nats.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/pubsub/nats.go b/internal/pubsub/nats.go index 3c642c7..ed3e2ec 100644 --- a/internal/pubsub/nats.go +++ b/internal/pubsub/nats.go @@ -123,6 +123,7 @@ func newSubscriber(ctx context.Context, config events.SubscriberConfig, logger * logger: logger, nats: conn, jetstream: js, + queueGroup: config.QueueGroup, topicPrefix: config.Prefix, subscriptionOptions: subOptions, context: ctx, From 1a802f39245492de72b73f7408a2394b876a6ba4 Mon Sep 17 00:00:00 2001 From: Mike Mason Date: Wed, 19 Jul 2023 18:36:52 +0000 Subject: [PATCH 29/33] match policy resource names --- internal/service/organizations.go | 2 +- internal/service/organizations_test.go | 8 ++++---- internal/service/projects.go | 2 +- internal/service/projects_test.go | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/internal/service/organizations.go b/internal/service/organizations.go index d91136e..f83c605 100644 --- a/internal/service/organizations.go +++ b/internal/service/organizations.go @@ -9,7 +9,7 @@ import ( "go.equinixmetal.net/infra9-metal-bridge/internal/metal/models" ) -const organizationEvent = "metalorganization" +const organizationEvent = "metal_organization" // buildOrganizationRelationships compiles all relations into a relationships object to be processed by the processors. func (s *service) buildOrganizationRelationships(org *models.OrganizationDetails) (Relationships, error) { diff --git a/internal/service/organizations_test.go b/internal/service/organizations_test.go index ead7efa..4b1cd1b 100644 --- a/internal/service/organizations_test.go +++ b/internal/service/organizations_test.go @@ -89,8 +89,8 @@ func TestTouchOrganizationEmpty(t *testing.T) { }, } - mPublisher.On("PublishChange", "metalorganization", orgTenantChangeMessage).Return(nil) - mPublisher.On("PublishChange", "metalorganization", relateProjectChangeMessage).Return(nil) + mPublisher.On("PublishChange", "metal_organization", orgTenantChangeMessage).Return(nil) + mPublisher.On("PublishChange", "metal_organization", relateProjectChangeMessage).Return(nil) // Memberships @@ -208,8 +208,8 @@ func TestTouchOrganizationCleanup(t *testing.T) { }, } - mPublisher.On("PublishChange", "metalorganization", orgTenantChangeMessage).Return(nil) - mPublisher.On("PublishChange", "metalorganization", relateProjectChangeMessage).Return(nil) + mPublisher.On("PublishChange", "metal_organization", orgTenantChangeMessage).Return(nil) + mPublisher.On("PublishChange", "metal_organization", relateProjectChangeMessage).Return(nil) // Memberships diff --git a/internal/service/projects.go b/internal/service/projects.go index 2400423..d47c428 100644 --- a/internal/service/projects.go +++ b/internal/service/projects.go @@ -9,7 +9,7 @@ import ( "go.equinixmetal.net/infra9-metal-bridge/internal/metal/models" ) -const projectEvent = "metalproject" +const projectEvent = "metal_project" // buildProjectRelationships compiles all relations into a relationships object to be processed by the processors. func (s *service) buildProjectRelationships(project *models.ProjectDetails) (Relationships, error) { diff --git a/internal/service/projects_test.go b/internal/service/projects_test.go index e3de5ec..4b1c095 100644 --- a/internal/service/projects_test.go +++ b/internal/service/projects_test.go @@ -68,7 +68,7 @@ func TestTouchProjectEmpty(t *testing.T) { }, } - mPublisher.On("PublishChange", "metalproject", relateParentChangeMessage).Return(nil) + mPublisher.On("PublishChange", "metal_project", relateParentChangeMessage).Return(nil) // Memberships @@ -166,7 +166,7 @@ func TestTouchProjectCleanup(t *testing.T) { }, } - mPublisher.On("PublishChange", "metalproject", relateParentChangeMessage).Return(nil) + mPublisher.On("PublishChange", "metal_project", relateParentChangeMessage).Return(nil) // Memberships From f4beebd02f6268cdc712eb010a4028ecc7d1754e Mon Sep 17 00:00:00 2001 From: Mike Mason Date: Wed, 19 Jul 2023 19:12:14 +0000 Subject: [PATCH 30/33] manual and explicit acks --- internal/pubsub/nats.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/pubsub/nats.go b/internal/pubsub/nats.go index ed3e2ec..22e4cd0 100644 --- a/internal/pubsub/nats.go +++ b/internal/pubsub/nats.go @@ -51,7 +51,7 @@ func (s *subscriber) SubscribeChanges(ctx context.Context, topic string) (<-chan opts := s.subscriptionOptions - opts = append(opts, nats.Durable(s.durableName(subject))) + opts = append(opts, nats.Durable(s.durableName(subject)), nats.AckExplicit(), nats.ManualAck()) msgCh := make(chan *changeEvent, subscriptionBufferSize) From 1bd86d3ec9f553ac1c147469da2fa18788f410b3 Mon Sep 17 00:00:00 2001 From: Mike Mason Date: Wed, 19 Jul 2023 19:49:03 +0000 Subject: [PATCH 31/33] correct user id generation --- internal/metal/models/users.go | 2 +- internal/service/organizations_test.go | 8 ++++---- internal/service/projects_test.go | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/internal/metal/models/users.go b/internal/metal/models/users.go index 8c75d7c..c8e425c 100644 --- a/internal/metal/models/users.go +++ b/internal/metal/models/users.go @@ -12,7 +12,7 @@ const ( MetalUserIssuer = "https://auth.equinix.com/" // MetalUserIssuerIDPrefix is the issuer id prefix added by the issuer. - MetalUserIssuerIDPrefix = "auth|" + MetalUserIssuerIDPrefix = "auth0|" ) // UserDetails contains the user information. diff --git a/internal/service/organizations_test.go b/internal/service/organizations_test.go index 4b1cd1b..9448fbb 100644 --- a/internal/service/organizations_test.go +++ b/internal/service/organizations_test.go @@ -35,7 +35,7 @@ func TestTouchOrganizationEmpty(t *testing.T) { }, } - userID := gidx.PrefixedID("idntusr-2s-9kVNPJBaInlHpRs3lAMsvU_kVkLaSlD4R_RhavDw") + userID := gidx.PrefixedID("idntusr-2aPXqbgfYSlld36XLfvNjEqUIIL9ekRZzgs2eBGOCBw") user := &models.UserDetails{ ID: "usr1", } @@ -133,10 +133,10 @@ func TestTouchOrganizationCleanup(t *testing.T) { }, } - oldUserID := gidx.PrefixedID("idntusr-2s-9kVNPJBaInlHpRs3lAMsvU_kVkLaSlD4R_RhavDw") + oldUserID := gidx.PrefixedID("idntusr-2aPXqbgfYSlld36XLfvNjEqUIIL9ekRZzgs2eBGOCBw") deadProjectID := gidx.PrefixedID("metlprj-prj1") - userID := gidx.PrefixedID("idntusr-RnKvdujrwqm4o1dBDgfgaqeCpKFMaGeOtGnNbZky0Kg") + userID := gidx.PrefixedID("idntusr-Ewj_PJUue9eDIDoyCoWG48GtwlysqTj2Y4qWPiJPN1s") user := &models.UserDetails{ ID: "usr2", } @@ -265,7 +265,7 @@ func TestTouchOrganizationNoChange(t *testing.T) { }, } - userID := gidx.PrefixedID("idntusr-2s-9kVNPJBaInlHpRs3lAMsvU_kVkLaSlD4R_RhavDw") + userID := gidx.PrefixedID("idntusr-2aPXqbgfYSlld36XLfvNjEqUIIL9ekRZzgs2eBGOCBw") user := &models.UserDetails{ ID: "usr1", } diff --git a/internal/service/projects_test.go b/internal/service/projects_test.go index 4b1c095..f10a6bf 100644 --- a/internal/service/projects_test.go +++ b/internal/service/projects_test.go @@ -24,7 +24,7 @@ func TestTouchProjectEmpty(t *testing.T) { }, } - userID := gidx.PrefixedID("idntusr-2s-9kVNPJBaInlHpRs3lAMsvU_kVkLaSlD4R_RhavDw") + userID := gidx.PrefixedID("idntusr-2aPXqbgfYSlld36XLfvNjEqUIIL9ekRZzgs2eBGOCBw") user := &models.UserDetails{ ID: "usr1", } @@ -110,10 +110,10 @@ func TestTouchProjectCleanup(t *testing.T) { }, } - oldUserID := gidx.PrefixedID("idntusr-2s-9kVNPJBaInlHpRs3lAMsvU_kVkLaSlD4R_RhavDw") + oldUserID := gidx.PrefixedID("idntusr-2aPXqbgfYSlld36XLfvNjEqUIIL9ekRZzgs2eBGOCBw") oldOrgID := gidx.PrefixedID("metlorg-org1") - userID := gidx.PrefixedID("idntusr-RnKvdujrwqm4o1dBDgfgaqeCpKFMaGeOtGnNbZky0Kg") + userID := gidx.PrefixedID("idntusr-Ewj_PJUue9eDIDoyCoWG48GtwlysqTj2Y4qWPiJPN1s") user := &models.UserDetails{ ID: "usr2", } @@ -222,7 +222,7 @@ func TestTouchProjectNoChange(t *testing.T) { }, } - userID := gidx.PrefixedID("idntusr-2s-9kVNPJBaInlHpRs3lAMsvU_kVkLaSlD4R_RhavDw") + userID := gidx.PrefixedID("idntusr-2aPXqbgfYSlld36XLfvNjEqUIIL9ekRZzgs2eBGOCBw") user := &models.UserDetails{ ID: "usr1", } From b421163ae221f55857c83d47e5a20ffb885e1fab Mon Sep 17 00:00:00 2001 From: Mike Mason Date: Fri, 21 Jul 2023 18:44:48 +0000 Subject: [PATCH 32/33] define endpoints as constants --- internal/permissions/assignments.go | 6 +++--- internal/permissions/client.go | 12 ++++++++++-- internal/permissions/relationships.go | 6 +++--- internal/permissions/roles.go | 4 ++-- 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/internal/permissions/assignments.go b/internal/permissions/assignments.go index 9958e92..2c8eadf 100644 --- a/internal/permissions/assignments.go +++ b/internal/permissions/assignments.go @@ -27,7 +27,7 @@ type roleAssignmentData struct { // AssignRole assigns the provided member ID to the given role ID. func (c *client) AssignRole(ctx context.Context, roleID gidx.PrefixedID, memberID gidx.PrefixedID) error { - path := fmt.Sprintf("/api/v1/roles/%s/assignments", roleID.String()) + path := fmt.Sprintf(permsPathRoleAssignmentsFormat, roleID.String()) body, err := encodeJSON(RoleAssign{ SubjectID: memberID.String(), @@ -51,7 +51,7 @@ func (c *client) AssignRole(ctx context.Context, roleID gidx.PrefixedID, memberI // UnassignRole removes the provided member ID from the given role ID. func (c *client) UnassignRole(ctx context.Context, roleID gidx.PrefixedID, memberID gidx.PrefixedID) error { - path := fmt.Sprintf("/api/v1/roles/%s/assignments", roleID.String()) + path := fmt.Sprintf(permsPathRoleAssignmentsFormat, roleID.String()) body, err := encodeJSON(RoleAssign{ SubjectID: memberID.String(), @@ -75,7 +75,7 @@ func (c *client) UnassignRole(ctx context.Context, roleID gidx.PrefixedID, membe // ListRoleAssignments lists all assignments for the given role. func (c *client) ListRoleAssignments(ctx context.Context, roleID gidx.PrefixedID) ([]gidx.PrefixedID, error) { - path := fmt.Sprintf("/api/v1/roles/%s/assignments", roleID.String()) + path := fmt.Sprintf(permsPathRoleAssignmentsFormat, roleID.String()) var response roleAssignmentData diff --git a/internal/permissions/client.go b/internal/permissions/client.go index e449d1b..8aa75bb 100644 --- a/internal/permissions/client.go +++ b/internal/permissions/client.go @@ -17,9 +17,17 @@ import ( ) const ( - defaultPermissionsURL = "https://permissions-api.hollow-a.sv15.metalkube.net" - defaultHTTPClientTimeout = 5 * time.Second + + defaultPermissionsURL = "https://permissions-api.hollow-a.sv15.metalkube.net" + permsPathAllow = "/api/v1/allow" + + permsPathResourceRelationshipsFormat = "/api/v1/resources/%s/relationships" + permsPathResourceRelationshipsFrom = "/api/v1/relationships/from/" + permsPathResourceRelationshipsTo = "/api/v1/relationships/to/" + permsPathResourceRolesFormat = "/api/v1/resources/%s/roles" + + permsPathRoleAssignmentsFormat = "/api/v1/roles/%s/assignments" ) // DefaultHTTPClient is the default HTTP client for the Permissions Client. diff --git a/internal/permissions/relationships.go b/internal/permissions/relationships.go index fe7d744..fa59927 100644 --- a/internal/permissions/relationships.go +++ b/internal/permissions/relationships.go @@ -34,7 +34,7 @@ type ResourceRelationshipDeleteResponse struct { // DeleteResourceRelationship deletes the provided resources relationship to the given subject id. func (c *client) DeleteResourceRelationship(ctx context.Context, resourceID gidx.PrefixedID, relation string, relatedResourceID gidx.PrefixedID) error { - path := fmt.Sprintf("/api/v1/resources/%s/relationships", resourceID.String()) + path := fmt.Sprintf(permsPathResourceRelationshipsFormat, resourceID.String()) body, err := encodeJSON(ResourceRelationshipRequest{ Relation: relation, @@ -63,7 +63,7 @@ func (c *client) ListResourceRelationshipsFrom(ctx context.Context, resourceID g Data []resourceRelationship `json:"data"` } - if _, err := c.DoRequest(ctx, http.MethodGet, fmt.Sprintf("/api/v1/relationships/from/%s", resourceID.String()), nil, &response); err != nil { // nolint:bodyclose // closed by Do on json decode. + if _, err := c.DoRequest(ctx, http.MethodGet, permsPathResourceRelationshipsFrom+resourceID.String(), nil, &response); err != nil { // nolint:bodyclose // closed by Do on json decode. return nil, err } @@ -90,7 +90,7 @@ func (c *client) ListResourceRelationshipsTo(ctx context.Context, resourceID gid Data []resourceRelationship `json:"data"` } - if _, err := c.DoRequest(ctx, http.MethodGet, fmt.Sprintf("/api/v1/relationships/to/%s", resourceID.String()), nil, &response); err != nil { // nolint:bodyclose // closed by Do on json decode. + if _, err := c.DoRequest(ctx, http.MethodGet, permsPathResourceRelationshipsTo+resourceID.String(), nil, &response); err != nil { // nolint:bodyclose // closed by Do on json decode. return nil, err } diff --git a/internal/permissions/roles.go b/internal/permissions/roles.go index 08ad258..89a0ea2 100644 --- a/internal/permissions/roles.go +++ b/internal/permissions/roles.go @@ -35,7 +35,7 @@ type ResourceRole struct { // CreateRole creates a role on the given resource id with the provided actions. func (c *client) CreateRole(ctx context.Context, resourceID gidx.PrefixedID, actions []string) (gidx.PrefixedID, error) { - path := fmt.Sprintf("/api/v1/resources/%s/roles", resourceID.String()) + path := fmt.Sprintf(permsPathResourceRolesFormat, resourceID.String()) body, err := encodeJSON(ResourceRoleCreate{ Actions: actions, @@ -77,7 +77,7 @@ func (c *client) DeleteRole(ctx context.Context, roleID gidx.PrefixedID) error { // ListResourceRoles fetches all roles assigned to the provided resource. func (c *client) ListResourceRoles(ctx context.Context, resourceID gidx.PrefixedID) (ResourceRoles, error) { - path := fmt.Sprintf("/api/v1/resources/%s/roles", resourceID.String()) + path := fmt.Sprintf(permsPathResourceRolesFormat, resourceID.String()) var response struct { Data ResourceRoles `json:"data"` From a6aa3674cd6e74c77a11383cb8237e7a6113d42c Mon Sep 17 00:00:00 2001 From: Mike Mason Date: Fri, 21 Jul 2023 18:47:40 +0000 Subject: [PATCH 33/33] sort service role actions on init --- internal/service/options.go | 9 ++++++++- internal/service/process_memberships.go | 2 -- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/internal/service/options.go b/internal/service/options.go index fc7b8a3..8ac12a9 100644 --- a/internal/service/options.go +++ b/internal/service/options.go @@ -3,6 +3,7 @@ package service import ( "go.infratographer.com/x/gidx" "go.uber.org/zap" + "golang.org/x/exp/slices" "go.equinixmetal.net/infra9-metal-bridge/internal/metal" "go.equinixmetal.net/infra9-metal-bridge/internal/permissions" @@ -64,7 +65,13 @@ func WithRootTenant(sid string) Option { // WithRoles defines the role to action mapping. func WithRoles(roles map[string][]string) Option { return func(s *service) error { - s.roles = roles + s.roles = make(map[string][]string, len(roles)) + + for role, actions := range roles { + slices.Sort(actions) + + s.roles[role] = actions + } return nil } diff --git a/internal/service/process_memberships.go b/internal/service/process_memberships.go index cbe4ff4..611c4dc 100644 --- a/internal/service/process_memberships.go +++ b/internal/service/process_memberships.go @@ -180,8 +180,6 @@ func (s *service) mapResourceWants(memberships []ResourceMemberships) (map[strin roleActionsKey := make(map[string]string) for role, actions := range s.roles { - slices.Sort(actions) - roleActionsKey[role] = strings.Join(actions, "|") }