package administrator import ( "sort" "sync" "github.com/pkg/errors" "github.com/smallstep/certificates/authority/admin" "github.com/smallstep/certificates/authority/provisioner" "go.step.sm/linkedca" ) // DefaultAdminLimit is the default limit for listing provisioners. const DefaultAdminLimit = 20 // DefaultAdminMax is the maximum limit for listing provisioners. const DefaultAdminMax = 100 type adminSlice []*linkedca.Admin func (p adminSlice) Len() int { return len(p) } func (p adminSlice) Less(i, j int) bool { return p[i].Id < p[j].Id } func (p adminSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] } // Collection is a memory map of admins. type Collection struct { byID *sync.Map bySubProv *sync.Map byProv *sync.Map sorted adminSlice provisioners *provisioner.Collection superCount int superCountByProvisioner map[string]int } // NewCollection initializes a collection of provisioners. The given list of // audiences are the audiences used by the JWT provisioner. func NewCollection(provisioners *provisioner.Collection) *Collection { return &Collection{ byID: new(sync.Map), byProv: new(sync.Map), bySubProv: new(sync.Map), superCountByProvisioner: map[string]int{}, provisioners: provisioners, } } // LoadByID a admin by the ID. func (c *Collection) LoadByID(id string) (*linkedca.Admin, bool) { return loadAdmin(c.byID, id) } type subProv struct { subject string provisioner string } func newSubProv(subject, provisioner string) subProv { return subProv{subject, provisioner} } // LoadBySubProv a admin by the subject and provisioner name. func (c *Collection) LoadBySubProv(sub, provName string) (*linkedca.Admin, bool) { return loadAdmin(c.bySubProv, newSubProv(sub, provName)) } // LoadByProvisioner a admin by the subject and provisioner name. func (c *Collection) LoadByProvisioner(provName string) ([]*linkedca.Admin, bool) { val, ok := c.byProv.Load(provName) if !ok { return nil, false } admins, ok := val.([]*linkedca.Admin) if !ok { return nil, false } return admins, true } // Store adds an admin to the collection and enforces the uniqueness of // admin IDs and amdin subject <-> provisioner name combos. func (c *Collection) Store(adm *linkedca.Admin, prov provisioner.Interface) error { // Input validation. if adm.ProvisionerId != prov.GetID() { return admin.NewErrorISE("admin.provisionerId does not match provisioner argument") } // Store admin always in byID. ID must be unique. if _, loaded := c.byID.LoadOrStore(adm.Id, adm); loaded { return errors.New("cannot add multiple admins with the same id") } provName := prov.GetName() // Store admin always in bySubProv. Subject <-> ProvisionerName must be unique. if _, loaded := c.bySubProv.LoadOrStore(newSubProv(adm.Subject, provName), adm); loaded { c.byID.Delete(adm.Id) return errors.New("cannot add multiple admins with the same subject and provisioner") } var isSuper = (adm.Type == linkedca.Admin_SUPER_ADMIN) if admins, ok := c.LoadByProvisioner(provName); ok { c.byProv.Store(provName, append(admins, adm)) if isSuper { c.superCountByProvisioner[provName]++ } } else { c.byProv.Store(provName, []*linkedca.Admin{adm}) if isSuper { c.superCountByProvisioner[provName] = 1 } } if isSuper { c.superCount++ } c.sorted = append(c.sorted, adm) sort.Sort(c.sorted) return nil } // Remove deletes an admin from all associated collections and lists. func (c *Collection) Remove(id string) error { adm, ok := c.LoadByID(id) if !ok { return admin.NewError(admin.ErrorNotFoundType, "admin %s not found", id) } if adm.Type == linkedca.Admin_SUPER_ADMIN && c.SuperCount() == 1 { return admin.NewError(admin.ErrorBadRequestType, "cannot remove the last super admin") } prov, ok := c.provisioners.Load(adm.ProvisionerId) if !ok { return admin.NewError(admin.ErrorNotFoundType, "provisioner %s for admin %s not found", adm.ProvisionerId, id) } provName := prov.GetName() adminsByProv, ok := c.LoadByProvisioner(provName) if !ok { return admin.NewError(admin.ErrorNotFoundType, "admins not found for provisioner %s", provName) } // Find index in sorted list. sortedIndex := sort.Search(c.sorted.Len(), func(i int) bool { return c.sorted[i].Id >= adm.Id }) if c.sorted[sortedIndex].Id != adm.Id { return admin.NewError(admin.ErrorNotFoundType, "admin %s not found in sorted list", adm.Id) } var found bool for i, a := range adminsByProv { if a.Id == adm.Id { // Remove admin from list. https://stackoverflow.com/questions/37334119/how-to-delete-an-element-from-a-slice-in-golang // Order does not matter. adminsByProv[i] = adminsByProv[len(adminsByProv)-1] c.byProv.Store(provName, adminsByProv[:len(adminsByProv)-1]) found = true } } if !found { return admin.NewError(admin.ErrorNotFoundType, "admin %s not found in adminsByProvisioner list", adm.Id) } // Remove index in sorted list copy(c.sorted[sortedIndex:], c.sorted[sortedIndex+1:]) // Shift a[i+1:] left one index. c.sorted[len(c.sorted)-1] = nil // Erase last element (write zero value). c.sorted = c.sorted[:len(c.sorted)-1] // Truncate slice. c.byID.Delete(adm.Id) c.bySubProv.Delete(newSubProv(adm.Subject, provName)) if adm.Type == linkedca.Admin_SUPER_ADMIN { c.superCount-- c.superCountByProvisioner[provName]-- } return nil } // Update updates the given admin in all related lists and collections. func (c *Collection) Update(id string, nu *linkedca.Admin) (*linkedca.Admin, error) { adm, ok := c.LoadByID(id) if !ok { return nil, admin.NewError(admin.ErrorNotFoundType, "admin %s not found", adm.Id) } if adm.Type == nu.Type { return nil, admin.NewError(admin.ErrorBadRequestType, "admin %s already has type %s", id, adm.Type) } if adm.Type == linkedca.Admin_SUPER_ADMIN && c.SuperCount() == 1 { return nil, admin.NewError(admin.ErrorBadRequestType, "cannot change role of last super admin") } adm.Type = nu.Type return adm, nil } // SuperCount returns the total number of admins. func (c *Collection) SuperCount() int { return c.superCount } // SuperCountByProvisioner returns the total number of admins. func (c *Collection) SuperCountByProvisioner(provName string) int { if cnt, ok := c.superCountByProvisioner[provName]; ok { return cnt } return 0 } // Find implements pagination on a list of sorted admins. func (c *Collection) Find(cursor string, limit int) ([]*linkedca.Admin, string) { switch { case limit <= 0: limit = DefaultAdminLimit case limit > DefaultAdminMax: limit = DefaultAdminMax } n := c.sorted.Len() i := sort.Search(n, func(i int) bool { return c.sorted[i].Id >= cursor }) slice := []*linkedca.Admin{} for ; i < n && len(slice) < limit; i++ { slice = append(slice, c.sorted[i]) } if i < n { return slice, c.sorted[i].Id } return slice, "" } func loadAdmin(m *sync.Map, key interface{}) (*linkedca.Admin, bool) { val, ok := m.Load(key) if !ok { return nil, false } adm, ok := val.(*linkedca.Admin) if !ok { return nil, false } return adm, true }