wip admin CRUD

This commit is contained in:
max furman 2021-05-12 00:03:40 -07:00
parent 98a6e54530
commit 4d48072746
9 changed files with 293 additions and 154 deletions

View file

@ -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

View file

@ -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()
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)
}
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
}
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)
*/
// 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")
ctx := r.Context()
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
}
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)
}

View file

@ -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)

View file

@ -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,

View file

@ -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
}

View file

@ -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,
}
)

View file

@ -23,12 +23,15 @@ type ProvisionerCtx struct {
type ProvisionerType string
var (
ProvisionerTypeJWK = ProvisionerType("JWK")
ProvisionerTypeOIDC = ProvisionerType("OIDC")
ProvisionerTypeACME = ProvisionerType("ACME")
ProvisionerTypeX5C = ProvisionerType("X5C")
ProvisionerTypeK8S = ProvisionerType("K8S")
ProvisionerTypeAWS = ProvisionerType("AWS")
ProvisionerTypeAZURE = ProvisionerType("AZURE")
ProvisionerTypeGCP = ProvisionerType("GCP")
ProvisionerTypeJWK = ProvisionerType("JWK")
ProvisionerTypeK8SSA = ProvisionerType("K8SSA")
ProvisionerTypeOIDC = ProvisionerType("OIDC")
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)
}
*/

View file

@ -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

View file

@ -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