From 4f3e5ef64df64948795c3be50ed92ee3ab853b8e Mon Sep 17 00:00:00 2001 From: max furman Date: Tue, 18 May 2021 16:50:54 -0700 Subject: [PATCH] wip --- api/api.go | 5 +- authority/admin/admin.go | 26 ++-- authority/admin/collection.go | 81 ++++++----- authority/authority.go | 4 +- authority/mgmt/admin.go | 42 +----- authority/mgmt/api/admin.go | 73 ++++++---- authority/mgmt/api/provisioner.go | 29 ++-- authority/mgmt/authConfig.go | 5 +- authority/mgmt/config.go | 10 -- authority/mgmt/db.go | 26 +--- authority/mgmt/db/nosql/admin.go | 68 ++-------- authority/mgmt/db/nosql/authConfig.go | 3 +- authority/mgmt/db/nosql/nosql.go | 2 - authority/mgmt/db/nosql/provisioner.go | 181 ++----------------------- authority/mgmt/provisioner.go | 5 +- authority/provisioner/collection.go | 2 +- authority/status/status.go | 11 ++ ca/mgmtClient.go | 103 ++++++++++---- 18 files changed, 276 insertions(+), 400 deletions(-) create mode 100644 authority/status/status.go diff --git a/api/api.go b/api/api.go index d6cf9ab7..20ef7e14 100644 --- a/api/api.go +++ b/api/api.go @@ -316,7 +316,7 @@ func certChainToPEM(certChain []*x509.Certificate) []Certificate { // Provisioners returns the list of provisioners configured in the authority. func (h *caHandler) Provisioners(w http.ResponseWriter, r *http.Request) { - cursor, limit, err := parseCursor(r) + cursor, limit, err := ParseCursor(r) if err != nil { WriteError(w, errs.BadRequestErr(err)) return @@ -427,7 +427,8 @@ func LogCertificate(w http.ResponseWriter, cert *x509.Certificate) { } } -func parseCursor(r *http.Request) (cursor string, limit int, err error) { +// ParseCursor parses the cursor and limit from the request query params. +func ParseCursor(r *http.Request) (cursor string, limit int, err error) { q := r.URL.Query() cursor = q.Get("cursor") if v := q.Get("limit"); len(v) > 0 { diff --git a/authority/admin/admin.go b/authority/admin/admin.go index 579538dc..f0058777 100644 --- a/authority/admin/admin.go +++ b/authority/admin/admin.go @@ -1,15 +1,25 @@ package admin -// Type specifies the type of administrator privileges the admin has. +import "github.com/smallstep/certificates/authority/status" + +// Type specifies the type of the admin. e.g. SUPER_ADMIN, REGULAR type Type string +var ( + // TypeSuper superadmin + TypeSuper = Type("SUPER_ADMIN") + // TypeRegular regular + TypeRegular = Type("REGULAR") +) + // Admin type. type Admin struct { - ID string `json:"id"` - AuthorityID string `json:"-"` - Subject string `json:"subject"` - ProvisionerName string `json:"provisionerName"` - ProvisionerType string `json:"provisionerType"` - ProvisionerID string `json:"provisionerID"` - Type Type `json:"type"` + ID string `json:"id"` + AuthorityID string `json:"-"` + Subject string `json:"subject"` + ProvisionerName string `json:"provisionerName"` + ProvisionerType string `json:"provisionerType"` + ProvisionerID string `json:"provisionerID"` + Type Type `json:"type"` + Status status.Type `json:"status"` } diff --git a/authority/admin/collection.go b/authority/admin/collection.go index 87dd63ce..9e8a2926 100644 --- a/authority/admin/collection.go +++ b/authority/admin/collection.go @@ -2,56 +2,54 @@ package admin import ( "crypto/sha1" + "encoding/binary" + "encoding/hex" + "fmt" + "sort" + "strings" "sync" "github.com/pkg/errors" - "go.step.sm/crypto/jose" + "github.com/smallstep/certificates/authority/provisioner" ) -// DefaultProvisionersLimit is the default limit for listing provisioners. -const DefaultProvisionersLimit = 20 +// DefaultAdminLimit is the default limit for listing provisioners. +const DefaultAdminLimit = 20 -// DefaultProvisionersMax is the maximum limit for listing provisioners. -const DefaultProvisionersMax = 100 +// DefaultAdminMax is the maximum limit for listing provisioners. +const DefaultAdminMax = 100 -/* -type uidProvisioner struct { - provisioner Interface - uid string +type uidAdmin struct { + admin *Admin + uid string } -type provisionerSlice []uidProvisioner - -func (p provisionerSlice) Len() int { return len(p) } -func (p provisionerSlice) Less(i, j int) bool { return p[i].uid < p[j].uid } -func (p provisionerSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] } -*/ +type adminSlice []uidAdmin -// loadByTokenPayload is a payload used to extract the id used to load the -// provisioner. -type loadByTokenPayload struct { - jose.Claims - AuthorizedParty string `json:"azp"` // OIDC client id - TenantID string `json:"tid"` // Microsoft Azure tenant id -} +func (p adminSlice) Len() int { return len(p) } +func (p adminSlice) Less(i, j int) bool { return p[i].uid < p[j].uid } +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 count int countByProvisioner map[string]int } // NewCollection initializes a collection of provisioners. The given list of // audiences are the audiences used by the JWT provisioner. -func NewCollection() *Collection { +func NewCollection(provisioners *provisioner.Collection) *Collection { return &Collection{ byID: new(sync.Map), byProv: new(sync.Map), bySubProv: new(sync.Map), countByProvisioner: map[string]int{}, + provisioners: provisioners, } } @@ -88,12 +86,18 @@ func (c *Collection) LoadByProvisioner(provName string) ([]*Admin, bool) { // 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 *Admin) error { - provName := adm.ProvisionerName + p, ok := c.provisioners.Load(adm.ProvisionerID) + if !ok { + return fmt.Errorf("provisioner %s not found", adm.ProvisionerID) + } + adm.ProvisionerName = p.GetName() + adm.ProvisionerType = p.GetType().String() // 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 := adm.ProvisionerName // Store admin alwasy in bySubProv. Subject <-> ProvisionerName must be unique. if _, loaded := c.bySubProv.LoadOrStore(subProvNameHash(adm.Subject, provName), adm); loaded { c.byID.Delete(adm.ID) @@ -109,6 +113,21 @@ func (c *Collection) Store(adm *Admin) error { } c.count++ + // Store sorted admins. + // Use the first 4 bytes (32bit) of the sum to insert the order + // Using big endian format to get the strings sorted: + // 0x00000000, 0x00000001, 0x00000002, ... + bi := make([]byte, 4) + _sum := sha1.Sum([]byte(adm.ID)) + sum := _sum[:] + binary.BigEndian.PutUint32(bi, uint32(c.sorted.Len())) + sum[0], sum[1], sum[2], sum[3] = bi[0], bi[1], bi[2], bi[3] + c.sorted = append(c.sorted, uidAdmin{ + admin: adm, + uid: hex.EncodeToString(sum), + }) + sort.Sort(c.sorted) + return nil } @@ -125,23 +144,22 @@ func (c *Collection) CountByProvisioner(provName string) int { return 0 } -/* // Find implements pagination on a list of sorted provisioners. -func (c *Collection) Find(cursor string, limit int) (List, string) { +func (c *Collection) Find(cursor string, limit int) ([]*Admin, string) { switch { case limit <= 0: - limit = DefaultProvisionersLimit - case limit > DefaultProvisionersMax: - limit = DefaultProvisionersMax + limit = DefaultAdminLimit + case limit > DefaultAdminMax: + limit = DefaultAdminMax } n := c.sorted.Len() cursor = fmt.Sprintf("%040s", cursor) i := sort.Search(n, func(i int) bool { return c.sorted[i].uid >= cursor }) - slice := List{} + slice := []*Admin{} for ; i < n && len(slice) < limit; i++ { - slice = append(slice, c.sorted[i].provisioner) + slice = append(slice, c.sorted[i].admin) } if i < n { @@ -149,7 +167,6 @@ func (c *Collection) Find(cursor string, limit int) (List, string) { } return slice, "" } -*/ func loadAdmin(m *sync.Map, key string) (*Admin, bool) { a, ok := m.Load(key) diff --git a/authority/authority.go b/authority/authority.go index 2da5b341..2772c444 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -175,7 +175,7 @@ func (a *Authority) ReloadAuthConfig() error { } } // Store all the admins - a.admins = admin.NewCollection() + a.admins = admin.NewCollection(a.provisioners) for _, adm := range a.config.AuthorityConfig.Admins { if err := a.admins.Store(adm); err != nil { return err @@ -431,7 +431,7 @@ func (a *Authority) init() error { } } // Store all the admins - a.admins = admin.NewCollection() + a.admins = admin.NewCollection(a.provisioners) for _, adm := range a.config.AuthorityConfig.Admins { if err := a.admins.Store(adm); err != nil { return err diff --git a/authority/mgmt/admin.go b/authority/mgmt/admin.go index f21abdce..d265a001 100644 --- a/authority/mgmt/admin.go +++ b/authority/mgmt/admin.go @@ -1,55 +1,23 @@ package mgmt import ( - "context" - "github.com/smallstep/certificates/authority/admin" ) // AdminType specifies the type of the admin. e.g. SUPER_ADMIN, REGULAR -type AdminType string +type AdminType admin.Type var ( // AdminTypeSuper superadmin - AdminTypeSuper = AdminType("SUPER_ADMIN") + AdminTypeSuper = admin.TypeSuper // AdminTypeRegular regular - AdminTypeRegular = AdminType("REGULAR") + AdminTypeRegular = admin.TypeRegular ) // Admin type. -type Admin struct { - ID string `json:"id"` - AuthorityID string `json:"-"` - ProvisionerID string `json:"provisionerID"` - Subject string `json:"subject"` - ProvisionerName string `json:"provisionerName"` - ProvisionerType string `json:"provisionerType"` - Type AdminType `json:"type"` - Status StatusType `json:"status"` -} - -// CreateAdmin builds and stores an admin type in the DB. -func CreateAdmin(ctx context.Context, db DB, provName, sub string, typ AdminType) (*Admin, error) { - adm := &Admin{ - Subject: sub, - ProvisionerName: provName, - Type: typ, - Status: StatusActive, - } - if err := db.CreateAdmin(ctx, adm); err != nil { - return nil, WrapErrorISE(err, "error creating admin") - } - return adm, nil -} +type Admin admin.Admin // ToCertificates converts an Admin to the Admin type expected by the authority. func (adm *Admin) ToCertificates() (*admin.Admin, error) { - return &admin.Admin{ - ID: adm.ID, - Subject: adm.Subject, - ProvisionerID: adm.ProvisionerID, - ProvisionerName: adm.ProvisionerName, - ProvisionerType: adm.ProvisionerType, - Type: admin.Type(adm.Type), - }, nil + return (*admin.Admin)(adm), nil } diff --git a/authority/mgmt/api/admin.go b/authority/mgmt/api/admin.go index f997095a..b474e131 100644 --- a/authority/mgmt/api/admin.go +++ b/authority/mgmt/api/admin.go @@ -8,27 +8,34 @@ import ( "github.com/smallstep/certificates/api" "github.com/smallstep/certificates/authority/admin" "github.com/smallstep/certificates/authority/mgmt" + "github.com/smallstep/certificates/authority/status" ) // CreateAdminRequest represents the body for a CreateAdmin request. type CreateAdminRequest struct { - Subject string `json:"subject"` - Provisioner string `json:"provisioner"` - Type mgmt.AdminType `json:"type"` + Subject string `json:"subject"` + Provisioner string `json:"provisioner"` + Type admin.Type `json:"type"` } // Validate validates a new-admin request body. func (car *CreateAdminRequest) Validate(c *admin.Collection) error { if _, ok := c.LoadBySubProv(car.Subject, car.Provisioner); ok { return mgmt.NewError(mgmt.ErrorBadRequestType, - "admin with subject %s and provisioner name %s already exists", car.Subject, car.Provisioner) + "admin with subject: '%s' and provisioner: '%s' already exists", car.Subject, car.Provisioner) } return nil } +// GetAdminsResponse for returning a list of admins. +type GetAdminsResponse struct { + Admins []*admin.Admin `json:"admins"` + NextCursor string `json:"nextCursor"` +} + // UpdateAdminRequest represents the body for a UpdateAdmin request. type UpdateAdminRequest struct { - Type mgmt.AdminType `json:"type"` + Type admin.Type `json:"type"` } // Validate validates a new-admin request body. @@ -43,27 +50,31 @@ type DeleteResponse struct { // GetAdmin returns the requested admin, or an error. func (h *Handler) GetAdmin(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() id := chi.URLParam(r, "id") - prov, err := h.db.GetAdmin(ctx, id) - if err != nil { - api.WriteError(w, err) + adm, ok := h.auth.GetAdminCollection().LoadByID(id) + if !ok { + api.WriteError(w, mgmt.NewError(mgmt.ErrorNotFoundType, + "admin %s not found", id)) return } - api.JSON(w, prov) + api.JSON(w, adm) } // GetAdmins returns all admins associated with the authority. func (h *Handler) GetAdmins(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - admins, err := h.db.GetAdmins(ctx) + cursor, limit, err := api.ParseCursor(r) if err != nil { - api.WriteError(w, err) + api.WriteError(w, mgmt.WrapError(mgmt.ErrorBadRequestType, err, + "error parsing cursor and limit from query params")) return } - api.JSON(w, admins) + + admins, nextCursor := h.auth.GetAdminCollection().Find(cursor, limit) + api.JSON(w, &GetAdminsResponse{ + Admins: admins, + NextCursor: nextCursor, + }) } // CreateAdmin creates a new admin. @@ -81,16 +92,24 @@ func (h *Handler) CreateAdmin(w http.ResponseWriter, r *http.Request) { return } + p, ok := h.auth.GetProvisionerCollection().LoadByName(body.Provisioner) + if !ok { + api.WriteError(w, mgmt.NewError(mgmt.ErrorNotFoundType, "provisioner %s not found", body.Provisioner)) + return + } + adm := &mgmt.Admin{ - ProvisionerName: body.Provisioner, - Subject: body.Subject, - Type: body.Type, - Status: mgmt.StatusActive, + ProvisionerID: p.GetID(), + Subject: body.Subject, + Type: body.Type, + Status: status.Active, } if err := h.db.CreateAdmin(ctx, adm); err != nil { api.WriteError(w, mgmt.WrapErrorISE(err, "error creating admin")) return } + adm.ProvisionerName = p.GetName() + adm.ProvisionerType = p.GetType().String() api.JSON(w, adm) if err := h.auth.ReloadAuthConfig(); err != nil { fmt.Printf("err = %+v\n", err) @@ -112,7 +131,7 @@ func (h *Handler) DeleteAdmin(w http.ResponseWriter, r *http.Request) { api.WriteError(w, mgmt.WrapErrorISE(err, "error retrieiving admin %s", id)) return } - adm.Status = mgmt.StatusDeleted + adm.Status = status.Deleted if err := h.db.UpdateAdmin(ctx, adm); err != nil { api.WriteError(w, mgmt.WrapErrorISE(err, "error updating admin %s", id)) return @@ -135,17 +154,19 @@ func (h *Handler) UpdateAdmin(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") - adm, err := h.db.GetAdmin(ctx, id) - if err != nil { - api.WriteError(w, mgmt.WrapErrorISE(err, "error retrieiving admin %s", id)) + adm, ok := h.auth.GetAdminCollection().LoadByID(id) + if !ok { + api.WriteError(w, mgmt.NewError(mgmt.ErrorNotFoundType, "admin %s not found", id)) + return + } + if adm.Type == body.Type { + api.WriteError(w, mgmt.NewError(mgmt.ErrorBadRequestType, "admin %s already has type %s", id, adm.Type)) return } - - // TODO validate adm.Type = body.Type - if err := h.db.UpdateAdmin(ctx, adm); err != nil { + if err := h.db.UpdateAdmin(ctx, (*mgmt.Admin)(adm)); err != nil { api.WriteError(w, mgmt.WrapErrorISE(err, "error updating admin %s", id)) return } diff --git a/authority/mgmt/api/provisioner.go b/authority/mgmt/api/provisioner.go index 8a8da08f..79ef5f74 100644 --- a/authority/mgmt/api/provisioner.go +++ b/authority/mgmt/api/provisioner.go @@ -8,6 +8,7 @@ import ( "github.com/smallstep/certificates/api" "github.com/smallstep/certificates/authority/mgmt" "github.com/smallstep/certificates/authority/provisioner" + "github.com/smallstep/certificates/authority/status" ) // CreateProvisionerRequest represents the body for a CreateProvisioner request. @@ -55,7 +56,13 @@ func (h *Handler) GetProvisioner(w http.ResponseWriter, r *http.Request) { ctx := r.Context() name := chi.URLParam(r, "name") - prov, err := h.db.GetProvisionerByName(ctx, name) + p, ok := h.auth.GetProvisionerCollection().LoadByName(name) + if !ok { + api.WriteError(w, mgmt.NewError(mgmt.ErrorNotFoundType, "provisioner %s not found", name)) + return + } + + prov, err := h.db.GetProvisioner(ctx, p.GetID()) if err != nil { api.WriteError(w, err) return @@ -123,6 +130,8 @@ func (h *Handler) DeleteProvisioner(w http.ResponseWriter, r *http.Request) { name := chi.URLParam(r, "name") c := h.auth.GetAdminCollection() + fmt.Printf("c.Count() = %+v\n", c.Count()) + fmt.Printf("c.CountByProvisioner() = %+v\n", c.CountByProvisioner(name)) if c.Count() == c.CountByProvisioner(name) { api.WriteError(w, mgmt.NewError(mgmt.ErrorBadRequestType, "cannot remove provisioner %s because no admins will remain", name)) @@ -130,14 +139,18 @@ func (h *Handler) DeleteProvisioner(w http.ResponseWriter, r *http.Request) { } ctx := r.Context() - prov, err := h.db.GetProvisionerByName(ctx, name) + p, ok := h.auth.GetProvisionerCollection().LoadByName(name) + if !ok { + api.WriteError(w, mgmt.NewError(mgmt.ErrorNotFoundType, "provisioner %s not found", name)) + return + } + prov, err := h.db.GetProvisioner(ctx, p.GetID()) if err != nil { - api.WriteError(w, mgmt.WrapErrorISE(err, "error retrieiving provisioner %s", name)) + api.WriteError(w, mgmt.WrapErrorISE(err, "error loading provisioner %s from db", name)) return } - fmt.Printf("prov = %+v\n", prov) - prov.Status = mgmt.StatusDeleted - if err := h.db.UpdateProvisioner(ctx, name, prov); err != nil { + prov.Status = status.Deleted + if err := h.db.UpdateProvisioner(ctx, prov); err != nil { api.WriteError(w, mgmt.WrapErrorISE(err, "error updating provisioner %s", name)) return } @@ -150,8 +163,8 @@ func (h *Handler) DeleteProvisioner(w http.ResponseWriter, r *http.Request) { ID: adm.ID, ProvisionerID: adm.ProvisionerID, Subject: adm.Subject, - Type: mgmt.AdminType(adm.Type), - Status: mgmt.StatusDeleted, + Type: adm.Type, + Status: status.Deleted, }); err != nil { api.WriteError(w, mgmt.WrapErrorISE(err, "error deleting admin %s, as part of provisioner %s deletion", adm.Subject, name)) return diff --git a/authority/mgmt/authConfig.go b/authority/mgmt/authConfig.go index 14448af4..6284a9e7 100644 --- a/authority/mgmt/authConfig.go +++ b/authority/mgmt/authConfig.go @@ -4,6 +4,7 @@ import ( "github.com/smallstep/certificates/authority/admin" "github.com/smallstep/certificates/authority/config" "github.com/smallstep/certificates/authority/provisioner" + "github.com/smallstep/certificates/authority/status" ) // AuthConfig represents the Authority Configuration. @@ -15,7 +16,7 @@ type AuthConfig struct { Admins []*Admin `json:"-"` Claims *Claims `json:"claims,omitempty"` Backdate string `json:"backdate,omitempty"` - Status StatusType `json:"status,omitempty"` + Status status.Type `json:"status,omitempty"` } func NewDefaultAuthConfig() *AuthConfig { @@ -23,7 +24,7 @@ func NewDefaultAuthConfig() *AuthConfig { Claims: NewDefaultClaims(), ASN1DN: &config.ASN1DN{}, Backdate: config.DefaultBackdate.String(), - Status: StatusActive, + Status: status.Active, } } diff --git a/authority/mgmt/config.go b/authority/mgmt/config.go index d569ad6f..8b23cad0 100644 --- a/authority/mgmt/config.go +++ b/authority/mgmt/config.go @@ -14,16 +14,6 @@ const ( DefaultAuthorityID = "00000000-0000-0000-0000-000000000000" ) -// StatusType is the type for status. -type StatusType string - -var ( - // StatusActive active - StatusActive = StatusType("active") - // StatusDeleted deleted - StatusDeleted = StatusType("deleted") -) - // Claims encapsulates all x509 and ssh claims applied to the authority // configuration. E.g. maxTLSCertDuration, defaultSSHCertDuration, etc. type Claims struct { diff --git a/authority/mgmt/db.go b/authority/mgmt/db.go index 8546c4b4..9cfab3e5 100644 --- a/authority/mgmt/db.go +++ b/authority/mgmt/db.go @@ -14,9 +14,8 @@ var ErrNotFound = errors.New("not found") type DB interface { CreateProvisioner(ctx context.Context, prov *Provisioner) error GetProvisioner(ctx context.Context, id string) (*Provisioner, error) - GetProvisionerByName(ctx context.Context, name string) (*Provisioner, error) GetProvisioners(ctx context.Context) ([]*Provisioner, error) - UpdateProvisioner(ctx context.Context, name string, prov *Provisioner) error + UpdateProvisioner(ctx context.Context, prov *Provisioner) error CreateAdmin(ctx context.Context, admin *Admin) error GetAdmin(ctx context.Context, id string) (*Admin, error) @@ -31,11 +30,10 @@ type DB interface { // MockDB is an implementation of the DB interface that should only be used as // a mock in tests. type MockDB struct { - MockCreateProvisioner func(ctx context.Context, prov *Provisioner) error - MockGetProvisioner func(ctx context.Context, id string) (*Provisioner, error) - MockGetProvisionerByName func(ctx context.Context, name string) (*Provisioner, error) - MockGetProvisioners func(ctx context.Context) ([]*Provisioner, error) - MockUpdateProvisioner func(ctx context.Context, name string, prov *Provisioner) error + MockCreateProvisioner func(ctx context.Context, prov *Provisioner) error + MockGetProvisioner func(ctx context.Context, id string) (*Provisioner, error) + MockGetProvisioners func(ctx context.Context) ([]*Provisioner, error) + MockUpdateProvisioner func(ctx context.Context, prov *Provisioner) error MockCreateAdmin func(ctx context.Context, adm *Admin) error MockGetAdmin func(ctx context.Context, id string) (*Admin, error) @@ -70,16 +68,6 @@ func (m *MockDB) GetProvisioner(ctx context.Context, id string) (*Provisioner, e return m.MockRet1.(*Provisioner), m.MockError } -// GetProvisionerByName mock. -func (m *MockDB) GetProvisionerByName(ctx context.Context, id string) (*Provisioner, error) { - if m.MockGetProvisionerByName != nil { - return m.MockGetProvisionerByName(ctx, id) - } else if m.MockError != nil { - return nil, m.MockError - } - return m.MockRet1.(*Provisioner), m.MockError -} - // GetProvisioners mock func (m *MockDB) GetProvisioners(ctx context.Context) ([]*Provisioner, error) { if m.MockGetProvisioners != nil { @@ -91,9 +79,9 @@ func (m *MockDB) GetProvisioners(ctx context.Context) ([]*Provisioner, error) { } // UpdateProvisioner mock -func (m *MockDB) UpdateProvisioner(ctx context.Context, name string, prov *Provisioner) error { +func (m *MockDB) UpdateProvisioner(ctx context.Context, prov *Provisioner) error { if m.MockUpdateProvisioner != nil { - return m.MockUpdateProvisioner(ctx, name, prov) + return m.MockUpdateProvisioner(ctx, prov) } return m.MockError } diff --git a/authority/mgmt/db/nosql/admin.go b/authority/mgmt/db/nosql/admin.go index 4e465489..9732b3f0 100644 --- a/authority/mgmt/db/nosql/admin.go +++ b/authority/mgmt/db/nosql/admin.go @@ -6,19 +6,21 @@ import ( "time" "github.com/pkg/errors" + "github.com/smallstep/certificates/authority/admin" "github.com/smallstep/certificates/authority/mgmt" + "github.com/smallstep/certificates/authority/status" "github.com/smallstep/nosql" ) // dbAdmin is the database representation of the Admin type. type dbAdmin struct { - ID string `json:"id"` - AuthorityID string `json:"authorityID"` - ProvisionerID string `json:"provisionerID"` - Subject string `json:"subject"` - Type mgmt.AdminType `json:"type"` - CreatedAt time.Time `json:"createdAt"` - DeletedAt time.Time `json:"deletedAt"` + ID string `json:"id"` + AuthorityID string `json:"authorityID"` + ProvisionerID string `json:"provisionerID"` + Subject string `json:"subject"` + Type admin.Type `json:"type"` + CreatedAt time.Time `json:"createdAt"` + DeletedAt time.Time `json:"deletedAt"` } func (dbp *dbAdmin) clone() *dbAdmin { @@ -71,10 +73,10 @@ func unmarshalAdmin(data []byte, id string) (*mgmt.Admin, error) { ProvisionerID: dba.ProvisionerID, Subject: dba.Subject, Type: dba.Type, - Status: mgmt.StatusActive, + Status: status.Active, } if !dba.DeletedAt.IsZero() { - adm.Status = mgmt.StatusDeleted + adm.Status = status.Deleted } return adm, nil } @@ -89,19 +91,13 @@ func (db *DB) GetAdmin(ctx context.Context, id string) (*mgmt.Admin, error) { if err != nil { return nil, err } - if adm.Status == mgmt.StatusDeleted { + if adm.Status == status.Deleted { return nil, mgmt.NewError(mgmt.ErrorDeletedType, "admin %s is deleted", adm.ID) } if adm.AuthorityID != db.authorityID { return nil, mgmt.NewError(mgmt.ErrorAuthorityMismatchType, "admin %s is not owned by authority %s", adm.ID, db.authorityID) } - prov, err := db.GetProvisioner(ctx, adm.ProvisionerID) - if err != nil { - return nil, err - } - adm.ProvisionerName = prov.Name - adm.ProvisionerType = prov.Type return adm, nil } @@ -114,34 +110,18 @@ func (db *DB) GetAdmins(ctx context.Context) ([]*mgmt.Admin, error) { if err != nil { return nil, errors.Wrap(err, "error loading admins") } - var ( - provCache = map[string]*mgmt.Provisioner{} - admins []*mgmt.Admin - ) + var admins = []*mgmt.Admin{} for _, entry := range dbEntries { adm, err := unmarshalAdmin(entry.Value, string(entry.Key)) if err != nil { return nil, err } - if adm.Status == mgmt.StatusDeleted { + if adm.Status == status.Deleted { continue } if adm.AuthorityID != db.authorityID { continue } - var ( - prov *mgmt.Provisioner - ok bool - ) - if prov, ok = provCache[adm.ProvisionerID]; !ok { - prov, err = db.GetProvisioner(ctx, adm.ProvisionerID) - if err != nil { - return nil, err - } - provCache[adm.ProvisionerID] = prov - } - adm.ProvisionerName = prov.Name - adm.ProvisionerType = prov.Type admins = append(admins, adm) } return admins, nil @@ -156,24 +136,6 @@ func (db *DB) CreateAdmin(ctx context.Context, adm *mgmt.Admin) error { } adm.AuthorityID = db.authorityID - // If provisionerID is set, then use it, otherwise load the provisioner - // to get the name. - if adm.ProvisionerID == "" { - prov, err := db.GetProvisionerByName(ctx, adm.ProvisionerName) - if err != nil { - return err - } - adm.ProvisionerID = prov.ID - adm.ProvisionerType = prov.Type - } else { - prov, err := db.GetProvisioner(ctx, adm.ProvisionerID) - if err != nil { - return err - } - adm.ProvisionerName = prov.Name - adm.ProvisionerType = prov.Type - } - dba := &dbAdmin{ ID: adm.ID, AuthorityID: db.authorityID, @@ -196,7 +158,7 @@ func (db *DB) UpdateAdmin(ctx context.Context, adm *mgmt.Admin) error { nu := old.clone() // If the admin was active but is now deleted ... - if old.DeletedAt.IsZero() && adm.Status == mgmt.StatusDeleted { + if old.DeletedAt.IsZero() && adm.Status == status.Deleted { nu.DeletedAt = clock.Now() } nu.Type = adm.Type diff --git a/authority/mgmt/db/nosql/authConfig.go b/authority/mgmt/db/nosql/authConfig.go index b9108fdc..222e729d 100644 --- a/authority/mgmt/db/nosql/authConfig.go +++ b/authority/mgmt/db/nosql/authConfig.go @@ -8,6 +8,7 @@ import ( "github.com/pkg/errors" "github.com/smallstep/certificates/authority/config" "github.com/smallstep/certificates/authority/mgmt" + "github.com/smallstep/certificates/authority/status" "github.com/smallstep/nosql" ) @@ -106,7 +107,7 @@ func (db *DB) UpdateAuthConfig(ctx context.Context, ac *mgmt.AuthConfig) error { nu := old.clone() // If the authority was active but is now deleted ... - if old.DeletedAt.IsZero() && ac.Status == mgmt.StatusDeleted { + if old.DeletedAt.IsZero() && ac.Status == status.Deleted { nu.DeletedAt = clock.Now() } nu.Claims = ac.Claims diff --git a/authority/mgmt/db/nosql/nosql.go b/authority/mgmt/db/nosql/nosql.go index d97b7700..47f79bde 100644 --- a/authority/mgmt/db/nosql/nosql.go +++ b/authority/mgmt/db/nosql/nosql.go @@ -3,7 +3,6 @@ package nosql import ( "context" "encoding/json" - "fmt" "time" "github.com/pkg/errors" @@ -60,7 +59,6 @@ func (db *DB) save(ctx context.Context, id string, nu interface{}, old interface return errors.Wrapf(err, "error marshaling acme type: %s, value: %v", typ, old) } } - fmt.Printf("oldB = %+v\n", oldB) _, swapped, err := db.db.CmpAndSwap(table, []byte(id), oldB, newB) switch { diff --git a/authority/mgmt/db/nosql/provisioner.go b/authority/mgmt/db/nosql/provisioner.go index d44c3be1..2c2d65b0 100644 --- a/authority/mgmt/db/nosql/provisioner.go +++ b/authority/mgmt/db/nosql/provisioner.go @@ -3,13 +3,12 @@ package nosql import ( "context" "encoding/json" - "fmt" "time" "github.com/pkg/errors" "github.com/smallstep/certificates/authority/mgmt" + "github.com/smallstep/certificates/authority/status" "github.com/smallstep/nosql" - "github.com/smallstep/nosql/database" ) // dbProvisioner is the database representation of a Provisioner type. @@ -39,20 +38,6 @@ func (dbp *dbProvisioner) clone() *dbProvisioner { return &u } -func (db *DB) getProvisionerIDByName(ctx context.Context, name string) (string, error) { - data, err := db.db.Get(authorityProvisionersNameIDIndexTable, []byte(name)) - if nosql.IsErrNotFound(err) { - return "", mgmt.NewError(mgmt.ErrorNotFoundType, "provisioner %s not found", name) - } else if err != nil { - return "", mgmt.WrapErrorISE(err, "error loading provisioner %s", name) - } - ni := new(provisionerNameID) - if err := json.Unmarshal(data, ni); err != nil { - return "", mgmt.WrapErrorISE(err, "error unmarshaling provisionerNameID for provisioner %s", name) - } - return ni.ID, nil -} - func (db *DB) getDBProvisionerBytes(ctx context.Context, id string) ([]byte, error) { data, err := db.db.Get(authorityProvisionersTable, []byte(id)) if nosql.IsErrNotFound(err) { @@ -82,25 +67,6 @@ func (db *DB) getDBProvisioner(ctx context.Context, id string) (*dbProvisioner, return dbp, nil } -func (db *DB) getDBProvisionerByName(ctx context.Context, name string) (*dbProvisioner, error) { - id, err := db.getProvisionerIDByName(ctx, name) - if err != nil { - return nil, err - } - dbp, err := db.getDBProvisioner(ctx, id) - if err != nil { - return nil, err - } - if !dbp.DeletedAt.IsZero() { - return nil, mgmt.NewError(mgmt.ErrorDeletedType, "provisioner %s is deleted", name) - } - if dbp.AuthorityID != db.authorityID { - return nil, mgmt.NewError(mgmt.ErrorAuthorityMismatchType, - "provisioner %s is not owned by authority %s", name, db.authorityID) - } - return dbp, nil -} - // GetProvisioner retrieves and unmarshals a provisioner from the database. func (db *DB) GetProvisioner(ctx context.Context, id string) (*mgmt.Provisioner, error) { data, err := db.getDBProvisionerBytes(ctx, id) @@ -112,7 +78,7 @@ func (db *DB) GetProvisioner(ctx context.Context, id string) (*mgmt.Provisioner, if err != nil { return nil, err } - if prov.Status == mgmt.StatusDeleted { + if prov.Status == status.Deleted { return nil, mgmt.NewError(mgmt.ErrorDeletedType, "provisioner %s is deleted", prov.ID) } if prov.AuthorityID != db.authorityID { @@ -122,15 +88,6 @@ func (db *DB) GetProvisioner(ctx context.Context, id string) (*mgmt.Provisioner, return prov, nil } -// GetProvisionerByName retrieves a provisioner from the database by name. -func (db *DB) GetProvisionerByName(ctx context.Context, name string) (*mgmt.Provisioner, error) { - p, err := db.getProvisionerIDByName(ctx, name) - if err != nil { - return nil, err - } - return db.GetProvisioner(ctx, id) -} - func unmarshalDBProvisioner(data []byte, name string) (*dbProvisioner, error) { var dbp = new(dbProvisioner) if err := json.Unmarshal(data, dbp); err != nil { @@ -157,14 +114,14 @@ func unmarshalProvisioner(data []byte, name string) (*mgmt.Provisioner, error) { Name: dbp.Name, Claims: dbp.Claims, Details: details, - Status: mgmt.StatusActive, + Status: status.Active, X509Template: dbp.X509Template, X509TemplateData: dbp.X509TemplateData, SSHTemplate: dbp.SSHTemplate, SSHTemplateData: dbp.SSHTemplateData, } if !dbp.DeletedAt.IsZero() { - prov.Status = mgmt.StatusDeleted + prov.Status = status.Deleted } return prov, nil } @@ -182,7 +139,7 @@ func (db *DB) GetProvisioners(ctx context.Context) ([]*mgmt.Provisioner, error) if err != nil { return nil, err } - if prov.Status == mgmt.StatusDeleted { + if prov.Status == status.Deleted { continue } if prov.AuthorityID != db.authorityID { @@ -219,37 +176,8 @@ func (db *DB) CreateProvisioner(ctx context.Context, prov *mgmt.Provisioner) err SSHTemplateData: prov.SSHTemplateData, CreatedAt: clock.Now(), } - dbpBytes, err := json.Marshal(dbp) - if err != nil { - return mgmt.WrapErrorISE(err, "error marshaling dbProvisioner %s", prov.Name) - } - pni := &provisionerNameID{ - Name: prov.Name, - ID: prov.ID, - } - pniBytes, err := json.Marshal(pni) - if err != nil { - return mgmt.WrapErrorISE(err, "error marshaling provisionerNameIndex %s", prov.Name) - } - if err := db.db.Update(&database.Tx{ - Operations: []*database.TxEntry{ - { - Bucket: authorityProvisionersTable, - Key: []byte(dbp.ID), - Cmd: database.CmpAndSwap, - Value: dbpBytes, - CmpValue: nil, - }, - { - Bucket: authorityProvisionersNameIDIndexTable, - Key: []byte(dbp.Name), - Cmd: database.CmpAndSwap, - Value: pniBytes, - CmpValue: nil, - }, - }, - }); err != nil { + if err := db.save(ctx, prov.ID, dbp, nil, "provisioner", authorityProvisionersTable); err != nil { return mgmt.WrapErrorISE(err, "error creating provisioner %s", prov.Name) } @@ -257,22 +185,11 @@ func (db *DB) CreateProvisioner(ctx context.Context, prov *mgmt.Provisioner) err } // UpdateProvisioner saves an updated provisioner to the database. -func (db *DB) UpdateProvisioner(ctx context.Context, name string, prov *mgmt.Provisioner) error { - id, err := db.getProvisionerIDByName(ctx, name) - if err != nil { - return err - } - prov.ID = id - oldBytes, err := db.getDBProvisionerBytes(ctx, id) - if err != nil { - return err - } - fmt.Printf("oldBytes = %+v\n", oldBytes) - old, err := unmarshalDBProvisioner(oldBytes, id) +func (db *DB) UpdateProvisioner(ctx context.Context, prov *mgmt.Provisioner) error { + old, err := db.getDBProvisioner(ctx, prov.ID) if err != nil { return err } - fmt.Printf("old = %+v\n", old) nu := old.clone() @@ -281,91 +198,15 @@ func (db *DB) UpdateProvisioner(ctx context.Context, name string, prov *mgmt.Pro nu.Claims = prov.Claims nu.Details, err = json.Marshal(prov.Details) if err != nil { - return mgmt.WrapErrorISE(err, "error marshaling details when updating provisioner %s", name) + return mgmt.WrapErrorISE(err, "error marshaling details when updating provisioner %s", prov.Name) } nu.X509Template = prov.X509Template nu.X509TemplateData = prov.X509TemplateData nu.SSHTemplateData = prov.SSHTemplateData - var txs = []*database.TxEntry{} - // If the provisioner was active but is now deleted ... - if old.DeletedAt.IsZero() && prov.Status == mgmt.StatusDeleted { - nu.DeletedAt = clock.Now() - txs = append(txs, &database.TxEntry{ - Bucket: authorityProvisionersNameIDIndexTable, - Key: []byte(name), - Cmd: database.Delete, - }) - } - - if prov.Name != name { - // If the new name does not match the old name then: - // 1) check that the new name is not already taken - // 2) delete the old name-id index resource - // 3) create a new name-id index resource - // 4) update the provisioner resource - nuBytes, err := json.Marshal(nu) - if err != nil { - return mgmt.WrapErrorISE(err, "error marshaling dbProvisioner %s", prov.Name) - } - pni := &provisionerNameID{ - Name: prov.Name, - ID: prov.ID, - } - pniBytes, err := json.Marshal(pni) - if err != nil { - return mgmt.WrapErrorISE(err, "error marshaling provisionerNameID for provisioner %s", prov.Name) - } - - _, err = db.db.Get(authorityProvisionersNameIDIndexTable, []byte(name)) - if err == nil { - return mgmt.NewError(mgmt.ErrorBadRequestType, "provisioner with name %s already exists", prov.Name) - } else if !nosql.IsErrNotFound(err) { - return mgmt.WrapErrorISE(err, "error loading provisionerNameID %s", prov.Name) - } - err = db.db.Update(&database.Tx{ - Operations: []*database.TxEntry{ - { - Bucket: authorityProvisionersNameIDIndexTable, - Key: []byte(name), - Cmd: database.Delete, - }, - { - Bucket: authorityProvisionersNameIDIndexTable, - Key: []byte(prov.Name), - Cmd: database.CmpAndSwap, - Value: pniBytes, - CmpValue: nil, - }, - { - Bucket: authorityProvisionersTable, - Key: []byte(nu.ID), - Cmd: database.CmpAndSwap, - Value: nuBytes, - CmpValue: oldBytes, - }, - }, - }) - } else { - err = db.db.Update(&database.Tx{ - Operations: []*database.TxEntry{ - { - Bucket: authorityProvisionersNameIDIndexTable, - Key: []byte(name), - Cmd: database.Delete, - }, - { - Bucket: authorityProvisionersTable, - Key: []byte(nu.ID), - Cmd: database.CmpAndSwap, - Value: nuBytes, - CmpValue: oldBytes, - }, - }, - }) - } - if err != nil { + if err := db.save(ctx, prov.ID, nu, old, "provisioner", authorityProvisionersTable); err != nil { return mgmt.WrapErrorISE(err, "error updating provisioner %s", prov.Name) } + return nil } diff --git a/authority/mgmt/provisioner.go b/authority/mgmt/provisioner.go index fb39f6bc..10f472de 100644 --- a/authority/mgmt/provisioner.go +++ b/authority/mgmt/provisioner.go @@ -6,6 +6,7 @@ import ( "fmt" "github.com/smallstep/certificates/authority/provisioner" + "github.com/smallstep/certificates/authority/status" "go.step.sm/crypto/jose" ) @@ -69,7 +70,7 @@ type Provisioner struct { X509TemplateData []byte `json:"x509TemplateData"` SSHTemplate string `json:"sshTemplate"` SSHTemplateData []byte `json:"sshTemplateData"` - Status StatusType `json:"status"` + Status status.Type `json:"status"` } func (p *Provisioner) GetOptions() *provisioner.Options { @@ -101,7 +102,7 @@ func CreateProvisioner(ctx context.Context, db DB, typ, name string, opts ...Pro X509TemplateData: pc.X509TemplateData, SSHTemplate: pc.SSHTemplate, SSHTemplateData: pc.SSHTemplateData, - Status: StatusActive, + Status: status.Active, } if err := db.CreateProvisioner(ctx, p); err != nil { diff --git a/authority/provisioner/collection.go b/authority/provisioner/collection.go index b6ff8b9e..23a4a2a0 100644 --- a/authority/provisioner/collection.go +++ b/authority/provisioner/collection.go @@ -183,7 +183,7 @@ func (c *Collection) Store(p Interface) error { // Store provisioner always by name. if _, loaded := c.byName.LoadOrStore(p.GetName(), p); loaded { c.byID.Delete(p.GetID()) - return errors.New("cannot add multiple provisioners with the same id") + return errors.New("cannot add multiple provisioners with the same name") } // Store provisioner in byKey if EncryptedKey is defined. diff --git a/authority/status/status.go b/authority/status/status.go new file mode 100644 index 00000000..49e4c0bb --- /dev/null +++ b/authority/status/status.go @@ -0,0 +1,11 @@ +package status + +// Type is the type for status. +type Type string + +var ( + // Active active + Active = Type("active") + // Deleted deleted + Deleted = Type("deleted") +) diff --git a/ca/mgmtClient.go b/ca/mgmtClient.go index 96c33f03..7a50e369 100644 --- a/ca/mgmtClient.go +++ b/ca/mgmtClient.go @@ -7,8 +7,10 @@ import ( "net/http" "net/url" "path" + "strconv" "github.com/pkg/errors" + "github.com/smallstep/certificates/authority/admin" "github.com/smallstep/certificates/authority/mgmt" mgmtAPI "github.com/smallstep/certificates/authority/mgmt/api" "github.com/smallstep/certificates/errs" @@ -88,6 +90,80 @@ retry: return adm, nil } +// AdminOption is the type of options passed to the Provisioner method. +type AdminOption func(o *adminOptions) error + +type adminOptions struct { + cursor string + limit int +} + +func (o *adminOptions) apply(opts []AdminOption) (err error) { + for _, fn := range opts { + if err = fn(o); err != nil { + return + } + } + return +} + +func (o *adminOptions) rawQuery() string { + v := url.Values{} + if len(o.cursor) > 0 { + v.Set("cursor", o.cursor) + } + if o.limit > 0 { + v.Set("limit", strconv.Itoa(o.limit)) + } + return v.Encode() +} + +// WithAdminCursor will request the admins starting with the given cursor. +func WithAdminCursor(cursor string) AdminOption { + return func(o *adminOptions) error { + o.cursor = cursor + return nil + } +} + +// WithAdminLimit will request the given number of admins. +func WithAdminLimit(limit int) AdminOption { + return func(o *adminOptions) error { + o.limit = limit + return nil + } +} + +// GetAdmins performs the GET /mgmt/admins request to the CA. +func (c *MgmtClient) GetAdmins(opts ...AdminOption) (*mgmtAPI.GetAdminsResponse, error) { + var retried bool + o := new(adminOptions) + if err := o.apply(opts); err != nil { + return nil, err + } + u := c.endpoint.ResolveReference(&url.URL{ + Path: "/mgmt/admins", + RawQuery: o.rawQuery(), + }) +retry: + resp, err := c.client.Get(u.String()) + if err != nil { + return nil, errors.Wrapf(err, "client GET %s failed", u) + } + if resp.StatusCode >= 400 { + if !retried && c.retryOnError(resp) { + retried = true + goto retry + } + return nil, readMgmtError(resp.Body) + } + var body = new(mgmtAPI.GetAdminsResponse) + if err := readJSON(resp.Body, body); err != nil { + return nil, errors.Wrapf(err, "error reading %s", u) + } + return body, nil +} + // CreateAdmin performs the POST /mgmt/admin request to the CA. func (c *MgmtClient) CreateAdmin(req *mgmtAPI.CreateAdminRequest) (*mgmt.Admin, error) { var retried bool @@ -139,7 +215,7 @@ retry: } // UpdateAdmin performs the PUT /mgmt/admin/{id} request to the CA. -func (c *MgmtClient) UpdateAdmin(id string, uar *mgmtAPI.UpdateAdminRequest) (*mgmt.Admin, error) { +func (c *MgmtClient) UpdateAdmin(id string, uar *mgmtAPI.UpdateAdminRequest) (*admin.Admin, error) { var retried bool body, err := json.Marshal(uar) if err != nil { @@ -162,36 +238,13 @@ retry: } return nil, readMgmtError(resp.Body) } - var adm = new(mgmt.Admin) + var adm = new(admin.Admin) if err := readJSON(resp.Body, adm); err != nil { return nil, errors.Wrapf(err, "error reading %s", u) } return adm, nil } -// GetAdmins performs the GET /mgmt/admins request to the CA. -func (c *MgmtClient) GetAdmins() ([]*mgmt.Admin, error) { - var retried bool - u := c.endpoint.ResolveReference(&url.URL{Path: "/mgmt/admins"}) -retry: - resp, err := c.client.Get(u.String()) - if err != nil { - return nil, errors.Wrapf(err, "client GET %s failed", u) - } - if resp.StatusCode >= 400 { - if !retried && c.retryOnError(resp) { - retried = true - goto retry - } - return nil, readMgmtError(resp.Body) - } - var admins = new([]*mgmt.Admin) - if err := readJSON(resp.Body, admins); err != nil { - return nil, errors.Wrapf(err, "error reading %s", u) - } - return *admins, nil -} - // GetProvisioner performs the GET /mgmt/provisioner/{id} request to the CA. func (c *MgmtClient) GetProvisioner(id string) (*mgmt.Provisioner, error) { var retried bool