add remaining org/proj/user initial sync
This commit is contained in:
@@ -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)
|
||||
|
||||
27
internal/metal/providers/emapi/helpers.go
Normal file
27
internal/metal/providers/emapi/helpers.go
Normal 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 ""
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ func WithRootTenant(sid string) Option {
|
||||
return err
|
||||
}
|
||||
|
||||
s.rootResource = rootResource{id}
|
||||
s.rootResource = prefixedID{id}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user