2021-05-03 19:48:20 +00:00
|
|
|
package ca
|
|
|
|
|
|
|
|
import (
|
2021-05-12 07:03:40 +00:00
|
|
|
"bytes"
|
|
|
|
"encoding/json"
|
2021-05-18 04:07:25 +00:00
|
|
|
"io"
|
2021-05-03 19:48:20 +00:00
|
|
|
"net/http"
|
|
|
|
"net/url"
|
|
|
|
"path"
|
2021-05-18 23:50:54 +00:00
|
|
|
"strconv"
|
2021-05-03 19:48:20 +00:00
|
|
|
|
|
|
|
"github.com/pkg/errors"
|
2021-05-18 23:50:54 +00:00
|
|
|
"github.com/smallstep/certificates/authority/admin"
|
2021-05-03 19:48:20 +00:00
|
|
|
"github.com/smallstep/certificates/authority/mgmt"
|
2021-05-12 07:03:40 +00:00
|
|
|
mgmtAPI "github.com/smallstep/certificates/authority/mgmt/api"
|
|
|
|
"github.com/smallstep/certificates/errs"
|
2021-05-03 19:48:20 +00:00
|
|
|
)
|
|
|
|
|
2021-05-20 23:02:20 +00:00
|
|
|
// AdminClient implements an HTTP client for the CA server.
|
|
|
|
type AdminClient struct {
|
2021-05-03 19:48:20 +00:00
|
|
|
client *uaClient
|
|
|
|
endpoint *url.URL
|
|
|
|
retryFunc RetryFunc
|
|
|
|
opts []ClientOption
|
|
|
|
}
|
|
|
|
|
2021-05-20 23:02:20 +00:00
|
|
|
// NewAdminClient creates a new AdminClient with the given endpoint and options.
|
|
|
|
func NewAdminClient(endpoint string, opts ...ClientOption) (*AdminClient, error) {
|
2021-05-03 19:48:20 +00:00
|
|
|
u, err := parseEndpoint(endpoint)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
// Retrieve transport from options.
|
|
|
|
o := new(clientOptions)
|
|
|
|
if err := o.apply(opts); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
tr, err := o.getTransport(endpoint)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2021-05-20 23:02:20 +00:00
|
|
|
return &AdminClient{
|
2021-05-03 19:48:20 +00:00
|
|
|
client: newClient(tr),
|
|
|
|
endpoint: u,
|
|
|
|
retryFunc: o.retryFunc,
|
|
|
|
opts: opts,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2021-05-20 23:02:20 +00:00
|
|
|
func (c *AdminClient) retryOnError(r *http.Response) bool {
|
2021-05-03 19:48:20 +00:00
|
|
|
if c.retryFunc != nil {
|
|
|
|
if c.retryFunc(r.StatusCode) {
|
|
|
|
o := new(clientOptions)
|
|
|
|
if err := o.apply(c.opts); err != nil {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
tr, err := o.getTransport(c.endpoint.String())
|
|
|
|
if err != nil {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
r.Body.Close()
|
|
|
|
c.client.SetTransport(tr)
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2021-05-20 23:02:20 +00:00
|
|
|
// GetAdmin performs the GET /admin/admin/{id} request to the CA.
|
|
|
|
func (c *AdminClient) GetAdmin(id string) (*mgmt.Admin, error) {
|
2021-05-03 19:48:20 +00:00
|
|
|
var retried bool
|
2021-05-20 23:02:20 +00:00
|
|
|
u := c.endpoint.ResolveReference(&url.URL{Path: path.Join("/admin/admin", id)})
|
2021-05-03 19:48:20 +00:00
|
|
|
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
|
|
|
|
}
|
2021-05-20 23:02:20 +00:00
|
|
|
return nil, readAdminError(resp.Body)
|
2021-05-03 19:48:20 +00:00
|
|
|
}
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2021-05-18 23:50:54 +00:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-05-20 20:01:58 +00:00
|
|
|
// GetAdmins performs the GET /admin/admins request to the CA.
|
2021-05-20 23:02:20 +00:00
|
|
|
func (c *AdminClient) GetAdmins(opts ...AdminOption) (*mgmtAPI.GetAdminsResponse, error) {
|
2021-05-18 23:50:54 +00:00
|
|
|
var retried bool
|
|
|
|
o := new(adminOptions)
|
|
|
|
if err := o.apply(opts); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
u := c.endpoint.ResolveReference(&url.URL{
|
2021-05-20 20:01:58 +00:00
|
|
|
Path: "/admin/admins",
|
2021-05-18 23:50:54 +00:00
|
|
|
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
|
|
|
|
}
|
2021-05-20 23:02:20 +00:00
|
|
|
return nil, readAdminError(resp.Body)
|
2021-05-18 23:50:54 +00:00
|
|
|
}
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2021-05-20 23:02:20 +00:00
|
|
|
// CreateAdmin performs the POST /admin/admins request to the CA.
|
|
|
|
func (c *AdminClient) CreateAdmin(req *mgmtAPI.CreateAdminRequest) (*mgmt.Admin, error) {
|
2021-05-12 07:03:40 +00:00
|
|
|
var retried bool
|
|
|
|
body, err := json.Marshal(req)
|
|
|
|
if err != nil {
|
|
|
|
return nil, errs.Wrap(http.StatusInternalServerError, err, "error marshaling request")
|
|
|
|
}
|
2021-05-20 23:02:20 +00:00
|
|
|
u := c.endpoint.ResolveReference(&url.URL{Path: "/admin/admins"})
|
2021-05-12 07:03:40 +00:00
|
|
|
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
|
|
|
|
}
|
2021-05-20 23:02:20 +00:00
|
|
|
return nil, readAdminError(resp.Body)
|
2021-05-12 07:03:40 +00:00
|
|
|
}
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2021-05-20 23:02:20 +00:00
|
|
|
// RemoveAdmin performs the DELETE /admin/admins/{id} request to the CA.
|
|
|
|
func (c *AdminClient) RemoveAdmin(id string) error {
|
2021-05-12 07:03:40 +00:00
|
|
|
var retried bool
|
2021-05-20 23:02:20 +00:00
|
|
|
u := c.endpoint.ResolveReference(&url.URL{Path: path.Join("/admin/admins", id)})
|
2021-05-12 07:03:40 +00:00
|
|
|
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
|
|
|
|
}
|
2021-05-20 23:02:20 +00:00
|
|
|
return readAdminError(resp.Body)
|
2021-05-12 07:03:40 +00:00
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2021-05-20 23:02:20 +00:00
|
|
|
// UpdateAdmin performs the PUT /admin/admins/{id} request to the CA.
|
|
|
|
func (c *AdminClient) UpdateAdmin(id string, uar *mgmtAPI.UpdateAdminRequest) (*admin.Admin, error) {
|
2021-05-12 07:03:40 +00:00
|
|
|
var retried bool
|
|
|
|
body, err := json.Marshal(uar)
|
|
|
|
if err != nil {
|
|
|
|
return nil, errs.Wrap(http.StatusInternalServerError, err, "error marshaling request")
|
|
|
|
}
|
2021-05-20 23:02:20 +00:00
|
|
|
u := c.endpoint.ResolveReference(&url.URL{Path: path.Join("/admin/admins", id)})
|
2021-05-18 04:07:25 +00:00
|
|
|
req, err := http.NewRequest("PATCH", u.String(), bytes.NewReader(body))
|
2021-05-12 07:03:40 +00:00
|
|
|
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
|
|
|
|
}
|
2021-05-20 23:02:20 +00:00
|
|
|
return nil, readAdminError(resp.Body)
|
2021-05-12 07:03:40 +00:00
|
|
|
}
|
2021-05-18 23:50:54 +00:00
|
|
|
var adm = new(admin.Admin)
|
2021-05-12 07:03:40 +00:00
|
|
|
if err := readJSON(resp.Body, adm); err != nil {
|
|
|
|
return nil, errors.Wrapf(err, "error reading %s", u)
|
|
|
|
}
|
|
|
|
return adm, nil
|
|
|
|
}
|
|
|
|
|
2021-05-20 23:02:20 +00:00
|
|
|
// GetProvisioner performs the GET /admin/provisioners/{name} request to the CA.
|
|
|
|
func (c *AdminClient) GetProvisioner(name string) (*mgmt.Provisioner, error) {
|
2021-05-18 04:07:25 +00:00
|
|
|
var retried bool
|
2021-05-20 23:02:20 +00:00
|
|
|
u := c.endpoint.ResolveReference(&url.URL{Path: path.Join("/admin/provisioners", name)})
|
2021-05-18 04:07:25 +00:00
|
|
|
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
|
|
|
|
}
|
2021-05-20 23:02:20 +00:00
|
|
|
return nil, readAdminError(resp.Body)
|
2021-05-18 04:07:25 +00:00
|
|
|
}
|
|
|
|
var prov = new(mgmt.Provisioner)
|
|
|
|
if err := readJSON(resp.Body, prov); err != nil {
|
|
|
|
return nil, errors.Wrapf(err, "error reading %s", u)
|
|
|
|
}
|
|
|
|
return prov, nil
|
|
|
|
}
|
|
|
|
|
2021-05-20 20:01:58 +00:00
|
|
|
// GetProvisioners performs the GET /admin/provisioners request to the CA.
|
2021-05-20 23:02:20 +00:00
|
|
|
func (c *AdminClient) GetProvisioners() ([]*mgmt.Provisioner, error) {
|
2021-05-11 22:25:37 +00:00
|
|
|
var retried bool
|
2021-05-20 20:01:58 +00:00
|
|
|
u := c.endpoint.ResolveReference(&url.URL{Path: "/admin/provisioners"})
|
2021-05-11 22:25:37 +00:00
|
|
|
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
|
|
|
|
}
|
2021-05-20 23:02:20 +00:00
|
|
|
return nil, readAdminError(resp.Body)
|
2021-05-11 22:25:37 +00:00
|
|
|
}
|
|
|
|
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
|
|
|
|
}
|
2021-05-18 04:07:25 +00:00
|
|
|
|
2021-05-20 23:02:20 +00:00
|
|
|
// RemoveProvisioner performs the DELETE /admin/provisioners/{name} request to the CA.
|
|
|
|
func (c *AdminClient) RemoveProvisioner(name string) error {
|
2021-05-18 04:07:25 +00:00
|
|
|
var retried bool
|
2021-05-20 23:02:20 +00:00
|
|
|
u := c.endpoint.ResolveReference(&url.URL{Path: path.Join("/admin/provisioners", name)})
|
2021-05-18 04:07:25 +00:00
|
|
|
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
|
|
|
|
}
|
2021-05-20 23:02:20 +00:00
|
|
|
return readAdminError(resp.Body)
|
2021-05-18 04:07:25 +00:00
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2021-05-20 23:02:20 +00:00
|
|
|
// CreateProvisioner performs the POST /admin/provisioners request to the CA.
|
2021-05-21 20:31:41 +00:00
|
|
|
func (c *AdminClient) CreateProvisioner(prov *mgmt.Provisioner) (*mgmt.Provisioner, error) {
|
2021-05-18 04:07:25 +00:00
|
|
|
var retried bool
|
2021-05-21 20:31:41 +00:00
|
|
|
body, err := json.Marshal(prov)
|
2021-05-18 04:07:25 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, errs.Wrap(http.StatusInternalServerError, err, "error marshaling request")
|
|
|
|
}
|
2021-05-20 23:02:20 +00:00
|
|
|
u := c.endpoint.ResolveReference(&url.URL{Path: "/admin/provisioners"})
|
2021-05-18 04:07:25 +00:00
|
|
|
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
|
|
|
|
}
|
2021-05-20 23:02:20 +00:00
|
|
|
return nil, readAdminError(resp.Body)
|
2021-05-18 04:07:25 +00:00
|
|
|
}
|
2021-05-21 20:31:41 +00:00
|
|
|
var nuProv = new(mgmt.Provisioner)
|
|
|
|
if err := readJSON(resp.Body, nuProv); err != nil {
|
2021-05-18 04:07:25 +00:00
|
|
|
return nil, errors.Wrapf(err, "error reading %s", u)
|
|
|
|
}
|
2021-05-21 20:31:41 +00:00
|
|
|
return nuProv, nil
|
2021-05-18 04:07:25 +00:00
|
|
|
}
|
|
|
|
|
2021-05-20 23:02:20 +00:00
|
|
|
// UpdateProvisioner performs the PUT /admin/provisioners/{id} request to the CA.
|
|
|
|
func (c *AdminClient) UpdateProvisioner(id string, upr *mgmtAPI.UpdateProvisionerRequest) (*mgmt.Provisioner, error) {
|
2021-05-18 04:07:25 +00:00
|
|
|
var retried bool
|
|
|
|
body, err := json.Marshal(upr)
|
|
|
|
if err != nil {
|
|
|
|
return nil, errs.Wrap(http.StatusInternalServerError, err, "error marshaling request")
|
|
|
|
}
|
2021-05-20 23:02:20 +00:00
|
|
|
u := c.endpoint.ResolveReference(&url.URL{Path: path.Join("/admin/provisioners", id)})
|
2021-05-18 04:07:25 +00:00
|
|
|
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
|
|
|
|
}
|
2021-05-20 23:02:20 +00:00
|
|
|
return nil, readAdminError(resp.Body)
|
2021-05-18 04:07:25 +00:00
|
|
|
}
|
|
|
|
var prov = new(mgmt.Provisioner)
|
|
|
|
if err := readJSON(resp.Body, prov); err != nil {
|
|
|
|
return nil, errors.Wrapf(err, "error reading %s", u)
|
|
|
|
}
|
|
|
|
return prov, nil
|
|
|
|
}
|
|
|
|
|
2021-05-20 20:01:58 +00:00
|
|
|
// GetAuthConfig performs the GET /admin/authconfig/{id} request to the CA.
|
2021-05-20 23:02:20 +00:00
|
|
|
func (c *AdminClient) GetAuthConfig(id string) (*mgmt.AuthConfig, error) {
|
2021-05-18 04:07:25 +00:00
|
|
|
var retried bool
|
2021-05-20 20:01:58 +00:00
|
|
|
u := c.endpoint.ResolveReference(&url.URL{Path: path.Join("/admin/authconfig", id)})
|
2021-05-18 04:07:25 +00:00
|
|
|
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
|
|
|
|
}
|
2021-05-20 23:02:20 +00:00
|
|
|
return nil, readAdminError(resp.Body)
|
2021-05-18 04:07:25 +00:00
|
|
|
}
|
|
|
|
var ac = new(mgmt.AuthConfig)
|
|
|
|
if err := readJSON(resp.Body, ac); err != nil {
|
|
|
|
return nil, errors.Wrapf(err, "error reading %s", u)
|
|
|
|
}
|
|
|
|
return ac, nil
|
|
|
|
}
|
|
|
|
|
2021-05-20 23:02:20 +00:00
|
|
|
func readAdminError(r io.ReadCloser) error {
|
2021-05-18 04:07:25 +00:00
|
|
|
defer r.Close()
|
|
|
|
mgmtErr := new(mgmt.Error)
|
|
|
|
if err := json.NewDecoder(r).Decode(mgmtErr); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return errors.New(mgmtErr.Message)
|
|
|
|
}
|