initial commit

This commit is contained in:
Mike Mason
2023-07-01 00:04:52 +00:00
commit 80fb879ef6
65 changed files with 3544 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
package service
// Config defines the service configuration.
type Config struct {
RootTenant string `mapstructure:"root_tenant"`
}
// ConfigRoles is a list of roles to a list of actions
type ConfigRoles map[string][]string

2
internal/service/doc.go Normal file
View File

@@ -0,0 +1,2 @@
// Package service handles integrating Equinix Metal with Infratographer
package service

View File

@@ -0,0 +1,14 @@
package service
import "errors"
var (
// ErrRootTenantRequired is returned when no root tenant has been defined for the service.
ErrRootTenantRequired = errors.New("root tenant required")
// ErrPublisherRequired is defined when a publisher is not provided to the service.
ErrPublisherRequired = errors.New("publisher required")
// ErrRoleUnrecognized is returned when no corresponding role was matched.
ErrRoleUnrecognized = errors.New("unrecognized role")
)

View File

@@ -0,0 +1,90 @@
package service
import (
"go.infratographer.com/x/gidx"
"go.uber.org/zap"
"go.equinixmetal.net/infra9-metal-bridge/internal/metal"
"go.equinixmetal.net/infra9-metal-bridge/internal/permissions"
)
// Option defines a service option.
type Option func(s *service) error
// WithLogger sets the logger for the service handler.
func WithLogger(logger *zap.SugaredLogger) Option {
return func(s *service) error {
s.logger = logger
return nil
}
}
// WithMetalClient sets the Equinix Metal client used by the service.
func WithMetalClient(client *metal.Client) Option {
return func(s *service) error {
s.metal = client
return nil
}
}
// WithPermissionsClient sets the permissions client used by the service.
func WithPermissionsClient(client *permissions.Client) Option {
return func(s *service) error {
s.perms = client
return nil
}
}
// WithPrefixMap sets the id prefix map relating id prefixes to type names.
func WithPrefixMap(idMap map[string]string) Option {
return func(s *service) error {
s.idPrefixMap = idMap
return nil
}
}
// WithRootTenant sets the root tenant referenced in organization relationships.
func WithRootTenant(sid string) Option {
return func(s *service) error {
id, err := gidx.Parse(sid)
if err != nil {
return err
}
s.rootResource = rootResource{id}
return nil
}
}
// WithRoles defines the role to action mapping.
func WithRoles(roles map[string][]string) Option {
return func(s *service) error {
s.roles = roles
return nil
}
}
// WithConfig applies all configurations defined in the config.
func WithConfig(config Config) Option {
return func(s *service) error {
var options []Option
if config.RootTenant != "" {
options = append(options, WithRootTenant(config.RootTenant))
}
for _, opt := range options {
if err := opt(s); err != nil {
return err
}
}
return nil
}
}

View File

@@ -0,0 +1,101 @@
package service
import (
"context"
"go.infratographer.com/x/gidx"
"go.equinixmetal.net/infra9-metal-bridge/internal/metal/models"
)
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,
},
},
}
for _, member := range org.Memberships {
for _, role := range member.Roles {
if _, ok := s.roles[role]; !ok {
s.logger.Warnf("unrecognized organization role '%s' for %s on %s", role, member.User.PrefixedID(), org.PrefixedID())
continue
}
relations.Memberships = append(relations.Memberships, ResourceMemberships{
Resource: org,
Role: role,
Member: member.User,
})
}
}
for _, project := range org.Projects {
relations.Relationships = append(relations.Relationships, Relationship{
Resource: project,
Relation: RelateParent,
RelatedResource: org,
})
for _, member := range project.Memberships {
for _, role := range member.Roles {
if _, ok := s.roles[role]; !ok {
s.logger.Warnf("unrecognized project role '%s' for %s on %s", role, member.User.PrefixedID(), project.PrefixedID())
continue
}
relations.Memberships = append(relations.Memberships, ResourceMemberships{
Resource: project,
Role: role,
Member: member.User,
})
}
}
}
return relations, nil
}
func (s *service) IsOrganizationID(id gidx.PrefixedID) bool {
if idType, ok := s.idPrefixMap[id.Prefix()]; ok {
return idType == TypeOrganization
}
return false
}
func (s *service) TouchOrganization(ctx context.Context, id gidx.PrefixedID) error {
logger := s.logger.With("organization.id", id.String())
org, err := s.metal.GetOrganizationDetails(ctx, id)
if err != nil {
logger.Errorw("failed to get organization", "error", err)
return err
}
relationships, err := s.buildOrganizationRelationships(org)
if err != nil {
logger.Errorw("failed to build organization relationships", "error", err)
return err
}
s.processRelationships(ctx, "metal-relation", relationships.Relationships)
s.processMemberships(ctx, relationships.Memberships)
s.logger.Infow("organization sync complete", "relationships", len(relationships.Relationships), "memberships", len(relationships.Memberships))
return nil
}
func (s *service) DeleteOrganization(ctx context.Context, id gidx.PrefixedID) error {
return nil
}

137
internal/service/process.go Normal file
View File

@@ -0,0 +1,137 @@
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]
}
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")
}
}
}
}

View File

@@ -0,0 +1,23 @@
package service
import (
"context"
"go.infratographer.com/x/gidx"
)
func (s *service) IsProjectID(id gidx.PrefixedID) bool {
if idType, ok := s.idPrefixMap[id.Prefix()]; ok {
return idType == TypeProject
}
return false
}
func (s *service) TouchProject(ctx context.Context, id gidx.PrefixedID) error {
return nil
}
func (s *service) DeleteProject(ctx context.Context, id gidx.PrefixedID) error {
return nil
}

View File

@@ -0,0 +1,64 @@
package service
import (
"go.infratographer.com/x/gidx"
"go.equinixmetal.net/infra9-metal-bridge/internal/metal/models"
)
const (
RelateOwner RelationshipType = "owner"
RelateParent RelationshipType = "parent"
)
type RelationshipType string
type IDPrefixableResource interface {
PrefixedID() gidx.PrefixedID
}
type Relationships struct {
Relationships []Relationship
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 ResourceMemberships struct {
Resource IDPrefixableResource
Role string
Member *models.UserDetails
}

108
internal/service/service.go Normal file
View File

@@ -0,0 +1,108 @@
package service
import (
"context"
"go.infratographer.com/x/events"
"go.infratographer.com/x/gidx"
"go.uber.org/zap"
"go.equinixmetal.net/infra9-metal-bridge/internal/metal"
"go.equinixmetal.net/infra9-metal-bridge/internal/permissions"
)
const (
// TypeOrganization defines the organization type.
TypeOrganization = "organization"
// TypeProject defines the project type.
TypeProject = "project"
// TypeUser defines the user type.
TypeUser = "user"
)
// DefaultPrefixMap is the default id prefix to type relationship.
var DefaultPrefixMap = map[string]string{
"metlorg": TypeOrganization,
"metlprj": TypeProject,
"metlusr": TypeUser,
}
// Service defines a bridge service methods
type Service interface {
// IsOrganizationID checks if the provided id has an id prefix which is an organization.
IsOrganizationID(id gidx.PrefixedID) bool
// TouchOrganization triggers a sync of an organization.
TouchOrganization(ctx context.Context, id gidx.PrefixedID) error
// DeleteOrganization deletes an organization and all of its resources.
DeleteOrganization(ctx context.Context, id gidx.PrefixedID) error
// IsProjectID checks if the provided id has an id prefix which is a project.
IsProjectID(id gidx.PrefixedID) bool
// TouchProject triggers a sync of an organization
TouchProject(ctx context.Context, id gidx.PrefixedID) error
// DeleteProject deletes the project and all of its resources.
DeleteProject(ctx context.Context, id gidx.PrefixedID) error
// 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
}
var _ Service = &service{}
type service struct {
logger *zap.SugaredLogger
publisher *events.Publisher
metal *metal.Client
perms *permissions.Client
idPrefixMap map[string]string
rootResource rootResource
roles map[string][]string
}
type rootResource struct {
id gidx.PrefixedID
}
func (r rootResource) PrefixedID() gidx.PrefixedID {
return r.id
}
func New(publisher *events.Publisher, metal *metal.Client, perms *permissions.Client, options ...Option) (Service, error) {
svc := &service{
publisher: publisher,
metal: metal,
perms: perms,
idPrefixMap: make(map[string]string),
}
for _, opt := range options {
if err := opt(svc); err != nil {
return nil, err
}
}
if svc.logger == nil {
svc.logger = zap.NewNop().Sugar()
}
if svc.rootResource.PrefixedID() == gidx.NullPrefixedID {
return nil, ErrRootTenantRequired
}
if svc.idPrefixMap == nil || len(svc.idPrefixMap) == 0 {
svc.idPrefixMap = DefaultPrefixMap
}
if svc.roles == nil {
svc.roles = make(map[string][]string)
}
return svc, nil
}

23
internal/service/users.go Normal file
View File

@@ -0,0 +1,23 @@
package service
import (
"context"
"go.infratographer.com/x/gidx"
)
func (s *service) IsUser(id gidx.PrefixedID) bool {
if idType, ok := s.idPrefixMap[id.Prefix()]; ok {
return idType == TypeUser
}
return false
}
func (s *service) TouchUser(ctx context.Context, id gidx.PrefixedID) error {
return nil
}
func (s *service) DeleteUser(ctx context.Context, id gidx.PrefixedID) error {
return nil
}