initial commit
This commit is contained in:
9
internal/service/config.go
Normal file
9
internal/service/config.go
Normal 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
2
internal/service/doc.go
Normal file
@@ -0,0 +1,2 @@
|
||||
// Package service handles integrating Equinix Metal with Infratographer
|
||||
package service
|
||||
14
internal/service/errors.go
Normal file
14
internal/service/errors.go
Normal 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")
|
||||
)
|
||||
90
internal/service/options.go
Normal file
90
internal/service/options.go
Normal 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
|
||||
}
|
||||
}
|
||||
101
internal/service/organizations.go
Normal file
101
internal/service/organizations.go
Normal 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
137
internal/service/process.go
Normal 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
23
internal/service/projects.go
Normal file
23
internal/service/projects.go
Normal 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
|
||||
}
|
||||
64
internal/service/relationships.go
Normal file
64
internal/service/relationships.go
Normal 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
108
internal/service/service.go
Normal 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
23
internal/service/users.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user