From 4d48072746864c44f44349f6f3d419a8a452af3f Mon Sep 17 00:00:00 2001 From: max furman Date: Wed, 12 May 2021 00:03:40 -0700 Subject: [PATCH] wip admin CRUD --- authority/authority.go | 1 + authority/mgmt/api/admin.go | 142 +++++++++++++++---------- authority/mgmt/api/handler.go | 4 +- authority/mgmt/db/nosql/admin.go | 1 + authority/mgmt/db/nosql/provisioner.go | 90 ++++++---------- authority/mgmt/errors.go | 11 +- authority/mgmt/provisioner.go | 112 +++++++++++-------- ca/client.go | 5 + ca/mgmtClient.go | 85 +++++++++++++++ 9 files changed, 295 insertions(+), 156 deletions(-) diff --git a/authority/authority.go b/authority/authority.go index 016b838a..ee82eb13 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -162,6 +162,7 @@ func (a *Authority) init() error { return mgmt.WrapErrorISE(err, "error getting authConfig from db") } } + a.config.AuthorityConfig, err = mgmtAuthConfig.ToCertificates() if err != nil { return err diff --git a/authority/mgmt/api/admin.go b/authority/mgmt/api/admin.go index f544b631..ae60b75a 100644 --- a/authority/mgmt/api/admin.go +++ b/authority/mgmt/api/admin.go @@ -5,13 +5,14 @@ import ( "github.com/go-chi/chi" "github.com/smallstep/certificates/api" + "github.com/smallstep/certificates/authority/mgmt" ) // CreateAdminRequest represents the body for a CreateAdmin request. type CreateAdminRequest struct { - Name string `json:"name"` - Provisioner string `json:"provisioner"` - IsSuperAdmin bool `json:"isSuperAdmin"` + Name string `json:"name"` + ProvisionerID string `json:"provisionerID"` + IsSuperAdmin bool `json:"isSuperAdmin"` } // Validate validates a new-admin request body. @@ -21,9 +22,10 @@ func (car *CreateAdminRequest) Validate() error { // UpdateAdminRequest represents the body for a UpdateAdmin request. type UpdateAdminRequest struct { - Name string `json:"name"` - Provisioner string `json:"provisioner"` - IsSuperAdmin bool `json:"isSuperAdmin"` + Name string `json:"name"` + ProvisionerID string `json:"provisionerID"` + IsSuperAdmin string `json:"isSuperAdmin"` + Status string `json:"status"` } // Validate validates a new-admin request body. @@ -31,6 +33,11 @@ func (uar *UpdateAdminRequest) Validate() error { return nil } +// DeleteResponse is the resource for successful DELETE responses. +type DeleteResponse struct { + Status string `json:"status"` +} + // GetAdmin returns the requested admin, or an error. func (h *Handler) GetAdmin(w http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -58,59 +65,84 @@ func (h *Handler) GetAdmins(w http.ResponseWriter, r *http.Request) { // CreateAdmin creates a new admin. func (h *Handler) CreateAdmin(w http.ResponseWriter, r *http.Request) { - /* - ctx := r.Context() - - var body CreateAdminRequest - if err := ReadJSON(r.Body, &body); err != nil { - api.WriteError(w, err) - return - } - if err := body.Validate(); err != nil { - api.WriteError(w, err) - } - - adm := &config.Admin{ - Name: body.Name, - Provisioner: body.Provisioner, - IsSuperAdmin: body.IsSuperAdmin, - } - if err := h.db.CreateAdmin(ctx, adm); err != nil { - api.WriteError(w, err) - return - } - api.JSONStatus(w, adm, http.StatusCreated) - */ + ctx := r.Context() + + var body CreateAdminRequest + if err := api.ReadJSON(r.Body, &body); err != nil { + api.WriteError(w, mgmt.WrapError(mgmt.ErrorBadRequestType, err, "error reading request body")) + return + } + + // TODO validate + + adm := &mgmt.Admin{ + ProvisionerID: body.ProvisionerID, + Name: body.Name, + IsSuperAdmin: body.IsSuperAdmin, + Status: mgmt.StatusActive, + } + if err := h.db.CreateAdmin(ctx, adm); err != nil { + api.WriteError(w, mgmt.WrapErrorISE(err, "error creating admin")) + return + } + api.JSON(w, adm) +} + +// DeleteAdmin deletes admin. +func (h *Handler) DeleteAdmin(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + 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)) + return + } + adm.Status = mgmt.StatusDeleted + if err := h.db.UpdateAdmin(ctx, adm); err != nil { + api.WriteError(w, mgmt.WrapErrorISE(err, "error updating admin %s", id)) + return + } + api.JSON(w, &DeleteResponse{Status: "ok"}) } // UpdateAdmin updates an existing admin. func (h *Handler) UpdateAdmin(w http.ResponseWriter, r *http.Request) { - /* - ctx := r.Context() - id := chi.URLParam(r, "id") - - var body UpdateAdminRequest - if err := ReadJSON(r.Body, &body); err != nil { - api.WriteError(w, err) - return - } - if err := body.Validate(); err != nil { - api.WriteError(w, err) - return - } - if adm, err := h.db.GetAdmin(ctx, id); err != nil { - api.WriteError(w, err) - return - } + ctx := r.Context() + + var body UpdateAdminRequest + if err := api.ReadJSON(r.Body, &body); err != nil { + api.WriteError(w, mgmt.WrapError(mgmt.ErrorBadRequestType, err, "error reading request body")) + return + } + + 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)) + return + } + + // TODO validate + + if len(body.Name) > 0 { adm.Name = body.Name - adm.Provisioner = body.Provisioner - adm.IsSuperAdmin = body.IsSuperAdmin - - if err := h.db.UpdateAdmin(ctx, adm); err != nil { - api.WriteError(w, err) - return - } - api.JSON(w, adm) - */ + } + if len(body.Status) > 0 { + adm.Status = mgmt.StatusActive // FIXME + } + // Set IsSuperAdmin iff the string was set in the update request. + if len(body.IsSuperAdmin) > 0 { + adm.IsSuperAdmin = (body.IsSuperAdmin == "true") + } + if len(body.ProvisionerID) > 0 { + adm.ProvisionerID = body.ProvisionerID + } + if err := h.db.UpdateAdmin(ctx, adm); err != nil { + api.WriteError(w, mgmt.WrapErrorISE(err, "error updating admin %s", id)) + return + } + api.JSON(w, adm) } diff --git a/authority/mgmt/api/handler.go b/authority/mgmt/api/handler.go index 0e512a39..778cdaea 100644 --- a/authority/mgmt/api/handler.go +++ b/authority/mgmt/api/handler.go @@ -33,13 +33,15 @@ func (h *Handler) Route(r api.Router) { r.MethodFunc("GET", "/provisioner/{id}", h.GetProvisioner) r.MethodFunc("GET", "/provisioners", h.GetProvisioners) r.MethodFunc("POST", "/provisioner", h.CreateProvisioner) - r.MethodFunc("PUT", "/provsiioner/{id}", h.UpdateProvisioner) + r.MethodFunc("PUT", "/provisioner/{id}", h.UpdateProvisioner) + //r.MethodFunc("DELETE", "/provisioner/{id}", h.UpdateAdmin) // Admins r.MethodFunc("GET", "/admin/{id}", h.GetAdmin) r.MethodFunc("GET", "/admins", h.GetAdmins) r.MethodFunc("POST", "/admin", h.CreateAdmin) r.MethodFunc("PUT", "/admin/{id}", h.UpdateAdmin) + r.MethodFunc("DELETE", "/admin/{id}", h.DeleteAdmin) // AuthConfig r.MethodFunc("GET", "/authconfig/{id}", h.GetAuthConfig) diff --git a/authority/mgmt/db/nosql/admin.go b/authority/mgmt/db/nosql/admin.go index 97fc81b0..70cb12d1 100644 --- a/authority/mgmt/db/nosql/admin.go +++ b/authority/mgmt/db/nosql/admin.go @@ -131,6 +131,7 @@ func (db *DB) CreateAdmin(ctx context.Context, adm *mgmt.Admin) error { if err != nil { return errors.Wrap(err, "error generating random id for admin") } + adm.AuthorityID = db.authorityID dba := &dbAdmin{ ID: adm.ID, diff --git a/authority/mgmt/db/nosql/provisioner.go b/authority/mgmt/db/nosql/provisioner.go index dbea49f4..6d9f74ab 100644 --- a/authority/mgmt/db/nosql/provisioner.go +++ b/authority/mgmt/db/nosql/provisioner.go @@ -126,7 +126,6 @@ func unmarshalProvisioner(data []byte, id string) (*mgmt.Provisioner, error) { // GetProvisioners retrieves and unmarshals all active (not deleted) provisioners // from the database. -// TODO should we be paginating? func (db *DB) GetProvisioners(ctx context.Context) ([]*mgmt.Provisioner, error) { dbEntries, err := db.db.List(authorityProvisionersTable) if err != nil { @@ -157,13 +156,18 @@ func (db *DB) CreateProvisioner(ctx context.Context, prov *mgmt.Provisioner) err return errors.Wrap(err, "error generating random id for provisioner") } + details, err := json.Marshal(prov.Details) + if err != nil { + return mgmt.WrapErrorISE(err, "error marshaling details when creating provisioner") + } + dbp := &dbProvisioner{ ID: prov.ID, AuthorityID: db.authorityID, Type: prov.Type, Name: prov.Name, Claims: prov.Claims, - Details: prov.Details, + Details: details, X509Template: prov.X509Template, SSHTemplate: prov.SSHTemplate, CreatedAt: clock.Now(), @@ -186,72 +190,44 @@ func (db *DB) UpdateProvisioner(ctx context.Context, prov *mgmt.Provisioner) err nu.DeletedAt = clock.Now() } nu.Claims = prov.Claims - nu.Details = prov.Details nu.X509Template = prov.X509Template nu.SSHTemplate = prov.SSHTemplate + nu.Details, err = json.Marshal(prov.Details) + if err != nil { + return mgmt.WrapErrorISE(err, "error marshaling details when creating provisioner") + } + 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 +func unmarshalDetails(typ mgmt.ProvisionerType, data []byte) (mgmt.ProvisionerDetails, error) { + var v mgmt.ProvisionerDetails 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) + case mgmt.ProvisionerTypeJWK: + v = new(mgmt.ProvisionerDetailsJWK) + case mgmt.ProvisionerTypeOIDC: + v = new(mgmt.ProvisionerDetailsOIDC) + case mgmt.ProvisionerTypeGCP: + v = new(mgmt.ProvisionerDetailsGCP) + case mgmt.ProvisionerTypeAWS: + v = new(mgmt.ProvisionerDetailsAWS) + case mgmt.ProvisionerTypeAZURE: + v = new(mgmt.ProvisionerDetailsAzure) + case mgmt.ProvisionerTypeACME: + v = new(mgmt.ProvisionerDetailsACME) + case mgmt.ProvisionerTypeX5C: + v = new(mgmt.ProvisionerDetailsX5C) + case mgmt.ProvisionerTypeK8SSA: + v = new(mgmt.ProvisionerDetailsK8SSA) + case mgmt.ProvisionerTypeSSHPOP: + v = new(mgmt.ProvisionerDetailsSSHPOP) default: return nil, fmt.Errorf("unsupported provisioner type %s", typ) } - if err := json.Unmarshal([]byte(s.String), v); err != nil { + if err := json.Unmarshal(data, v); err != nil { return nil, err } - return &ProvisionerDetails{Data: v}, nil + return v, nil } diff --git a/authority/mgmt/errors.go b/authority/mgmt/errors.go index ae18619c..f0f90400 100644 --- a/authority/mgmt/errors.go +++ b/authority/mgmt/errors.go @@ -23,6 +23,8 @@ const ( ErrorAuthorityMismatchType // ErrorDeletedType resource has been deleted. ErrorDeletedType + // ErrorBadRequestType bad request. + ErrorBadRequestType // ErrorServerInternalType internal server error. ErrorServerInternalType ) @@ -37,6 +39,8 @@ func (ap ProblemType) String() string { return "authorityMismatch" case ErrorDeletedType: return "deleted" + case ErrorBadRequestType: + return "badRequest" case ErrorServerInternalType: return "internalServerError" default: @@ -69,10 +73,15 @@ var ( status: 401, }, ErrorDeletedType: { - typ: ErrorNotFoundType.String(), + typ: ErrorDeletedType.String(), details: "resource is deleted", status: 403, }, + ErrorBadRequestType: { + typ: ErrorBadRequestType.String(), + details: "bad request", + status: 400, + }, ErrorServerInternalType: errorServerInternalMetadata, } ) diff --git a/authority/mgmt/provisioner.go b/authority/mgmt/provisioner.go index c455bf85..961907f8 100644 --- a/authority/mgmt/provisioner.go +++ b/authority/mgmt/provisioner.go @@ -23,12 +23,15 @@ type ProvisionerCtx struct { type ProvisionerType string var ( + ProvisionerTypeACME = ProvisionerType("ACME") + ProvisionerTypeAWS = ProvisionerType("AWS") + ProvisionerTypeAZURE = ProvisionerType("AZURE") + ProvisionerTypeGCP = ProvisionerType("GCP") ProvisionerTypeJWK = ProvisionerType("JWK") + ProvisionerTypeK8SSA = ProvisionerType("K8SSA") ProvisionerTypeOIDC = ProvisionerType("OIDC") - ProvisionerTypeACME = ProvisionerType("ACME") - ProvisionerTypeX5C = ProvisionerType("X5C") - ProvisionerTypeK8S = ProvisionerType("K8S") ProvisionerTypeSSHPOP = ProvisionerType("SSHPOP") + ProvisionerTypeX5C = ProvisionerType("X5C") ) func NewProvisionerCtx(opts ...ProvisionerOption) *ProvisionerCtx { @@ -56,8 +59,8 @@ func WithPassword(pass string) func(*ProvisionerCtx) { // Provisioner type. type Provisioner struct { - ID string `json:"-"` - AuthorityID string `json:"-"` + ID string `json:"id"` + AuthorityID string `json:"authorityID"` Type string `json:"type"` Name string `json:"name"` Claims *Claims `json:"claims"` @@ -108,6 +111,10 @@ func CreateProvisioner(ctx context.Context, db DB, typ, name string, opts ...Pro return p, nil } +type ProvisionerDetails interface { + isProvisionerDetails() +} + // ProvisionerDetailsJWK represents the values required by a JWK provisioner. type ProvisionerDetailsJWK struct { Type ProvisionerType `json:"type"` @@ -115,6 +122,64 @@ type ProvisionerDetailsJWK struct { EncPrivKey string `json:"privKey"` } +// ProvisionerDetailsOIDC represents the values required by a OIDC provisioner. +type ProvisionerDetailsOIDC struct { + Type ProvisionerType `json:"type"` +} + +// ProvisionerDetailsGCP represents the values required by a GCP provisioner. +type ProvisionerDetailsGCP struct { + Type ProvisionerType `json:"type"` +} + +// ProvisionerDetailsAWS represents the values required by a AWS provisioner. +type ProvisionerDetailsAWS struct { + Type ProvisionerType `json:"type"` +} + +// ProvisionerDetailsAzure represents the values required by a Azure provisioner. +type ProvisionerDetailsAzure struct { + Type ProvisionerType `json:"type"` +} + +// ProvisionerDetailsACME represents the values required by a ACME provisioner. +type ProvisionerDetailsACME struct { + Type ProvisionerType `json:"type"` +} + +// ProvisionerDetailsX5C represents the values required by a X5C provisioner. +type ProvisionerDetailsX5C struct { + Type ProvisionerType `json:"type"` +} + +// ProvisionerDetailsK8SSA represents the values required by a K8SSA provisioner. +type ProvisionerDetailsK8SSA struct { + Type ProvisionerType `json:"type"` +} + +// ProvisionerDetailsSSHPOP represents the values required by a SSHPOP provisioner. +type ProvisionerDetailsSSHPOP struct { + Type ProvisionerType `json:"type"` +} + +func (*ProvisionerDetailsJWK) isProvisionerDetails() {} + +func (*ProvisionerDetailsOIDC) isProvisionerDetails() {} + +func (*ProvisionerDetailsGCP) isProvisionerDetails() {} + +func (*ProvisionerDetailsAWS) isProvisionerDetails() {} + +func (*ProvisionerDetailsAzure) isProvisionerDetails() {} + +func (*ProvisionerDetailsACME) isProvisionerDetails() {} + +func (*ProvisionerDetailsX5C) isProvisionerDetails() {} + +func (*ProvisionerDetailsK8SSA) isProvisionerDetails() {} + +func (*ProvisionerDetailsSSHPOP) isProvisionerDetails() {} + func createJWKDetails(pc *ProvisionerCtx) (*ProvisionerDetailsJWK, error) { var err error @@ -159,10 +224,6 @@ func (p *Provisioner) ToCertificates() (provisioner.Interface, error) { return nil, err } - if err != nil { - return nil, err - } - switch details := p.Details.(type) { case *ProvisionerDetailsJWK: jwk := new(jose.JSONWebKey) @@ -325,36 +386,3 @@ func (c *Claims) ToCertificates() (*provisioner.Claims, error) { EnableSSHCA: &c.SSH.Enabled, }, nil } - -/* -func marshalDetails(d *ProvisionerDetails) (sql.NullString, error) { - b, err := json.Marshal(d.GetData()) - if err != nil { - return sql.NullString{}, nil - } - return sql.NullString{ - String: string(b), - Valid: len(b) > 0, - }, nil -} - - -func marshalClaims(c *Claims) (sql.NullString, error) { - b, err := json.Marshal(c) - if err != nil { - return sql.NullString{}, nil - } - return sql.NullString{ - String: string(b), - Valid: len(b) > 0, - }, nil -} - -func unmarshalClaims(s sql.NullString) (*Claims, error) { - if !s.Valid { - return nil, nil - } - v := new(Claims) - return v, json.Unmarshal([]byte(s.String), v) -} -*/ diff --git a/ca/client.go b/ca/client.go index 2292c41e..3a3350ac 100644 --- a/ca/client.go +++ b/ca/client.go @@ -88,6 +88,11 @@ func (c *uaClient) Post(url, contentType string, body io.Reader) (*http.Response return c.Client.Do(req) } +func (c *uaClient) Do(req *http.Request) (*http.Response, error) { + req.Header.Set("User-Agent", UserAgent) + return c.Client.Do(req) +} + // RetryFunc defines the method used to retry a request. If it returns true, the // request will be retried once. type RetryFunc func(code int) bool diff --git a/ca/mgmtClient.go b/ca/mgmtClient.go index 67d6631c..47316ad6 100644 --- a/ca/mgmtClient.go +++ b/ca/mgmtClient.go @@ -1,12 +1,16 @@ package ca import ( + "bytes" + "encoding/json" "net/http" "net/url" "path" "github.com/pkg/errors" "github.com/smallstep/certificates/authority/mgmt" + mgmtAPI "github.com/smallstep/certificates/authority/mgmt/api" + "github.com/smallstep/certificates/errs" ) // MgmtClient implements an HTTP client for the CA server. @@ -83,6 +87,87 @@ retry: return adm, nil } +// CreateAdmin performs the POST /mgmt/admin request to the CA. +func (c *MgmtClient) CreateAdmin(req *mgmtAPI.CreateAdminRequest) (*mgmt.Admin, error) { + var retried bool + body, err := json.Marshal(req) + if err != nil { + return nil, errs.Wrap(http.StatusInternalServerError, err, "error marshaling request") + } + u := c.endpoint.ResolveReference(&url.URL{Path: "/mgmt/admin"}) +retry: + resp, err := c.client.Post(u.String(), "application/json", bytes.NewReader(body)) + if err != nil { + return nil, errors.Wrapf(err, "client POST %s failed", u) + } + if resp.StatusCode >= 400 { + if !retried && c.retryOnError(resp) { + retried = true + goto retry + } + return nil, readError(resp.Body) + } + var adm = new(mgmt.Admin) + if err := readJSON(resp.Body, adm); err != nil { + return nil, errors.Wrapf(err, "error reading %s", u) + } + return adm, nil +} + +// RemoveAdmin performs the DELETE /mgmt/admin/{id} request to the CA. +func (c *MgmtClient) RemoveAdmin(id string) error { + var retried bool + u := c.endpoint.ResolveReference(&url.URL{Path: path.Join("/mgmt/admin", id)}) + req, err := http.NewRequest("DELETE", u.String(), nil) + if err != nil { + return errors.Wrapf(err, "create DELETE %s request failed", u) + } +retry: + resp, err := c.client.Do(req) + if err != nil { + return errors.Wrapf(err, "client DELETE %s failed", u) + } + if resp.StatusCode >= 400 { + if !retried && c.retryOnError(resp) { + retried = true + goto retry + } + return readError(resp.Body) + } + return nil +} + +// UpdateAdmin performs the PUT /mgmt/admin/{id} request to the CA. +func (c *MgmtClient) UpdateAdmin(id string, uar *mgmtAPI.UpdateAdminRequest) (*mgmt.Admin, error) { + var retried bool + body, err := json.Marshal(uar) + if err != nil { + return nil, errs.Wrap(http.StatusInternalServerError, err, "error marshaling request") + } + u := c.endpoint.ResolveReference(&url.URL{Path: path.Join("/mgmt/admin", id)}) + req, err := http.NewRequest("PUT", u.String(), bytes.NewReader(body)) + if err != nil { + return nil, errors.Wrapf(err, "create PUT %s request failed", u) + } +retry: + resp, err := c.client.Do(req) + if err != nil { + return nil, errors.Wrapf(err, "client PUT %s failed", u) + } + if resp.StatusCode >= 400 { + if !retried && c.retryOnError(resp) { + retried = true + goto retry + } + return nil, readError(resp.Body) + } + var adm = new(mgmt.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