first commit
parent
f84c8f846a
commit
7b5d6968a5
@ -0,0 +1,12 @@
|
||||
package authority
|
||||
|
||||
// Admin is the type definining Authority admins. Admins can update Authority
|
||||
// configuration, provisioners, and even other admins.
|
||||
type Admin struct {
|
||||
ID string `json:"-"`
|
||||
AuthorityID string `json:"-"`
|
||||
Name string `json:"name"`
|
||||
Provisioner string `json:"provisioner"`
|
||||
IsSuperAdmin bool `json:"isSuperAdmin"`
|
||||
IsDeleted bool `json:"isDeleted"`
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package authority
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
@ -0,0 +1,94 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
"go.step.sm/crypto/jose"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// SSHConfig contains the user and host keys.
|
||||
type SSHConfig struct {
|
||||
HostKey string `json:"hostKey"`
|
||||
UserKey string `json:"userKey"`
|
||||
Keys []*SSHPublicKey `json:"keys,omitempty"`
|
||||
AddUserPrincipal string `json:"addUserPrincipal,omitempty"`
|
||||
AddUserCommand string `json:"addUserCommand,omitempty"`
|
||||
Bastion *Bastion `json:"bastion,omitempty"`
|
||||
}
|
||||
|
||||
// Bastion contains the custom properties used on bastion.
|
||||
type Bastion struct {
|
||||
Hostname string `json:"hostname"`
|
||||
User string `json:"user,omitempty"`
|
||||
Port string `json:"port,omitempty"`
|
||||
Command string `json:"cmd,omitempty"`
|
||||
Flags string `json:"flags,omitempty"`
|
||||
}
|
||||
|
||||
// HostTag are tagged with k,v pairs. These tags are how a user is ultimately
|
||||
// associated with a host.
|
||||
type HostTag struct {
|
||||
ID string
|
||||
Name string
|
||||
Value string
|
||||
}
|
||||
|
||||
// Host defines expected attributes for an ssh host.
|
||||
type Host struct {
|
||||
HostID string `json:"hid"`
|
||||
HostTags []HostTag `json:"host_tags"`
|
||||
Hostname string `json:"hostname"`
|
||||
}
|
||||
|
||||
// Validate checks the fields in SSHConfig.
|
||||
func (c *SSHConfig) Validate() error {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
for _, k := range c.Keys {
|
||||
if err := k.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SSHPublicKey contains a public key used by federated CAs to keep old signing
|
||||
// keys for this ca.
|
||||
type SSHPublicKey struct {
|
||||
Type string `json:"type"`
|
||||
Federated bool `json:"federated"`
|
||||
Key jose.JSONWebKey `json:"key"`
|
||||
publicKey ssh.PublicKey
|
||||
}
|
||||
|
||||
// Validate checks the fields in SSHPublicKey.
|
||||
func (k *SSHPublicKey) Validate() error {
|
||||
switch {
|
||||
case k.Type == "":
|
||||
return errors.New("type cannot be empty")
|
||||
case k.Type != provisioner.SSHHostCert && k.Type != provisioner.SSHUserCert:
|
||||
return errors.Errorf("invalid type %s, it must be user or host", k.Type)
|
||||
case !k.Key.IsPublic():
|
||||
return errors.New("invalid key type, it must be a public key")
|
||||
}
|
||||
|
||||
key, err := ssh.NewPublicKey(k.Key.Key)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error creating ssh key")
|
||||
}
|
||||
k.publicKey = key
|
||||
return nil
|
||||
}
|
||||
|
||||
// PublicKey returns the ssh public key.
|
||||
func (k *SSHPublicKey) PublicKey() ssh.PublicKey {
|
||||
return k.publicKey
|
||||
}
|
||||
|
||||
// SSHKeys represents the SSH User and Host public keys.
|
||||
type SSHKeys struct {
|
||||
UserKeys []ssh.PublicKey
|
||||
HostKeys []ssh.PublicKey
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package authority
|
||||
package config
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
@ -1,4 +1,4 @@
|
||||
package authority
|
||||
package config
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
@ -1,4 +1,4 @@
|
||||
package authority
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
@ -1,4 +1,4 @@
|
||||
package authority
|
||||
package config
|
||||
|
||||
import (
|
||||
"reflect"
|
@ -0,0 +1,112 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/smallstep/certificates/api"
|
||||
)
|
||||
|
||||
// CreateAdminRequest represents the body for a CreateAdmin request.
|
||||
type CreateAdminRequest struct {
|
||||
Name string `json:"name"`
|
||||
Provisioner string `json:"provisioner"`
|
||||
IsSuperAdmin bool `json:"isSuperAdmin"`
|
||||
}
|
||||
|
||||
// Validate validates a new-admin request body.
|
||||
func (car *CreateAdminRequest) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateAdminRequest represents the body for a UpdateAdmin request.
|
||||
type UpdateAdminRequest struct {
|
||||
Name string `json:"name"`
|
||||
Provisioner string `json:"provisioner"`
|
||||
IsSuperAdmin bool `json:"isSuperAdmin"`
|
||||
}
|
||||
|
||||
// Validate validates a new-admin request body.
|
||||
func (uar *UpdateAdminRequest) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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)
|
||||
return
|
||||
}
|
||||
api.JSON(w, prov)
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
api.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
api.JSON(w, admins)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
@ -0,0 +1,121 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/smallstep/certificates/api"
|
||||
"github.com/smallstep/certificates/authority"
|
||||
"github.com/smallstep/certificates/authority/config"
|
||||
)
|
||||
|
||||
// CreateAuthConfigRequest represents the body for a CreateAuthConfig request.
|
||||
type CreateAuthConfigRequest struct {
|
||||
ASN1DN *authority.ASN1DN `json:"asn1dn,omitempty"`
|
||||
Claims *config.Claims `json:"claims,omitempty"`
|
||||
DisableIssuedAtCheck bool `json:"disableIssuedAtCheck,omitempty"`
|
||||
Backdate string `json:"backdate,omitempty"`
|
||||
}
|
||||
|
||||
// Validate validates a CreateAuthConfig request body.
|
||||
func (car *CreateAuthConfigRequest) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateAuthConfigRequest represents the body for a UpdateAuthConfig request.
|
||||
type UpdateAuthConfigRequest struct {
|
||||
ASN1DN *authority.ASN1DN `json:"asn1dn"`
|
||||
Claims *config.Claims `json:"claims"`
|
||||
DisableIssuedAtCheck bool `json:"disableIssuedAtCheck,omitempty"`
|
||||
Backdate string `json:"backdate,omitempty"`
|
||||
}
|
||||
|
||||
// Validate validates a new-admin request body.
|
||||
func (uar *UpdateAuthConfigRequest) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAuthConfig returns the requested admin, or an error.
|
||||
func (h *Handler) GetAuthConfig(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
id := chi.URLParam(r, "id")
|
||||
|
||||
ac, err := h.db.GetAuthConfig(ctx, id)
|
||||
if err != nil {
|
||||
api.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
api.JSON(w, ac)
|
||||
}
|
||||
|
||||
// CreateAuthConfig creates a new admin.
|
||||
func (h *Handler) CreateAuthConfig(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
var body CreateAuthConfigRequest
|
||||
if err := ReadJSON(r.Body, &body); err != nil {
|
||||
api.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
if err := body.Validate(); err != nil {
|
||||
api.WriteError(w, err)
|
||||
}
|
||||
|
||||
ac := config.AuthConfig{
|
||||
Status: config.StatusActive,
|
||||
DisableIssuedAtCheck: body.DisableIssuedAtCheck,
|
||||
Backdate: "1m",
|
||||
}
|
||||
if body.ASN1DN != nil {
|
||||
ac.ASN1DN = body.ASN1DN
|
||||
}
|
||||
if body.Claims != nil {
|
||||
ac.Claims = body.Claims
|
||||
}
|
||||
if body.Backdate != "" {
|
||||
ac.Backdate = body.Backdate
|
||||
}
|
||||
if err := h.db.CreateAuthConfig(ctx, ac); err != nil {
|
||||
api.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
api.JSONStatus(w, ac, http.StatusCreated)
|
||||
}
|
||||
|
||||
// UpdateAuthConfig updates an existing AuthConfig.
|
||||
func (h *Handler) UpdateAuthConfig(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
id := chi.URLParam(r, "id")
|
||||
|
||||
var body UpdateAuthConfigRequest
|
||||
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 ac, err := h.db.GetAuthConfig(ctx, id); err != nil {
|
||||
api.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
ac.DisableIssuedAtCheck = body.DisableIssuedAtCheck
|
||||
ac.Status = body.Status
|
||||
if body.ASN1DN != nil {
|
||||
ac.ASN1DN = body.ASN1DN
|
||||
}
|
||||
if body.Claims != nil {
|
||||
ac.Claims = body.Claims
|
||||
}
|
||||
if body.Backdate != "" {
|
||||
ac.Backdate = body.Backdate
|
||||
}
|
||||
|
||||
if err := h.db.UpdateAuthConfig(ctx, ac); err != nil {
|
||||
api.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
api.JSON(w, ac)
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/smallstep/certificates/api"
|
||||
"github.com/smallstep/certificates/authority/config"
|
||||
)
|
||||
|
||||
// Clock that returns time in UTC rounded to seconds.
|
||||
type Clock struct{}
|
||||
|
||||
// Now returns the UTC time rounded to seconds.
|
||||
func (c *Clock) Now() time.Time {
|
||||
return time.Now().UTC().Truncate(time.Second)
|
||||
}
|
||||
|
||||
var clock Clock
|
||||
|
||||
// Handler is the ACME API request handler.
|
||||
type Handler struct {
|
||||
db config.DB
|
||||
}
|
||||
|
||||
// NewHandler returns a new Authority Config Handler.
|
||||
func NewHandler(db config.DB) api.RouterHandler {
|
||||
return &Handler{
|
||||
db: ops.DB,
|
||||
}
|
||||
}
|
||||
|
||||
// Route traffic and implement the Router interface.
|
||||
func (h *Handler) Route(r api.Router) {
|
||||
// Provisioners
|
||||
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)
|
||||
|
||||
// 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)
|
||||
|
||||
// AuthConfig
|
||||
r.MethodFunc("GET", "/authconfig/{id}", h.GetAuthConfig)
|
||||
r.MethodFunc("POST", "/authconfig", h.CreateAuthConfig)
|
||||
r.MethodFunc("PUT", "/authconfig/{id}", h.UpdateAuthConfig)
|
||||
}
|
@ -0,0 +1,122 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/smallstep/certificates/api"
|
||||
"github.com/smallstep/certificates/authority/config"
|
||||
)
|
||||
|
||||
// CreateProvisionerRequest represents the body for a CreateProvisioner request.
|
||||
type CreateProvisionerRequest struct {
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Claims *config.Claims `json:"claims"`
|
||||
Details interface{} `json:"details"`
|
||||
X509Template string `json:"x509Template"`
|
||||
SSHTemplate string `json:"sshTemplate"`
|
||||
}
|
||||
|
||||
// Validate validates a new-provisioner request body.
|
||||
func (car *CreateProvisionerRequest) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateProvisionerRequest represents the body for a UpdateProvisioner request.
|
||||
type UpdateProvisionerRequest struct {
|
||||
Claims *config.Claims `json:"claims"`
|
||||
Details interface{} `json:"details"`
|
||||
X509Template string `json:"x509Template"`
|
||||
SSHTemplate string `json:"sshTemplate"`
|
||||
}
|
||||
|
||||
// Validate validates a new-provisioner request body.
|
||||
func (uar *UpdateProvisionerRequest) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetProvisioner returns the requested provisioner, or an error.
|
||||
func (h *Handler) GetProvisioner(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
id := chi.URLParam(r, "id")
|
||||
|
||||
prov, err := h.db.GetProvisioner(ctx, id)
|
||||
if err != nil {
|
||||
api.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
api.JSON(w, prov)
|
||||
}
|
||||
|
||||
// GetProvisioners returns all provisioners associated with the authority.
|
||||
func (h *Handler) GetProvisioners(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
provs, err := h.db.GetProvisioners(ctx)
|
||||
if err != nil {
|
||||
api.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
api.JSON(w, provs)
|
||||
}
|
||||
|
||||
// CreateProvisioner creates a new prov.
|
||||
func (h *Handler) CreateProvisioner(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
var body CreateProvisionerRequest
|
||||
if err := ReadJSON(r.Body, &body); err != nil {
|
||||
api.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
if err := body.Validate(); err != nil {
|
||||
api.WriteError(w, err)
|
||||
}
|
||||
|
||||
prov := &config.Provisioner{
|
||||
Type: body.Type,
|
||||
Name: body.Name,
|
||||
Claims: body.Claims,
|
||||
Details: body.Details,
|
||||
X509Template: body.X509Template,
|
||||
SSHTemplate: body.SSHTemplate,
|
||||
}
|
||||
if err := h.db.CreateProvisioner(ctx, prov); err != nil {
|
||||
api.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
api.JSONStatus(w, prov, http.StatusCreated)
|
||||
}
|
||||
|
||||
// UpdateProvisioner updates an existing prov.
|
||||
func (h *Handler) UpdateProvisioner(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
id := chi.URLParam(r, "id")
|
||||
|
||||
var body UpdateProvisionerRequest
|
||||
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 prov, err := h.db.GetProvisioner(ctx, id); err != nil {
|
||||
api.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
prov.Claims = body.Claims
|
||||
prov.Details = body.Provisioner
|
||||
prov.X509Template = body.X509Template
|
||||
prov.SSHTemplate = body.SSHTemplate
|
||||
prov.Status = body.Status
|
||||
|
||||
if err := h.db.UpdateProvisioner(ctx, prov); err != nil {
|
||||
api.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
api.JSON(w, prov)
|
||||
}
|
@ -0,0 +1,357 @@
|
||||
package mgmt
|
||||
|
||||
import (
|
||||
"github.com/smallstep/certificates/authority/config"
|
||||
authority "github.com/smallstep/certificates/authority/config"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultAuthorityID = "00000000-0000-0000-0000-000000000000"
|
||||
)
|
||||
|
||||
// StatusType is the type for status.
|
||||
type StatusType int
|
||||
|
||||
const (
|
||||
// StatusActive active
|
||||
StatusActive StatusType = iota
|
||||
// StatusDeleted deleted
|
||||
StatusDeleted
|
||||
)
|
||||
|
||||
type Claims struct {
|
||||
*X509Claims `json:"x509Claims"`
|
||||
*SSHClaims `json:"sshClaims"`
|
||||
DisableRenewal *bool `json:"disableRenewal"`
|
||||
}
|
||||
|
||||
type X509Claims struct {
|
||||
Durations *Durations `json:"durations"`
|
||||
}
|
||||
|
||||
type SSHClaims struct {
|
||||
UserDuration *Durations `json:"userDurations"`
|
||||
HostDuration *Durations `json:"hostDuration"`
|
||||
}
|
||||
|
||||
type Durations struct {
|
||||
Min string `json:"min"`
|
||||
Max string `json:"max"`
|
||||
Default string `json:"default"`
|
||||
}
|
||||
|
||||
// Admin type.
|
||||
type Admin struct {
|
||||
ID string `json:"-"`
|
||||
AuthorityID string `json:"-"`
|
||||
Name string `json:"name"`
|
||||
Provisioner string `json:"provisioner"`
|
||||
IsSuperAdmin bool `json:"isSuperAdmin"`
|
||||
Status StatusType `json:"status"`
|
||||
}
|
||||
|
||||
// Provisioner type.
|
||||
type Provisioner struct {
|
||||
ID string `json:"-"`
|
||||
AuthorityID string `json:"-"`
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Claims *Claims `json:"claims"`
|
||||
Details interface{} `json:"details"`
|
||||
X509Template string `json:"x509Template"`
|
||||
SSHTemplate string `json:"sshTemplate"`
|
||||
Status StatusType `json:"status"`
|
||||
}
|
||||
|
||||
// AuthConfig represents the Authority Configuration.
|
||||
type AuthConfig struct {
|
||||
//*cas.Options `json:"cas"`
|
||||
ID string `json:"id"`
|
||||
ASN1DN *config.ASN1DN `json:"template,omitempty"`
|
||||
Provisioners []*Provisioner `json:"-"`
|
||||
Claims *Claims `json:"claims,omitempty"`
|
||||
DisableIssuedAtCheck bool `json:"disableIssuedAtCheck,omitempty"`
|
||||
Backdate string `json:"backdate,omitempty"`
|
||||
Status StatusType `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
func (ac *AuthConfig) ToCertificates() (*config.AuthConfig, error) {
|
||||
return &authority.AuthConfig{}, nil
|
||||
}
|
||||
|
||||
/*
|
||||
// ToCertificates converts the landlord provisioner type to the open source
|
||||
// provisioner type.
|
||||
func (p *Provisioner) ToCertificates(ctx context.Context, db database.DB) (provisioner.Interface, error) {
|
||||
claims, err := p.Claims.ToCertificates()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
details := p.Details.GetData()
|
||||
if details == nil {
|
||||
return nil, fmt.Errorf("provisioner does not have any details")
|
||||
}
|
||||
|
||||
options, err := p.getOptions(ctx, db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch d := details.(type) {
|
||||
case *ProvisionerDetails_JWK:
|
||||
k := d.JWK.GetKey()
|
||||
jwk := new(jose.JSONWebKey)
|
||||
if err := json.Unmarshal(k.Key.Public, &jwk); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &provisioner.JWK{
|
||||
Type: p.Type.String(),
|
||||
Name: p.Name,
|
||||
Key: jwk,
|
||||
EncryptedKey: string(k.Key.Private),
|
||||
Claims: claims,
|
||||
Options: options,
|
||||
}, nil
|
||||
case *ProvisionerDetails_OIDC:
|
||||
cfg := d.OIDC
|
||||
return &provisioner.OIDC{
|
||||
Type: p.Type.String(),
|
||||
Name: p.Name,
|
||||
TenantID: cfg.TenantId,
|
||||
ClientID: cfg.ClientId,
|
||||
ClientSecret: cfg.ClientSecret,
|
||||
ConfigurationEndpoint: cfg.ConfigurationEndpoint,
|
||||
Admins: cfg.Admins,
|
||||
Domains: cfg.Domains,
|
||||
Groups: cfg.Groups,
|
||||
ListenAddress: cfg.ListenAddress,
|
||||
Claims: claims,
|
||||
Options: options,
|
||||
}, nil
|
||||
case *ProvisionerDetails_GCP:
|
||||
cfg := d.GCP
|
||||
return &provisioner.GCP{
|
||||
Type: p.Type.String(),
|
||||
Name: p.Name,
|
||||
ServiceAccounts: cfg.ServiceAccounts,
|
||||
ProjectIDs: cfg.ProjectIds,
|
||||
DisableCustomSANs: cfg.DisableCustomSans,
|
||||
DisableTrustOnFirstUse: cfg.DisableTrustOnFirstUse,
|
||||
InstanceAge: durationValue(cfg.InstanceAge),
|
||||
Claims: claims,
|
||||
Options: options,
|
||||
}, nil
|
||||
case *ProvisionerDetails_AWS:
|
||||
cfg := d.AWS
|
||||
return &provisioner.AWS{
|
||||
Type: p.Type.String(),
|
||||
Name: p.Name,
|
||||
Accounts: cfg.Accounts,
|
||||
DisableCustomSANs: cfg.DisableCustomSans,
|
||||
DisableTrustOnFirstUse: cfg.DisableTrustOnFirstUse,
|
||||
InstanceAge: durationValue(cfg.InstanceAge),
|
||||
Claims: claims,
|
||||
Options: options,
|
||||
}, nil
|
||||
case *ProvisionerDetails_Azure:
|
||||
cfg := d.Azure
|
||||
return &provisioner.Azure{
|
||||
Type: p.Type.String(),
|
||||
Name: p.Name,
|
||||
TenantID: cfg.TenantId,
|
||||
ResourceGroups: cfg.ResourceGroups,
|
||||
Audience: cfg.Audience,
|
||||
DisableCustomSANs: cfg.DisableCustomSans,
|
||||
DisableTrustOnFirstUse: cfg.DisableTrustOnFirstUse,
|
||||
Claims: claims,
|
||||
Options: options,
|
||||
}, nil
|
||||
case *ProvisionerDetails_X5C:
|
||||
var roots []byte
|
||||
for i, k := range d.X5C.GetRoots() {
|
||||
if b := k.GetKey().GetPublic(); b != nil {
|
||||
if i > 0 {
|
||||
roots = append(roots, '\n')
|
||||
}
|
||||
roots = append(roots, b...)
|
||||
}
|
||||
}
|
||||
return &provisioner.X5C{
|
||||
Type: p.Type.String(),
|
||||
Name: p.Name,
|
||||
Roots: roots,
|
||||
Claims: claims,
|
||||
Options: options,
|
||||
}, nil
|
||||
case *ProvisionerDetails_K8SSA:
|
||||
var publicKeys []byte
|
||||
for i, k := range d.K8SSA.GetPublicKeys() {
|
||||
if b := k.GetKey().GetPublic(); b != nil {
|
||||
if i > 0 {
|
||||
publicKeys = append(publicKeys, '\n')
|
||||
}
|
||||
publicKeys = append(publicKeys, k.Key.Public...)
|
||||
}
|
||||
}
|
||||
return &provisioner.K8sSA{
|
||||
Type: p.Type.String(),
|
||||
Name: p.Name,
|
||||
PubKeys: publicKeys,
|
||||
Claims: claims,
|
||||
Options: options,
|
||||
}, nil
|
||||
case *ProvisionerDetails_SSHPOP:
|
||||
return &provisioner.SSHPOP{
|
||||
Type: p.Type.String(),
|
||||
Name: p.Name,
|
||||
Claims: claims,
|
||||
}, nil
|
||||
case *ProvisionerDetails_ACME:
|
||||
cfg := d.ACME
|
||||
return &provisioner.ACME{
|
||||
Type: p.Type.String(),
|
||||
Name: p.Name,
|
||||
ForceCN: cfg.ForceCn,
|
||||
Claims: claims,
|
||||
Options: options,
|
||||
}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("provisioner %s not implemented", p.Type.String())
|
||||
}
|
||||
}
|
||||
|
||||
// ToCertificates converts the landlord provisioner claims type to the open source
|
||||
// (step-ca) claims type.
|
||||
func (c *Claims) ToCertificates() (*provisioner.Claims, error) {
|
||||
x509, ssh := c.GetX509(), c.GetSsh()
|
||||
x509Durations := x509.GetDurations()
|
||||
hostDurations := ssh.GetHostDurations()
|
||||
userDurations := ssh.GetUserDurations()
|
||||
enableSSHCA := ssh.GetEnabled()
|
||||
return &provisioner.Claims{
|
||||
MinTLSDur: durationPtr(x509Durations.GetMin()),
|
||||
MaxTLSDur: durationPtr(x509Durations.GetMax()),
|
||||
DefaultTLSDur: durationPtr(x509Durations.GetDefault()),
|
||||
DisableRenewal: &c.DisableRenewal,
|
||||
MinUserSSHDur: durationPtr(userDurations.GetMin()),
|
||||
MaxUserSSHDur: durationPtr(userDurations.GetMax()),
|
||||
DefaultUserSSHDur: durationPtr(userDurations.GetDefault()),
|
||||
MinHostSSHDur: durationPtr(hostDurations.GetMin()),
|
||||
MaxHostSSHDur: durationPtr(hostDurations.GetMax()),
|
||||
DefaultHostSSHDur: durationPtr(hostDurations.GetDefault()),
|
||||
EnableSSHCA: &enableSSHCA,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func durationPtr(d *duration.Duration) *provisioner.Duration {
|
||||
if d == nil {
|
||||
return nil
|
||||
}
|
||||
return &provisioner.Duration{
|
||||
Duration: time.Duration(d.Seconds)*time.Second + time.Duration(d.Nanos)*time.Nanosecond,
|
||||
}
|
||||
}
|
||||
|
||||
func durationValue(d *duration.Duration) provisioner.Duration {
|
||||
if d == nil {
|
||||
return provisioner.Duration{}
|
||||
}
|
||||
return provisioner.Duration{
|
||||
Duration: time.Duration(d.Seconds)*time.Second + time.Duration(d.Nanos)*time.Nanosecond,
|
||||
}
|
||||
}
|
||||
|
||||
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 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)
|
||||
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)
|
||||
}
|
||||
*/
|
@ -0,0 +1,149 @@
|
||||
package mgmt
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// ErrNotFound is an error that should be used by the authority.DB interface to
|
||||
// indicate that an entity does not exist.
|
||||
var ErrNotFound = errors.New("not found")
|
||||
|
||||
// DB is the DB interface expected by the step-ca ACME API.
|
||||
type DB interface {
|
||||
CreateProvisioner(ctx context.Context, prov *Provisioner) error
|
||||
GetProvisioner(ctx context.Context, id string) (*Provisioner, error)
|
||||
GetProvisioners(ctx context.Context) ([]*Provisioner, error)
|
||||
UpdateProvisioner(ctx context.Context, prov *Provisioner) error
|
||||
|
||||
CreateAdmin(ctx context.Context, admin *Admin) error
|
||||
GetAdmin(ctx context.Context, id string) error
|
||||
GetAdmins(ctx context.Context) ([]*Admin, error)
|
||||
UpdateAdmin(ctx context.Context, admin *Admin) error
|
||||
|
||||
CreateAuthConfig(ctx context.Context, ac *AuthConfig) error
|
||||
GetAuthConfig(ctx context.Context, id string) (*AuthConfig, error)
|
||||
UpdateAuthConfig(ctx context.Context, ac *AuthConfig) error
|
||||
}
|
||||
|
||||
// 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)
|
||||
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)
|
||||
MockGetAdmins func(ctx context.Context) ([]*Admin, error)
|
||||
MockUpdateAdmin func(ctx context.Context, adm *Admin) error
|
||||
|
||||
MockCreateAuthConfig func(ctx context.Context, ac *AuthConfig) error
|
||||
MockGetAuthConfig func(ctx context.Context, id string) (*AuthConfig, error)
|
||||
MockUpdateAuthConfig func(ctx context.Context, ac *AuthConfig) error
|
||||
|
||||
MockError error
|
||||
MockRet1 interface{}
|
||||
}
|
||||
|
||||
// CreateProvisioner mock.
|
||||
func (m *MockDB) CreateProvisioner(ctx context.Context, prov *Provisioner) error {
|
||||
if m.MockCreateProvisioner != nil {
|
||||
return m.MockCreateProvisioner(ctx, prov)
|
||||
} else if m.MockError != nil {
|
||||
return m.MockError
|
||||
}
|
||||
return m.MockError
|
||||
}
|
||||
|
||||
// GetProvisioner mock.
|
||||
func (m *MockDB) GetProvisioner(ctx context.Context, id string) (*Provisioner, error) {
|
||||
if m.MockGetProvisioner != nil {
|
||||
return m.MockGetProvisioner(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 {
|
||||
return m.MockGetProvisioners(ctx)
|
||||
} else if m.MockError != nil {
|
||||
return nil, m.MockError
|
||||
}
|
||||
return m.MockRet1.([]*Provisioner), m.MockError
|
||||
}
|
||||
|
||||
// UpdateProvisioner mock
|
||||
func (m *MockDB) UpdateProvisioner(ctx context.Context, prov *Provisioner) error {
|
||||
if m.MockUpdateProvisioner != nil {
|
||||
return m.MockUpdateProvisioner(ctx, prov)
|
||||
}
|
||||
return m.MockError
|
||||
}
|
||||
|
||||
// CreateAdmin mock
|
||||
func (m *MockDB) CreateAdmin(ctx context.Context, admin *Admin) error {
|
||||
if m.MockCreateAdmin != nil {
|
||||
return m.MockCreateAdmin(ctx, admin)
|
||||
}
|
||||
return m.MockError
|
||||
}
|
||||
|
||||
// GetAdmin mock.
|
||||
func (m *MockDB) GetAdmin(ctx context.Context, id string) (*Admin, error) {
|
||||
if m.MockGetAdmin != nil {
|
||||
return m.MockGetAdmin(ctx, id)
|
||||
} else if m.MockError != nil {
|
||||
return nil, m.MockError
|
||||
}
|
||||
return m.MockRet1.(*Admin), m.MockError
|
||||
}
|
||||
|
||||
// GetAdmins mock
|
||||
func (m *MockDB) GetAdmins(ctx context.Context) ([]*Admin, error) {
|
||||
if m.MockGetAdmins != nil {
|
||||
return m.MockGetAdmins(ctx)
|
||||
} else if m.MockError != nil {
|
||||
return nil, m.MockError
|
||||
}
|
||||
return m.MockRet1.([]*Admin), m.MockError
|
||||
}
|
||||
|
||||
// UpdateAdmin mock
|
||||
func (m *MockDB) UpdateAdmin(ctx context.Context, adm *Admin) error {
|
||||
if m.UpdateAdmin != nil {
|
||||
return m.MockUpdateAdmin(ctx, adm)
|
||||
}
|
||||
return m.MockError
|
||||
}
|
||||
|
||||
// CreateAuthConfig mock
|
||||
func (m *MockDB) CreateAuthConfig(ctx context.Context, admin *AuthConfig) error {
|
||||
if m.MockCreateAuthConfig != nil {
|
||||
return m.MockCreateAuthConfig(ctx, admin)
|
||||
}
|
||||
return m.MockError
|
||||
}
|
||||
|
||||
// GetAuthConfig mock.
|
||||
func (m *MockDB) GetAuthConfig(ctx context.Context, id string) (*AuthConfig, error) {
|
||||
if m.MockGetAuthConfig != nil {
|
||||
return m.MockGetAuthConfig(ctx, id)
|
||||
} else if m.MockError != nil {
|
||||
return nil, m.MockError
|
||||
}
|
||||
return m.MockRet1.(*AuthConfig), m.MockError
|
||||
}
|
||||
|
||||
// UpdateAuthConfig mock
|
||||
func (m *MockDB) UpdateAuthConfig(ctx context.Context, adm *AuthConfig) error {
|
||||
if m.UpdateAuthConfig != nil {
|
||||
return m.MockUpdateAuthConfig(ctx, adm)
|
||||
}
|
||||
return m.MockError
|
||||
}
|
@ -0,0 +1,163 @@
|
||||
package nosql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/acme"
|
||||
"github.com/smallstep/certificates/authority/mgmt"
|
||||
"github.com/smallstep/nosql"
|
||||
)
|
||||
|
||||
// dbAdmin is the database representation of the Admin type.
|
||||
type dbAdmin struct {
|
||||
ID string `json:"id"`
|
||||
AuthorityID string `json:"authorityID"`
|
||||
Name string `json:"name"`
|
||||
Provisioner string `json:"provisioner"`
|
||||
IsSuperAdmin bool `json:"isSuperAdmin"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
DeletedAt time.Time `json:"deletedAt"`
|
||||
}
|
||||
|
||||
func (dbp *dbAdmin) clone() *dbAdmin {
|
||||
u := *dbp
|
||||
return &u
|
||||
}
|
||||
|
||||
func (db *DB) getDBAdminBytes(ctx context.Context, id string) ([]byte, error) {
|
||||
data, err := db.db.Get(authorityAdminsTable, []byte(id))
|
||||
if nosql.IsErrNotFound(err) {
|
||||
return nil, mgmt.NewError(mgmt.ErrorNotFoundType, "admin %s not found", id)
|
||||
} else if err != nil {
|
||||
return nil, errors.Wrapf(err, "error loading admin %s", id)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (db *DB) getDBAdmin(ctx context.Context, id string) (*dbAdmin, error) {
|
||||
data, err := db.getDBAdminBytes(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dba, err := unmarshalDBAdmin(data, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if dba.AuthorityID != db.authorityID {
|
||||
return nil, mgmt.NewError(mgmt.ErrorAuthorityMismatchType,
|
||||
"admin %s is not owned by authority %s", dba.ID, db.authorityID)
|
||||
}
|
||||
return dba, nil
|
||||
}
|
||||
|
||||
// GetAdmin retrieves and unmarshals a admin from the database.
|
||||
func (db *DB) GetAdmin(ctx context.Context, id string) (*mgmt.Admin, error) {
|
||||
data, err := db.getDBAdminBytes(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
adm, err := unmarshalAdmin(data, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if adm.Status == mgmt.StatusDeleted {
|
||||
return nil, mgmt.NewError(mgmt.ErrorDeletedType, "admin %s is deleted")
|
||||
}
|
||||
if adm.AuthorityID != db.authorityID {
|
||||
return nil, mgmt.NewError(mgmt.ErrorAuthorityMismatchType,
|
||||
"admin %s is not owned by authority %s", adm.ID, db.authorityID)
|
||||
}
|
||||
return adm, nil
|
||||
}
|
||||
|
||||
func unmarshalDBAdmin(data []byte, id string) (*dbAdmin, error) {
|
||||
var dba = new(dbAdmin)
|
||||
if err := json.Unmarshal(data, dba); err != nil {
|
||||
return nil, errors.Wrapf(err, "error unmarshaling admin %s into dbAdmin", id)
|
||||
}
|
||||
return dba, nil
|
||||
}
|
||||
|
||||
func unmarshalAdmin(data []byte, id string) (*mgmt.Admin, error) {
|
||||
var dba = new(dbAdmin)
|
||||
if err := json.Unmarshal(data, dba); err != nil {
|
||||
return nil, errors.Wrapf(err, "error unmarshaling admin %s into dbAdmin", id)
|
||||
}
|
||||
adm := &mgmt.Admin{
|
||||
ID: dba.ID,
|
||||
Name: dba.Name,
|
||||
Provisioner: dba.Provisioner,
|
||||
IsSuperAdmin: dba.IsSuperAdmin,
|
||||
}
|
||||
if !dba.DeletedAt.IsZero() {
|
||||
adm.Status = mgmt.StatusDeleted
|
||||
}
|
||||
return adm, nil
|
||||
}
|
||||
|
||||
// GetAdmins retrieves and unmarshals all active (not deleted) admins
|
||||
// from the database.
|
||||
// TODO should we be paginating?
|
||||
func (db *DB) GetAdmins(ctx context.Context, az *acme.Authorization) ([]*mgmt.Admin, error) {
|
||||
dbEntries, err := db.db.List(authorityAdminsTable)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error loading admins")
|
||||
}
|
||||
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 {
|
||||
continue
|
||||
}
|
||||
if adm.AuthorityID != db.authorityID {
|
||||
continue
|
||||
}
|
||||
admins = append(admins, adm)
|
||||
}
|
||||
return admins, nil
|
||||
}
|
||||
|
||||
// CreateAdmin stores a new admin to the database.
|
||||
func (db *DB) CreateAdmin(ctx context.Context, adm *mgmt.Admin) error {
|
||||
var err error
|
||||
adm.ID, err = randID()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error generating random id for admin")
|
||||
}
|
||||
|
||||
dba := &dbAdmin{
|
||||
ID: adm.ID,
|
||||
AuthorityID: db.authorityID,
|
||||
Name: adm.Name,
|
||||
Provisioner: adm.Provisioner,
|
||||
IsSuperAdmin: adm.IsSuperAdmin,
|
||||
CreatedAt: clock.Now(),
|
||||
}
|
||||
|
||||
return db.save(ctx, dba.ID, dba, nil, "admin", authorityAdminsTable)
|
||||
}
|
||||
|
||||
// UpdateAdmin saves an updated admin to the database.
|
||||
func (db *DB) UpdateAdmin(ctx context.Context, adm *mgmt.Admin) error {
|
||||
old, err := db.getDBAdmin(ctx, adm.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
nu := old.clone()
|
||||
|
||||
// If the admin was active but is now deleted ...
|
||||
if old.DeletedAt.IsZero() && adm.Status == mgmt.StatusDeleted {
|
||||
nu.DeletedAt = clock.Now()
|
||||
}
|
||||
nu.Provisioner = adm.Provisioner
|
||||
nu.IsSuperAdmin = adm.IsSuperAdmin
|
||||
|
||||
return db.save(ctx, old.ID, nu, old, "admin", authorityAdminsTable)
|
||||
}
|
@ -0,0 +1,113 @@
|
||||
package nosql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/authority/config"
|
||||
"github.com/smallstep/certificates/authority/mgmt"
|
||||
"github.com/smallstep/nosql"
|
||||
)
|
||||
|
||||
type dbAuthConfig struct {
|
||||
ID string `json:"id"`
|
||||
ASN1DN *config.ASN1DN `json:"asn1dn"`
|
||||
Claims *mgmt.Claims `json:"claims"`
|
||||
DisableIssuedAtCheck bool `json:"disableIssuedAtCheck,omitempty"`
|
||||
Backdate string `json:"backdate,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
DeletedAt time.Time `json:"deletedAt"`
|
||||
}
|
||||
|
||||
func (dbp *dbAuthConfig) clone() *dbAuthConfig {
|
||||
u := *dbp
|
||||
return &u
|
||||
}
|
||||
|
||||
func (db *DB) getDBAuthConfigBytes(ctx context.Context, id string) ([]byte, error) {
|
||||
data, err := db.db.Get(authorityConfigsTable, []byte(id))
|
||||
if nosql.IsErrNotFound(err) {
|
||||
return nil, mgmt.NewError(mgmt.ErrorNotFoundType, "authConfig %s not found", id)
|
||||
} else if err != nil {
|
||||
return nil, errors.Wrapf(err, "error loading authConfig %s", id)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (db *DB) getDBAuthConfig(ctx context.Context, id string) (*dbAuthConfig, error) {
|
||||
data, err := db.getDBAuthConfigBytes(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var dba = new(dbAuthConfig)
|
||||
if err = json.Unmarshal(data, dba); err != nil {
|
||||
return nil, errors.Wrapf(err, "error unmarshaling authority %s into dbAuthConfig", id)
|
||||
}
|
||||
|
||||
return dba, nil
|
||||
}
|
||||
|
||||
// GetAuthConfig retrieves an AuthConfig configuration from the DB.
|
||||
func (db *DB) GetAuthConfig(ctx context.Context, id string) (*mgmt.AuthConfig, error) {
|
||||
dba, err := db.getDBAuthConfig(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
provs, err := db.GetProvisioners(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &mgmt.AuthConfig{
|
||||
ID: dba.ID,
|
||||
Provisioners: provs,
|
||||
ASN1DN: dba.ASN1DN,
|
||||
Backdate: dba.Backdate,
|
||||
Claims: dba.Claims,
|
||||
DisableIssuedAtCheck: dba.DisableIssuedAtCheck,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CreateAuthConfig stores a new provisioner to the database.
|
||||
func (db *DB) CreateAuthConfig(ctx context.Context, ac *mgmt.AuthConfig) error {
|
||||
var err error
|
||||
ac.ID, err = randID()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error generating random id for provisioner")
|
||||
}
|
||||
|
||||
dba := &dbAuthConfig{
|
||||
ID: ac.ID,
|
||||
ASN1DN: ac.ASN1DN,
|
||||
Claims: ac.Claims,
|
||||
DisableIssuedAtCheck: ac.DisableIssuedAtCheck,
|
||||
Backdate: ac.Backdate,
|
||||
CreatedAt: clock.Now(),
|
||||
}
|
||||
|
||||
return db.save(ctx, dba.ID, dba, nil, "authConfig", authorityConfigsTable)
|
||||
}
|
||||
|
||||
// UpdateAuthConfig saves an updated provisioner to the database.
|
||||
func (db *DB) UpdateAuthConfig(ctx context.Context, ac *mgmt.AuthConfig) error {
|
||||
old, err := db.getDBAuthConfig(ctx, ac.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
nu := old.clone()
|
||||
|
||||
// If the authority was active but is now deleted ...
|
||||
if old.DeletedAt.IsZero() && ac.Status == mgmt.StatusDeleted {
|
||||
nu.DeletedAt = clock.Now()
|
||||
}
|
||||
nu.Claims = ac.Claims
|
||||
nu.DisableIssuedAtCheck = ac.DisableIssuedAtCheck
|
||||
nu.Backdate = ac.Backdate
|
||||
|
||||
return db.save(ctx, old.ID, nu, old, "authConfig", authorityProvisionersTable)
|
||||
}
|
@ -0,0 +1,91 @@
|
||||
package nosql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
nosqlDB "github.com/smallstep/nosql/database"
|
||||
"go.step.sm/crypto/randutil"
|
||||
)
|
||||
|
||||
var (
|
||||
authorityAdminsTable = []byte("authority_admins")
|
||||
authorityConfigsTable = []byte("authority_configs")
|
||||
authorityProvisionersTable = []byte("authority_provisioners")
|
||||
)
|
||||
|
||||
// DB is a struct that implements the AcmeDB interface.
|
||||
type DB struct {
|
||||
db nosqlDB.DB
|
||||
authorityID string
|
||||
}
|
||||
|
||||
// New configures and returns a new Authority DB backend implemented using a nosql DB.
|
||||
func New(db nosqlDB.DB, authorityID string) (*DB, error) {
|
||||
tables := [][]byte{authorityAdminsTable, authorityConfigsTable, authorityProvisionersTable}
|
||||
for _, b := range tables {
|
||||
if err := db.CreateTable(b); err != nil {
|
||||
return nil, errors.Wrapf(err, "error creating table %s",
|
||||
string(b))
|
||||
}
|
||||
}
|
||||
return &DB{db, authorityID}, nil
|
||||
}
|
||||
|
||||
// save writes the new data to the database, overwriting the old data if it
|
||||
// existed.
|
||||
func (db *DB) save(ctx context.Context, id string, nu interface{}, old interface{}, typ string, table []byte) error {
|
||||
var (
|
||||
err error
|
||||
newB []byte
|
||||
)
|
||||
if nu == nil {
|
||||
newB = nil
|
||||
} else {
|
||||
newB, err = json.Marshal(nu)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error marshaling authority type: %s, value: %v", typ, nu)
|
||||
}
|
||||
}
|
||||
var oldB []byte
|
||||
if old == nil {
|
||||
oldB = nil
|
||||
} else {
|
||||
oldB, err = json.Marshal(old)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error marshaling acme type: %s, value: %v", typ, old)
|
||||
}
|
||||
}
|
||||
|
||||
_, swapped, err := db.db.CmpAndSwap(table, []byte(id), oldB, newB)
|
||||
switch {
|
||||
case err != nil:
|
||||
return errors.Wrapf(err, "error saving authority %s", typ)
|
||||
case !swapped:
|
||||
return errors.Errorf("error saving authority %s; changed since last read", typ)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var idLen = 32
|
||||
|
||||
func randID() (val string, err error) {
|
||||
val, err = randutil.Alphanumeric(idLen)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "error generating random alphanumeric ID")
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
// Clock that returns time in UTC rounded to seconds.
|
||||
type Clock struct{}
|
||||
|
||||
// Now returns the UTC time rounded to seconds.
|
||||
func (c *Clock) Now() time.Time {
|
||||
return time.Now().UTC().Truncate(time.Second)
|
||||
}
|
||||
|
||||
var clock = new(Clock)
|
@ -0,0 +1,174 @@
|
||||
package nosql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/authority/mgmt"
|
||||
"github.com/smallstep/nosql"
|
||||
)
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
func (dbp *dbProvisioner) clone() *dbProvisioner {
|
||||
u := *dbp
|
||||
return &u
|
||||
}
|
||||
|
||||
func (db *DB) getDBProvisionerBytes(ctx context.Context, id string) ([]byte, error) {
|
||||
data, err := db.db.Get(authorityProvisionersTable, []byte(id))
|
||||
if nosql.IsErrNotFound(err) {
|
||||
return nil, mgmt.NewError(mgmt.ErrorNotFoundType, "provisioner %s not found", id)
|
||||
} else if err != nil {
|
||||
return nil, errors.Wrapf(err, "error loading provisioner %s", id)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (db *DB) getDBProvisioner(ctx context.Context, id string) (*dbProvisioner, error) {
|
||||
data, err := db.getDBProvisionerBytes(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dbp, err := unmarshalDBProvisioner(data, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if dbp.AuthorityID != db.authorityID {
|
||||
return nil, mgmt.NewError(mgmt.ErrorAuthorityMismatchType,
|
||||
"provisioner %s is not owned by authority %s", dbp.ID, 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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
prov, err := unmarshalProvisioner(data, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if prov.Status == mgmt.StatusDeleted {
|
||||
return nil, mgmt.NewError(mgmt.ErrorDeletedType, "provisioner %s is deleted", prov.ID)
|
||||
}
|
||||
if prov.AuthorityID != db.authorityID {
|
||||
return nil, mgmt.NewError(mgmt.ErrorAuthorityMismatchType,
|
||||
"provisioner %s is not owned by authority %s", prov.ID, db.authorityID)
|
||||
}
|
||||
return prov, nil
|
||||
}
|
||||
|
||||
func unmarshalDBProvisioner(data []byte, id string) (*dbProvisioner, error) {
|
||||
var dbp = new(dbProvisioner)
|
||||
if err := json.Unmarshal(data, dbp); err != nil {
|
||||
return nil, errors.Wrapf(err, "error unmarshaling provisioner %s into dbProvisioner", id)
|
||||
}
|
||||
return dbp, nil
|
||||
}
|
||||
|
||||
func unmarshalProvisioner(data []byte, id string) (*mgmt.Provisioner, error) {
|
||||
dbp, err := unmarshalDBProvisioner(data, id)
|
||||
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,
|
||||
}
|
||||
if !dbp.DeletedAt.IsZero() {
|
||||
prov.Status = mgmt.StatusDeleted
|
||||
}
|
||||
return prov, nil
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return nil, errors.Wrap(err, "error loading provisioners")
|
||||
}
|
||||
var provs []*mgmt.Provisioner
|
||||
for _, entry := range dbEntries {
|
||||
prov, err := unmarshalProvisioner(entry.Value, string(entry.Key))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if prov.Status == mgmt.StatusDeleted {
|
||||
continue
|
||||
}
|
||||
if prov.AuthorityID != db.authorityID {
|
||||
continue
|
||||
}
|
||||
provs = append(provs, prov)
|
||||
}
|
||||
return provs, nil
|
||||
}
|
||||
|
||||
// CreateProvisioner stores a new provisioner to the database.
|
||||
func (db *DB) CreateProvisioner(ctx context.Context, prov *mgmt.Provisioner) error {
|
||||
var err error
|
||||
prov.ID, err = randID()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error generating random id for provisioner")
|
||||
}
|
||||
|
||||
dbp := &dbProvisioner{
|
||||
ID: prov.ID,
|
||||
AuthorityID: db.authorityID,
|
||||
Type: prov.Type,
|
||||
Name: prov.Name,
|
||||
Claims: prov.Claims,
|
||||
Details: prov.Details,
|
||||
X509Template: prov.X509Template,
|
||||
SSHTemplate: prov.SSHTemplate,
|
||||
CreatedAt: clock.Now(),
|
||||
}
|
||||
|
||||
return db.save(ctx, dbp.ID, dbp, nil, "provisioner", authorityProvisionersTable)
|
||||
}
|
||||
|
||||
// UpdateProvisioner saves an updated provisioner to the database.
|
||||
func (db *DB) UpdateProvisioner(ctx context.Context, prov *mgmt.Provisioner) error {
|
||||
old, err := db.getDBProvisioner(ctx, prov.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
nu := old.clone()
|
||||
|
||||
// If the provisioner was active but is now deleted ...
|
||||
if old.DeletedAt.IsZero() && prov.Status == mgmt.StatusDeleted {
|
||||
nu.DeletedAt = clock.Now()
|
||||
}
|
||||
nu.Claims = prov.Claims
|
||||
nu.Details = prov.Details
|
||||
nu.X509Template = prov.X509Template
|
||||
nu.SSHTemplate = prov.SSHTemplate
|
||||
|
||||
return db.save(ctx, old.ID, nu, old, "provisioner", authorityProvisionersTable)
|
||||
}
|
@ -0,0 +1,191 @@
|
||||
package mgmt
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/errs"
|
||||
"github.com/smallstep/certificates/logging"
|
||||
)
|
||||
|
||||
// ProblemType is the type of the ACME problem.
|
||||
type ProblemType int
|
||||
|
||||
const (
|
||||
// ErrorNotFoundType resource not found.
|
||||
ErrorNotFoundType ProblemType = iota
|
||||
// ErrorAuthorityMismatchType resource Authority ID does not match the
|
||||
// context Authority ID.
|
||||
ErrorAuthorityMismatchType
|
||||
// ErrorDeletedType resource has been deleted.
|
||||
ErrorDeletedType
|
||||
// ErrorServerInternalType internal server error.
|
||||
ErrorServerInternalType
|
||||
)
|
||||
|
||||
// String returns the string representation of the acme problem type,
|
||||
// fulfilling the Stringer interface.
|
||||
func (ap ProblemType) String() string {
|
||||
switch ap {
|
||||
case ErrorNotFoundType:
|
||||
return "notFound"
|
||||
case ErrorAuthorityMismatchType:
|
||||
return "authorityMismatch"
|
||||
case ErrorDeletedType:
|
||||
return "deleted"
|
||||
case ErrorServerInternalType:
|
||||
return "internalServerError"
|
||||
default:
|
||||
return fmt.Sprintf("unsupported error type '%d'", int(ap))
|
||||
}
|
||||
}
|
||||
|
||||
type errorMetadata struct {
|
||||
details string
|
||||
status int
|
||||
typ string
|
||||
String string
|
||||
}
|
||||
|
||||
var (
|
||||
errorServerInternalMetadata = errorMetadata{
|
||||
typ: ErrorServerInternalType.String(),
|
||||
details: "the server experienced an internal error",
|
||||
status: 500,
|
||||
}
|
||||
errorMap = map[ProblemType]errorMetadata{
|
||||
ErrorNotFoundType: {
|
||||
typ: ErrorNotFoundType.String(),
|
||||
details: "resource not found",
|
||||
status: 400,
|
||||
},
|
||||
ErrorAuthorityMismatchType: {
|
||||
typ: ErrorAuthorityMismatchType.String(),
|
||||
details: "resource not owned by authority",
|
||||
status: 401,
|
||||
},
|
||||
ErrorDeletedType: {
|
||||
typ: ErrorNotFoundType.String(),
|
||||
details: "resource is deleted",
|
||||
status: 403,
|
||||
},
|
||||
ErrorServerInternalType: errorServerInternalMetadata,
|
||||
}
|
||||
)
|
||||
|
||||
// Error represents an ACME
|
||||
type Error struct {
|
||||
Type string `json:"type"`
|
||||
Detail string `json:"detail"`
|
||||
Subproblems []interface{} `json:"subproblems,omitempty"`
|
||||
Identifier interface{} `json:"identifier,omitempty"`
|
||||
Err error `json:"-"`
|
||||
Status int `json:"-"`
|
||||
}
|
||||
|
||||
// NewError creates a new Error type.
|
||||
func NewError(pt ProblemType, msg string, args ...interface{}) *Error {
|
||||
return newError(pt, errors.Errorf(msg, args...))
|
||||
}
|
||||
|
||||
func newError(pt ProblemType, err error) *Error {
|
||||
meta, ok := errorMap[pt]
|
||||
if !ok {
|
||||
meta = errorServerInternalMetadata
|
||||
return &Error{
|
||||
Type: meta.typ,
|
||||
Detail: meta.details,
|
||||
Status: meta.status,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
return &Error{
|
||||
Type: meta.typ,
|
||||
Detail: meta.details,
|
||||
Status: meta.status,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
// NewErrorISE creates a new ErrorServerInternalType Error.
|
||||
func NewErrorISE(msg string, args ...interface{}) *Error {
|
||||
return NewError(ErrorServerInternalType, msg, args...)
|
||||
}
|
||||
|
||||
// WrapError attempts to wrap the internal error.
|
||||
func WrapError(typ ProblemType, err error, msg string, args ...interface{}) *Error {
|
||||
switch e := err.(type) {
|
||||
case nil:
|
||||
return nil
|
||||
case *Error:
|
||||
if e.Err == nil {
|
||||
e.Err = errors.Errorf(msg+"; "+e.Detail, args...)
|
||||
} else {
|
||||
e.Err = errors.Wrapf(e.Err, msg, args...)
|
||||
}
|
||||
return e
|
||||
default:
|
||||
return newError(typ, errors.Wrapf(err, msg, args...))
|
||||
}
|
||||
}
|
||||
|
||||
// WrapErrorISE shortcut to wrap an internal server error type.
|
||||
func WrapErrorISE(err error, msg string, args ...interface{}) *Error {
|
||||
return WrapError(ErrorServerInternalType, err, msg, args...)
|
||||
}
|
||||
|
||||
// StatusCode returns the status code and implements the StatusCoder interface.
|
||||
func (e *Error) StatusCode() int {
|
||||
return e.Status
|
||||
}
|
||||
|
||||
// Error allows AError to implement the error interface.
|
||||
func (e *Error) Error() string {
|
||||
return e.Detail
|
||||
}
|
||||
|
||||
// Cause returns the internal error and implements the Causer interface.
|
||||
func (e *Error) Cause() error {
|
||||
if e.Err == nil {
|
||||
return errors.New(e.Detail)
|
||||
}
|
||||
return e.Err
|
||||
}
|
||||
|
||||
// ToLog implements the EnableLogger interface.
|
||||
func (e *Error) ToLog() (interface{}, error) {
|
||||
b, err := json.Marshal(e)
|
||||
if err != nil {
|
||||
return nil, WrapErrorISE(err, "error marshaling authority.Error for logging")
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
// WriteError writes to w a JSON representation of the given error.
|
||||
func WriteError(w http.ResponseWriter, err *Error) {
|
||||
w.Header().Set("Content-Type", "application/problem+json")
|
||||
w.WriteHeader(err.StatusCode())
|
||||
|
||||
// Write errors in the response writer
|
||||
if rl, ok := w.(logging.ResponseLogger); ok {
|
||||
rl.WithFields(map[string]interface{}{
|
||||
"error": err.Err,
|
||||
})
|
||||
if os.Getenv("STEPDEBUG") == "1" {
|
||||
if e, ok := err.Err.(errs.StackTracer); ok {
|
||||
rl.WithFields(map[string]interface{}{
|
||||
"stack-trace": fmt.Sprintf("%+v", e),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(err); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
@ -0,0 +1,107 @@
|
||||
package ca
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/authority/mgmt"
|
||||
)
|
||||
|
||||
// MgmtClient implements an HTTP client for the CA server.
|
||||
type MgmtClient struct {
|
||||
client *uaClient
|
||||
endpoint *url.URL
|
||||
retryFunc RetryFunc
|
||||
opts []ClientOption
|
||||
}
|
||||
|
||||
// NewMgmtClient creates a new MgmtClient with the given endpoint and options.
|
||||
func NewMgmtClient(endpoint string, opts ...ClientOption) (*MgmtClient, error) {
|
||||
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
|
||||
}
|
||||
|
||||
return &MgmtClient{
|
||||
client: newClient(tr),
|
||||
endpoint: u,
|
||||
retryFunc: o.retryFunc,
|
||||
opts: opts,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *MgmtClient) retryOnError(r *http.Response) bool {
|
||||
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
|
||||
}
|
||||
|
||||
// GetAdmin performs the GET /config/admin/{id} request to the CA.
|
||||
func (c *MgmtClient) GetAdmin(id string) (*mgmt.Admin, error) {
|
||||
var retried bool
|
||||
u := c.endpoint.ResolveReference(&url.URL{Path: path.Join("/config/admin", id)})
|
||||
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 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 /config/admins request to the CA.
|
||||
func (c *MgmtClient) GetAdmins() ([]*mgmt.Admin, error) {
|
||||
var retried bool
|
||||
u := c.endpoint.ResolveReference(&url.URL{Path: "/config/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, readError(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
|
||||
}
|
Loading…
Reference in New Issue