From 98a6e545301bdfe88bc23786982a2d254f4b0b1c Mon Sep 17 00:00:00 2001 From: max furman Date: Tue, 11 May 2021 15:25:37 -0700 Subject: [PATCH] wip --- authority/mgmt/admin.go | 26 +++--- authority/mgmt/api/provisioner.go | 40 +++++---- authority/mgmt/config.go | 14 ++- authority/mgmt/db/nosql/admin.go | 8 +- authority/mgmt/db/nosql/provisioner.go | 115 +++++++++++++++++++++---- authority/mgmt/provisioner.go | 94 +++++--------------- ca/mgmtClient.go | 23 +++++ 7 files changed, 194 insertions(+), 126 deletions(-) diff --git a/authority/mgmt/admin.go b/authority/mgmt/admin.go index 8a4104a5..9ceabe93 100644 --- a/authority/mgmt/admin.go +++ b/authority/mgmt/admin.go @@ -4,25 +4,21 @@ import "context" // Admin type. type Admin struct { - ID string `json:"-"` - AuthorityID string `json:"-"` - ProvisionerID string `json:"provisionerID"` - Name string `json:"name"` - ProvisionerName string `json:"provisionerName"` - ProvisionerType string `json:"provisionerType"` - IsSuperAdmin bool `json:"isSuperAdmin"` - Status StatusType `json:"status"` + ID string `json:"id"` + AuthorityID string `json:"-"` + ProvisionerID string `json:"provisionerID"` + Name string `json:"name"` + IsSuperAdmin bool `json:"isSuperAdmin"` + Status StatusType `json:"status"` } // CreateAdmin builds and stores an admin type in the DB. -func CreateAdmin(ctx context.Context, db DB, name string, prov *Provisioner, isSuperAdmin bool) (*Admin, error) { +func CreateAdmin(ctx context.Context, db DB, name string, provID string, isSuperAdmin bool) (*Admin, error) { adm := &Admin{ - Name: name, - ProvisionerID: prov.ID, - ProvisionerName: prov.Name, - ProvisionerType: prov.Type, - IsSuperAdmin: isSuperAdmin, - Status: StatusActive, + Name: name, + ProvisionerID: provID, + IsSuperAdmin: isSuperAdmin, + Status: StatusActive, } if err := db.CreateAdmin(ctx, adm); err != nil { return nil, WrapErrorISE(err, "error creating admin") diff --git a/authority/mgmt/api/provisioner.go b/authority/mgmt/api/provisioner.go index 43293221..b6b4c1c7 100644 --- a/authority/mgmt/api/provisioner.go +++ b/authority/mgmt/api/provisioner.go @@ -1,6 +1,7 @@ package api import ( + "fmt" "net/http" "github.com/go-chi/chi" @@ -10,12 +11,14 @@ import ( // CreateProvisionerRequest represents the body for a CreateProvisioner request. type CreateProvisionerRequest struct { - Type string `json:"type"` - Name string `json:"name"` - Claims *mgmt.Claims `json:"claims"` - Details interface{} `json:"details"` - X509Template string `json:"x509Template"` - SSHTemplate string `json:"sshTemplate"` + Type string `json:"type"` + Name string `json:"name"` + Claims *mgmt.Claims `json:"claims"` + Details interface{} `json:"details"` + X509Template string `json:"x509Template"` + X509TemplateData []byte `json:"x509TemplateData"` + SSHTemplate string `json:"sshTemplate"` + SSHTemplateData []byte `json:"sshTemplateData"` } // Validate validates a new-provisioner request body. @@ -25,10 +28,12 @@ func (car *CreateProvisionerRequest) Validate() error { // UpdateProvisionerRequest represents the body for a UpdateProvisioner request. type UpdateProvisionerRequest struct { - Claims *mgmt.Claims `json:"claims"` - Details interface{} `json:"details"` - X509Template string `json:"x509Template"` - SSHTemplate string `json:"sshTemplate"` + Claims *mgmt.Claims `json:"claims"` + Details interface{} `json:"details"` + X509Template string `json:"x509Template"` + X509TemplateData []byte `json:"x509TemplateData"` + SSHTemplate string `json:"sshTemplate"` + SSHTemplateData []byte `json:"sshTemplateData"` } // Validate validates a new-provisioner request body. @@ -58,6 +63,7 @@ func (h *Handler) GetProvisioners(w http.ResponseWriter, r *http.Request) { api.WriteError(w, err) return } + fmt.Printf("provs = %+v\n", provs) api.JSON(w, provs) } @@ -75,12 +81,14 @@ func (h *Handler) CreateProvisioner(w http.ResponseWriter, r *http.Request) { } prov := &mgmt.Provisioner{ - Type: body.Type, - Name: body.Name, - Claims: body.Claims, - Details: body.Details, - X509Template: body.X509Template, - SSHTemplate: body.SSHTemplate, + Type: body.Type, + Name: body.Name, + Claims: body.Claims, + Details: body.Details, + X509Template: body.X509Template, + X509TemplateData: body.X509TemplateData, + SSHTemplate: body.SSHTemplate, + SSHTemplateData: body.SSHTemplateData, } if err := h.db.CreateProvisioner(ctx, prov); err != nil { api.WriteError(w, err) diff --git a/authority/mgmt/config.go b/authority/mgmt/config.go index 0cd25aad..b3ece47f 100644 --- a/authority/mgmt/config.go +++ b/authority/mgmt/config.go @@ -2,6 +2,7 @@ package mgmt import ( "context" + "fmt" "github.com/pkg/errors" "github.com/smallstep/certificates/authority/config" @@ -24,6 +25,17 @@ const ( StatusDeleted ) +func (st StatusType) String() string { + switch st { + case StatusActive: + return "active" + case StatusDeleted: + return "deleted" + default: + return fmt.Sprintf("status %d not found", st) + } +} + // Claims encapsulates all x509 and ssh claims applied to the authority // configuration. E.g. maxTLSCertDuration, defaultSSHCertDuration, etc. type Claims struct { @@ -111,7 +123,7 @@ func CreateAuthority(ctx context.Context, db DB, options ...AuthorityOption) (*A return nil, WrapErrorISE(err, "error creating first provisioner") } - admin, err := CreateAdmin(ctx, db, "Change Me", prov, true) + admin, err := CreateAdmin(ctx, db, "Change Me", prov.ID, true) if err != nil { // TODO should we try to clean up? return nil, WrapErrorISE(err, "error creating first provisioner") diff --git a/authority/mgmt/db/nosql/admin.go b/authority/mgmt/db/nosql/admin.go index 564ac1c7..97fc81b0 100644 --- a/authority/mgmt/db/nosql/admin.go +++ b/authority/mgmt/db/nosql/admin.go @@ -63,19 +63,13 @@ func (db *DB) GetAdmin(ctx context.Context, id string) (*mgmt.Admin, error) { return nil, err } if adm.Status == mgmt.StatusDeleted { - return nil, mgmt.NewError(mgmt.ErrorDeletedType, "admin %s is 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 } diff --git a/authority/mgmt/db/nosql/provisioner.go b/authority/mgmt/db/nosql/provisioner.go index 93dcc47b..dbea49f4 100644 --- a/authority/mgmt/db/nosql/provisioner.go +++ b/authority/mgmt/db/nosql/provisioner.go @@ -3,6 +3,7 @@ package nosql import ( "context" "encoding/json" + "fmt" "time" "github.com/pkg/errors" @@ -12,16 +13,18 @@ import ( // dbProvisioner is the database representation of a Provisioner type. type dbProvisioner struct { - ID string `json:"id"` - AuthorityID string `json:"authorityID"` - Type string `json:"type"` - Name string `json:"name"` - Claims *mgmt.Claims `json:"claims"` - Details interface{} `json:"details"` - X509Template string `json:"x509Template"` - SSHTemplate string `json:"sshTemplate"` - CreatedAt time.Time `json:"createdAt"` - DeletedAt time.Time `json:"deletedAt"` + ID string `json:"id"` + AuthorityID string `json:"authorityID"` + Type string `json:"type"` + Name string `json:"name"` + Claims *mgmt.Claims `json:"claims"` + Details []byte `json:"details"` + X509Template string `json:"x509Template"` + X509TemplateData []byte `json:"x509TemplateData"` + SSHTemplate string `json:"sshTemplate"` + SSHTemplateData []byte `json:"sshTemplateData"` + CreatedAt time.Time `json:"createdAt"` + DeletedAt time.Time `json:"deletedAt"` } func (dbp *dbProvisioner) clone() *dbProvisioner { @@ -84,19 +87,36 @@ func unmarshalDBProvisioner(data []byte, id string) (*dbProvisioner, error) { return dbp, nil } +type detailsType struct { + Type mgmt.ProvisionerType +} + func unmarshalProvisioner(data []byte, id string) (*mgmt.Provisioner, error) { dbp, err := unmarshalDBProvisioner(data, id) if err != nil { return nil, err } + dt := new(detailsType) + if err := json.Unmarshal(dbp.Details, dt); err != nil { + return nil, mgmt.WrapErrorISE(err, "error unmarshaling details to detailsType for provisioner %s", id) + } + details, err := unmarshalDetails(dt.Type, dbp.Details) + if err != nil { + return nil, err + } + prov := &mgmt.Provisioner{ - ID: dbp.ID, - Type: dbp.Type, - Name: dbp.Name, - Claims: dbp.Claims, - X509Template: dbp.X509Template, - SSHTemplate: dbp.SSHTemplate, + ID: dbp.ID, + AuthorityID: dbp.AuthorityID, + Type: dbp.Type, + Name: dbp.Name, + Claims: dbp.Claims, + Details: details, + X509Template: dbp.X509Template, + X509TemplateData: dbp.X509TemplateData, + SSHTemplate: dbp.SSHTemplate, + SSHTemplateData: dbp.SSHTemplateData, } if !dbp.DeletedAt.IsZero() { prov.Status = mgmt.StatusDeleted @@ -172,3 +192,66 @@ func (db *DB) UpdateProvisioner(ctx context.Context, prov *mgmt.Provisioner) err return db.save(ctx, old.ID, nu, old, "provisioner", authorityProvisionersTable) } + +func unmarshalDetails(typ ProvisionerType, details []byte) (interface{}, error) { + if !s.Valid { + return nil, nil + } + var v isProvisionerDetails_Data + switch typ { + case ProvisionerTypeJWK: + p := new(ProvisionerDetailsJWK) + if err := json.Unmarshal([]byte(s.String), p); err != nil { + return nil, err + } + if p.JWK.Key.Key == nil { + key, err := LoadKey(ctx, db, p.JWK.Key.Id.Id) + if err != nil { + return nil, err + } + p.JWK.Key = key + } + return &ProvisionerDetails{Data: p}, nil + case ProvisionerType_OIDC: + v = new(ProvisionerDetails_OIDC) + case ProvisionerType_GCP: + v = new(ProvisionerDetails_GCP) + case ProvisionerType_AWS: + v = new(ProvisionerDetails_AWS) + case ProvisionerType_AZURE: + v = new(ProvisionerDetails_Azure) + case ProvisionerType_ACME: + v = new(ProvisionerDetails_ACME) + case ProvisionerType_X5C: + p := new(ProvisionerDetails_X5C) + if err := json.Unmarshal([]byte(s.String), p); err != nil { + return nil, err + } + for _, k := range p.X5C.GetRoots() { + if err := k.Select(ctx, db, k.Id.Id); err != nil { + return nil, err + } + } + return &ProvisionerDetails{Data: p}, nil + case ProvisionerType_K8SSA: + p := new(ProvisionerDetails_K8SSA) + if err := json.Unmarshal([]byte(s.String), p); err != nil { + return nil, err + } + for _, k := range p.K8SSA.GetPublicKeys() { + if err := k.Select(ctx, db, k.Id.Id); err != nil { + return nil, err + } + } + return &ProvisionerDetails{Data: p}, nil + case ProvisionerType_SSHPOP: + v = new(ProvisionerDetails_SSHPOP) + default: + return nil, fmt.Errorf("unsupported provisioner type %s", typ) + } + + if err := json.Unmarshal([]byte(s.String), v); err != nil { + return nil, err + } + return &ProvisionerDetails{Data: v}, nil +} diff --git a/authority/mgmt/provisioner.go b/authority/mgmt/provisioner.go index 427846fa..c455bf85 100644 --- a/authority/mgmt/provisioner.go +++ b/authority/mgmt/provisioner.go @@ -20,6 +20,17 @@ type ProvisionerCtx struct { Password string } +type ProvisionerType string + +var ( + ProvisionerTypeJWK = ProvisionerType("JWK") + ProvisionerTypeOIDC = ProvisionerType("OIDC") + ProvisionerTypeACME = ProvisionerType("ACME") + ProvisionerTypeX5C = ProvisionerType("X5C") + ProvisionerTypeK8S = ProvisionerType("K8S") + ProvisionerTypeSSHPOP = ProvisionerType("SSHPOP") +) + func NewProvisionerCtx(opts ...ProvisionerOption) *ProvisionerCtx { pc := &ProvisionerCtx{ Claims: NewDefaultClaims(), @@ -97,12 +108,14 @@ func CreateProvisioner(ctx context.Context, db DB, typ, name string, opts ...Pro return p, nil } -type ProvisionerDetails_JWK struct { - PubKey []byte `json:"pubKey"` - PrivKey string `json:"privKey"` +// ProvisionerDetailsJWK represents the values required by a JWK provisioner. +type ProvisionerDetailsJWK struct { + Type ProvisionerType `json:"type"` + PubKey []byte `json:"pubKey"` + EncPrivKey string `json:"privKey"` } -func createJWKDetails(pc *ProvisionerCtx) (*ProvisionerDetails_JWK, error) { +func createJWKDetails(pc *ProvisionerCtx) (*ProvisionerDetailsJWK, error) { var err error if pc.JWK != nil && pc.JWE == nil { @@ -131,9 +144,10 @@ func createJWKDetails(pc *ProvisionerCtx) (*ProvisionerDetails_JWK, error) { return nil, WrapErrorISE(err, "error serializing JWE") } - return &ProvisionerDetails_JWK{ - PubKey: jwkPubBytes, - PrivKey: jwePrivStr, + return &ProvisionerDetailsJWK{ + Type: ProvisionerTypeJWK, + PubKey: jwkPubBytes, + EncPrivKey: jwePrivStr, }, nil } @@ -150,7 +164,7 @@ func (p *Provisioner) ToCertificates() (provisioner.Interface, error) { } switch details := p.Details.(type) { - case *ProvisionerDetails_JWK: + case *ProvisionerDetailsJWK: jwk := new(jose.JSONWebKey) if err := json.Unmarshal(details.PubKey, &jwk); err != nil { return nil, err @@ -159,7 +173,7 @@ func (p *Provisioner) ToCertificates() (provisioner.Interface, error) { Type: p.Type, Name: p.Name, Key: jwk, - EncryptedKey: details.PrivKey, + EncryptedKey: details.EncPrivKey, Claims: claims, Options: p.GetOptions(), }, nil @@ -324,68 +338,6 @@ func marshalDetails(d *ProvisionerDetails) (sql.NullString, error) { }, nil } -func unmarshalDetails(ctx context.Context, db database.DB, typ ProvisionerType, s sql.NullString) (*ProvisionerDetails, error) { - if !s.Valid { - return nil, nil - } - var v isProvisionerDetails_Data - switch typ { - case ProvisionerType_JWK: - p := new(ProvisionerDetails_JWK) - if err := json.Unmarshal([]byte(s.String), p); err != nil { - return nil, err - } - if p.JWK.Key.Key == nil { - key, err := LoadKey(ctx, db, p.JWK.Key.Id.Id) - if err != nil { - return nil, err - } - p.JWK.Key = key - } - return &ProvisionerDetails{Data: p}, nil - case ProvisionerType_OIDC: - v = new(ProvisionerDetails_OIDC) - case ProvisionerType_GCP: - v = new(ProvisionerDetails_GCP) - case ProvisionerType_AWS: - v = new(ProvisionerDetails_AWS) - case ProvisionerType_AZURE: - v = new(ProvisionerDetails_Azure) - case ProvisionerType_ACME: - v = new(ProvisionerDetails_ACME) - case ProvisionerType_X5C: - p := new(ProvisionerDetails_X5C) - if err := json.Unmarshal([]byte(s.String), p); err != nil { - return nil, err - } - for _, k := range p.X5C.GetRoots() { - if err := k.Select(ctx, db, k.Id.Id); err != nil { - return nil, err - } - } - return &ProvisionerDetails{Data: p}, nil - case ProvisionerType_K8SSA: - p := new(ProvisionerDetails_K8SSA) - if err := json.Unmarshal([]byte(s.String), p); err != nil { - return nil, err - } - for _, k := range p.K8SSA.GetPublicKeys() { - if err := k.Select(ctx, db, k.Id.Id); err != nil { - return nil, err - } - } - return &ProvisionerDetails{Data: p}, nil - case ProvisionerType_SSHPOP: - v = new(ProvisionerDetails_SSHPOP) - default: - return nil, fmt.Errorf("unsupported provisioner type %s", typ) - } - - if err := json.Unmarshal([]byte(s.String), v); err != nil { - return nil, err - } - return &ProvisionerDetails{Data: v}, nil -} func marshalClaims(c *Claims) (sql.NullString, error) { b, err := json.Marshal(c) diff --git a/ca/mgmtClient.go b/ca/mgmtClient.go index ac494d90..67d6631c 100644 --- a/ca/mgmtClient.go +++ b/ca/mgmtClient.go @@ -105,3 +105,26 @@ retry: } return *admins, nil } + +// GetProvisioners performs the GET /mgmt/provisioners request to the CA. +func (c *MgmtClient) GetProvisioners() ([]*mgmt.Provisioner, error) { + var retried bool + u := c.endpoint.ResolveReference(&url.URL{Path: "/mgmt/provisioners"}) +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, readError(resp.Body) + } + var provs = new([]*mgmt.Provisioner) + if err := readJSON(resp.Body, provs); err != nil { + return nil, errors.Wrapf(err, "error reading %s", u) + } + return *provs, nil +}