add remaining org/proj/user initial sync

This commit is contained in:
Mike Mason
2023-07-11 21:32:30 +00:00
parent 80fb879ef6
commit 11fe8f8f2a
15 changed files with 215 additions and 34 deletions

View File

@@ -3,10 +3,11 @@ package metal
import ( import (
"context" "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.infratographer.com/x/gidx"
"go.uber.org/zap" "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. // 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) 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. // New creates a new Client.
func New(options ...Option) (*Client, error) { func New(options ...Option) (*Client, error) {
client := new(Client) client := new(Client)

View File

@@ -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 ""
}

View File

@@ -48,12 +48,18 @@ type Organization struct {
} }
func (o *Organization) ToDetails() *models.OrganizationDetails { 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 return nil
} }
details := &models.OrganizationDetails{ details := &models.OrganizationDetails{
ID: o.ID, ID: id,
Name: o.Name, Name: o.Name,
Projects: o.Projects.ToDetails(), 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) { func (c *Client) getOrganizationWithMemberships(ctx context.Context, id string) (*Organization, error) {
var org Organization 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 { if err != nil {
return nil, fmt.Errorf("error loading organization: %w", err) return nil, fmt.Errorf("error loading organization: %w", err)
} }

View File

@@ -48,12 +48,18 @@ type Project struct {
} }
func (p *Project) ToDetails() *models.ProjectDetails { 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 return nil
} }
details := &models.ProjectDetails{ details := &models.ProjectDetails{
ID: p.ID, ID: id,
Name: p.Name, Name: p.Name,
Organization: p.Organization.ToDetails(), Organization: p.Organization.ToDetails(),
} }
@@ -63,19 +69,19 @@ func (p *Project) ToDetails() *models.ProjectDetails {
return details 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 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 { if err != nil {
return nil, fmt.Errorf("error loading project: %w", err) return nil, fmt.Errorf("error loading organization: %w", err)
} }
return &project, nil return &project, nil
} }
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) {
project, err := c.getProject(ctx, id.String()[gidx.PrefixPartLength+1:]) project, err := c.getProjectWithMemberships(ctx, id.String()[gidx.PrefixPartLength+1:])
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -47,12 +47,18 @@ type User struct {
} }
func (u *User) ToDetails() *models.UserDetails { 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 nil
} }
return &models.UserDetails{ return &models.UserDetails{
ID: u.ID, ID: id,
FullName: u.FullName, FullName: u.FullName,
Organizations: nil, Organizations: nil,
Projects: u.Projects.ToDetails(), 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) { func (c *Client) getUser(ctx context.Context, id string) (*User, error) {
var user User 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 { if err != nil {
return nil, fmt.Errorf("error loading user: %w", err) return nil, fmt.Errorf("error loading organization: %w", err)
} }
return &user, nil return &user, nil
@@ -78,3 +84,11 @@ func (c *Client) GetUserDetails(ctx context.Context, id gidx.PrefixedID) (*model
return user.ToDetails(), nil 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
}

View File

@@ -3,10 +3,19 @@ package emgql
import ( import (
"context" "context"
"go.equinixmetal.net/infra9-metal-bridge/internal/metal/models"
"go.infratographer.com/x/gidx" "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) { func (c *Client) GetUserDetails(ctx context.Context, id gidx.PrefixedID) (*models.UserDetails, error) {
return nil, nil 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
}

View File

@@ -3,12 +3,15 @@ package provider
import ( import (
"context" "context"
"go.equinixmetal.net/infra9-metal-bridge/internal/metal/models"
"go.infratographer.com/x/gidx" "go.infratographer.com/x/gidx"
"go.equinixmetal.net/infra9-metal-bridge/internal/metal/models"
) )
type Provider interface { type Provider interface {
GetOrganizationDetails(ctx context.Context, id gidx.PrefixedID) (*models.OrganizationDetails, error) GetOrganizationDetails(ctx context.Context, id gidx.PrefixedID) (*models.OrganizationDetails, error)
GetProjectDetails(ctx context.Context, id gidx.PrefixedID) (*models.ProjectDetails, error) GetProjectDetails(ctx context.Context, id gidx.PrefixedID) (*models.ProjectDetails, error)
GetUserDetails(ctx context.Context, id gidx.PrefixedID) (*models.UserDetails, 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)
} }

View File

@@ -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) { 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("/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 { if _, err := c.DoRequest(ctx, http.MethodGet, path, nil, &response); err != nil {
return nil, err return nil, err
} }
return response, nil return response.Data, nil
} }
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) {

View File

@@ -173,7 +173,7 @@ func (s *Subscriber) handleTouchEvent(ctx context.Context, msg *message.Message,
} }
if s.svc.IsUser(changeMsg.SubjectID) { 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 // TODO: only return errors on retryable errors
return err return err
} }
@@ -206,7 +206,7 @@ func (s *Subscriber) handleDeleteEvent(ctx context.Context, msg *message.Message
} }
if s.svc.IsUser(changeMsg.SubjectID) { 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 // TODO: only return errors on retryable errors
return err return err
} }

View File

@@ -55,7 +55,7 @@ func WithRootTenant(sid string) Option {
return err return err
} }
s.rootResource = rootResource{id} s.rootResource = prefixedID{id}
return nil return nil
} }

View File

@@ -50,6 +50,7 @@ func (s *service) processMemberships(ctx context.Context, memberships []Resource
roleActions[role] = s.roles[role] roleActions[role] = s.roles[role]
} }
resourceRoleID[resourceID][role] = gidx.NullPrefixedID
resourceRoleMembers[resourceID][role][memberID] = true resourceRoleMembers[resourceID][role][memberID] = true
} }

View File

@@ -4,8 +4,41 @@ import (
"context" "context"
"go.infratographer.com/x/gidx" "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 { func (s *service) IsProjectID(id gidx.PrefixedID) bool {
if idType, ok := s.idPrefixMap[id.Prefix()]; ok { if idType, ok := s.idPrefixMap[id.Prefix()]; ok {
return idType == TypeProject 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 { 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 return nil
} }

View File

@@ -2,8 +2,6 @@ package service
import ( import (
"go.infratographer.com/x/gidx" "go.infratographer.com/x/gidx"
"go.equinixmetal.net/infra9-metal-bridge/internal/metal/models"
) )
const ( const (
@@ -60,5 +58,5 @@ type Relationship struct {
type ResourceMemberships struct { type ResourceMemberships struct {
Resource IDPrefixableResource Resource IDPrefixableResource
Role string Role string
Member *models.UserDetails Member IDPrefixableResource
} }

View File

@@ -47,10 +47,13 @@ type Service interface {
// IsUser checks if the provided id has an id prefix which is a user. // IsUser checks if the provided id has an id prefix which is a user.
IsUser(id gidx.PrefixedID) bool IsUser(id gidx.PrefixedID) bool
// TouchUser triggers a sync of a user and their permissions. // AssignUser assigns a user to the given resource.
TouchUser(ctx context.Context, id gidx.PrefixedID) error AssignUser(ctx context.Context, userID gidx.PrefixedID, resourceIDs ...gidx.PrefixedID) error
// DeleteUser deletes the user and their permissions. // RemoveUser removes the users from the given resource.
DeleteUser(ctx context.Context, id gidx.PrefixedID) error 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{} var _ Service = &service{}
@@ -62,15 +65,15 @@ type service struct {
perms *permissions.Client perms *permissions.Client
idPrefixMap map[string]string idPrefixMap map[string]string
rootResource rootResource rootResource prefixedID
roles map[string][]string roles map[string][]string
} }
type rootResource struct { type prefixedID struct {
id gidx.PrefixedID id gidx.PrefixedID
} }
func (r rootResource) PrefixedID() gidx.PrefixedID { func (r prefixedID) PrefixedID() gidx.PrefixedID {
return r.id return r.id
} }

View File

@@ -14,10 +14,59 @@ func (s *service) IsUser(id gidx.PrefixedID) bool {
return false 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 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 return nil
} }