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 (
|
import (
|
||||||
"fmt"
|
"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 (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
@ -1,4 +1,4 @@
|
|||||||
package authority
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
@ -1,4 +1,4 @@
|
|||||||
package authority
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
@ -1,4 +1,4 @@
|
|||||||
package authority
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"reflect"
|
"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