Add API implementation for authority and provisioner policy

This commit is contained in:
Herman Slatman 2022-03-15 15:51:45 +01:00
parent 3ec9a7310c
commit 81b0c6c37c
No known key found for this signature in database
GPG key ID: F4D8A44EA0A75A4F
19 changed files with 883 additions and 43 deletions

View file

@ -66,6 +66,13 @@ func JSONStatus(w http.ResponseWriter, v interface{}, status int) {
LogEnabledResponse(w, v)
}
// JSONNotFound writes a HTTP Not Found response with empty body.
func JSONNotFound(w http.ResponseWriter) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusNotFound)
LogEnabledResponse(w, nil)
}
// ProtoJSON writes the passed value into the http.ResponseWriter.
func ProtoJSON(w http.ResponseWriter, m proto.Message) {
ProtoJSONStatus(w, m, http.StatusOK)

View file

@ -25,6 +25,10 @@ type adminAuthority interface {
LoadProvisionerByID(id string) (provisioner.Interface, error)
UpdateProvisioner(ctx context.Context, nu *linkedca.Provisioner) error
RemoveProvisioner(ctx context.Context, id string) error
GetAuthorityPolicy(ctx context.Context) (*linkedca.Policy, error)
StoreAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error
UpdateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error
RemoveAuthorityPolicy(ctx context.Context) error
}
// CreateAdminRequest represents the body for a CreateAdmin request.

View file

@ -37,6 +37,11 @@ type mockAdminAuthority struct {
MockLoadProvisionerByID func(id string) (provisioner.Interface, error)
MockUpdateProvisioner func(ctx context.Context, nu *linkedca.Provisioner) error
MockRemoveProvisioner func(ctx context.Context, id string) error
MockGetAuthorityPolicy func(ctx context.Context) (*linkedca.Policy, error)
MockStoreAuthorityPolicy func(ctx context.Context, policy *linkedca.Policy) error
MockUpdateAuthorityPolicy func(ctx context.Context, policy *linkedca.Policy) error
MockRemoveAuthorityPolicy func(ctx context.Context) error
}
func (m *mockAdminAuthority) IsAdminAPIEnabled() bool {
@ -130,6 +135,22 @@ func (m *mockAdminAuthority) RemoveProvisioner(ctx context.Context, id string) e
return m.MockErr
}
func (m *mockAdminAuthority) GetAuthorityPolicy(ctx context.Context) (*linkedca.Policy, error) {
return nil, errors.New("not implemented yet")
}
func (m *mockAdminAuthority) StoreAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error {
return errors.New("not implemented yet")
}
func (m *mockAdminAuthority) UpdateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error {
return errors.New("not implemented yet")
}
func (m *mockAdminAuthority) RemoveAuthorityPolicy(ctx context.Context) error {
return errors.New("not implemented yet")
}
func TestCreateAdminRequest_Validate(t *testing.T) {
type fields struct {
Subject string

View file

@ -8,32 +8,44 @@ import (
// Handler is the Admin API request handler.
type Handler struct {
adminDB admin.DB
auth adminAuthority
acmeDB acme.DB
acmeResponder acmeAdminResponderInterface
adminDB admin.DB
auth adminAuthority
acmeDB acme.DB
acmeResponder acmeAdminResponderInterface
policyResponder policyAdminResponderInterface
}
// NewHandler returns a new Authority Config Handler.
func NewHandler(auth adminAuthority, adminDB admin.DB, acmeDB acme.DB, acmeResponder acmeAdminResponderInterface) api.RouterHandler {
func NewHandler(auth adminAuthority, adminDB admin.DB, acmeDB acme.DB, acmeResponder acmeAdminResponderInterface, policyResponder policyAdminResponderInterface) api.RouterHandler {
return &Handler{
auth: auth,
adminDB: adminDB,
acmeDB: acmeDB,
acmeResponder: acmeResponder,
auth: auth,
adminDB: adminDB,
acmeDB: acmeDB,
acmeResponder: acmeResponder,
policyResponder: policyResponder,
}
}
// Route traffic and implement the Router interface.
func (h *Handler) Route(r api.Router) {
authnz := func(next nextHTTP) nextHTTP {
return h.extractAuthorizeTokenAdmin(h.requireAPIEnabled(next))
//return h.extractAuthorizeTokenAdmin(h.requireAPIEnabled(next))
return h.requireAPIEnabled(next) // TODO(hs): remove this; temporarily no auth checks for simple testing...
}
requireEABEnabled := func(next nextHTTP) nextHTTP {
return h.requireEABEnabled(next)
}
enabledInStandalone := func(next nextHTTP) nextHTTP {
return h.checkAction(next, true)
}
disabledInStandalone := func(next nextHTTP) nextHTTP {
return h.checkAction(next, false)
}
// Provisioners
r.MethodFunc("GET", "/provisioners/{name}", authnz(h.GetProvisioner))
r.MethodFunc("GET", "/provisioners", authnz(h.GetProvisioners))
@ -53,4 +65,24 @@ func (h *Handler) Route(r api.Router) {
r.MethodFunc("GET", "/acme/eab/{provisionerName}", authnz(requireEABEnabled(h.acmeResponder.GetExternalAccountKeys)))
r.MethodFunc("POST", "/acme/eab/{provisionerName}", authnz(requireEABEnabled(h.acmeResponder.CreateExternalAccountKey)))
r.MethodFunc("DELETE", "/acme/eab/{provisionerName}/{id}", authnz(requireEABEnabled(h.acmeResponder.DeleteExternalAccountKey)))
// Policy - Authority
r.MethodFunc("GET", "/policy", authnz(enabledInStandalone(h.policyResponder.GetAuthorityPolicy)))
r.MethodFunc("POST", "/policy", authnz(enabledInStandalone(h.policyResponder.CreateAuthorityPolicy)))
r.MethodFunc("PUT", "/policy", authnz(enabledInStandalone(h.policyResponder.UpdateAuthorityPolicy)))
r.MethodFunc("DELETE", "/policy", authnz(enabledInStandalone(h.policyResponder.DeleteAuthorityPolicy)))
// Policy - Provisioner
//r.MethodFunc("GET", "/provisioners/{name}/policy", noauth(h.policyResponder.GetProvisionerPolicy))
r.MethodFunc("GET", "/provisioners/{name}/policy", authnz(disabledInStandalone(h.policyResponder.GetProvisionerPolicy)))
r.MethodFunc("POST", "/provisioners/{name}/policy", authnz(disabledInStandalone(h.policyResponder.CreateProvisionerPolicy)))
r.MethodFunc("PUT", "/provisioners/{name}/policy", authnz(disabledInStandalone(h.policyResponder.UpdateProvisionerPolicy)))
r.MethodFunc("DELETE", "/provisioners/{name}/policy", authnz(disabledInStandalone(h.policyResponder.DeleteProvisionerPolicy)))
// Policy - ACME Account
// TODO: ensure we don't clash with eab; might want to change eab paths slightly (as long as we don't have it released completely; needs changes in adminClient too)
r.MethodFunc("GET", "/acme/{provisionerName}/{accountID}/policy", authnz(disabledInStandalone(h.policyResponder.GetACMEAccountPolicy)))
r.MethodFunc("POST", "/acme/{provisionerName}/{accountID}/policy", authnz(disabledInStandalone(h.policyResponder.CreateACMEAccountPolicy)))
r.MethodFunc("PUT", "/acme/{provisionerName}/{accountID}/policy", authnz(disabledInStandalone(h.policyResponder.UpdateACMEAccountPolicy)))
r.MethodFunc("DELETE", "/acme/{provisionerName}/{accountID}/policy", authnz(disabledInStandalone(h.policyResponder.DeleteACMEAccountPolicy)))
}

View file

@ -6,6 +6,7 @@ import (
"github.com/smallstep/certificates/api"
"github.com/smallstep/certificates/authority/admin"
"github.com/smallstep/certificates/authority/admin/db/nosql"
)
type nextHTTP = func(http.ResponseWriter, *http.Request)
@ -44,6 +45,28 @@ func (h *Handler) extractAuthorizeTokenAdmin(next nextHTTP) nextHTTP {
}
}
// checkAction checks if an action is supported in standalone or not
func (h *Handler) checkAction(next nextHTTP, supportedInStandalone bool) nextHTTP {
return func(w http.ResponseWriter, r *http.Request) {
// actions allowed in standalone mode are always allowed
if supportedInStandalone {
next(w, r)
return
}
// when in standalone mode, actions are not supported
if _, ok := h.adminDB.(*nosql.DB); ok {
api.WriteError(w, admin.NewError(admin.ErrorNotImplementedType,
"operation not supported in standalone mode"))
return
}
// continue to next http handler
next(w, r)
}
}
// ContextKey is the key type for storing and searching for ACME request
// essentials in the context of a request.
type ContextKey string

View file

@ -0,0 +1,313 @@
package api
import (
"net/http"
"github.com/go-chi/chi"
"github.com/smallstep/certificates/api"
"github.com/smallstep/certificates/authority/admin"
"github.com/smallstep/certificates/authority/provisioner"
"go.step.sm/linkedca"
)
type policyAdminResponderInterface interface {
GetAuthorityPolicy(w http.ResponseWriter, r *http.Request)
CreateAuthorityPolicy(w http.ResponseWriter, r *http.Request)
UpdateAuthorityPolicy(w http.ResponseWriter, r *http.Request)
DeleteAuthorityPolicy(w http.ResponseWriter, r *http.Request)
GetProvisionerPolicy(w http.ResponseWriter, r *http.Request)
CreateProvisionerPolicy(w http.ResponseWriter, r *http.Request)
UpdateProvisionerPolicy(w http.ResponseWriter, r *http.Request)
DeleteProvisionerPolicy(w http.ResponseWriter, r *http.Request)
GetACMEAccountPolicy(w http.ResponseWriter, r *http.Request)
CreateACMEAccountPolicy(w http.ResponseWriter, r *http.Request)
UpdateACMEAccountPolicy(w http.ResponseWriter, r *http.Request)
DeleteACMEAccountPolicy(w http.ResponseWriter, r *http.Request)
}
// PolicyAdminResponder is responsible for writing ACME admin responses
type PolicyAdminResponder struct {
auth adminAuthority
adminDB admin.DB
}
// NewACMEAdminResponder returns a new ACMEAdminResponder
func NewPolicyAdminResponder(auth adminAuthority, adminDB admin.DB) *PolicyAdminResponder {
return &PolicyAdminResponder{
auth: auth,
adminDB: adminDB,
}
}
// GetAuthorityPolicy handles the GET /admin/authority/policy request
func (par *PolicyAdminResponder) GetAuthorityPolicy(w http.ResponseWriter, r *http.Request) {
policy, err := par.auth.GetAuthorityPolicy(r.Context())
if ae, ok := err.(*admin.Error); ok {
if !ae.IsType(admin.ErrorNotFoundType) {
api.WriteError(w, admin.WrapErrorISE(ae, "error retrieving authority policy"))
return
}
}
if policy == nil {
api.JSONNotFound(w)
return
}
api.ProtoJSONStatus(w, policy, http.StatusOK)
}
// CreateAuthorityPolicy handles the POST /admin/authority/policy request
func (par *PolicyAdminResponder) CreateAuthorityPolicy(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
policy, err := par.auth.GetAuthorityPolicy(ctx)
shouldWriteError := false
if ae, ok := err.(*admin.Error); ok {
shouldWriteError = !ae.IsType(admin.ErrorNotFoundType)
}
if shouldWriteError {
api.WriteError(w, admin.WrapErrorISE(err, "error retrieving authority policy"))
return
}
if policy != nil {
adminErr := admin.NewError(admin.ErrorBadRequestType, "authority already has a policy")
adminErr.Status = http.StatusConflict
api.WriteError(w, adminErr)
return
}
var newPolicy = new(linkedca.Policy)
if err := api.ReadProtoJSON(r.Body, newPolicy); err != nil {
api.WriteError(w, err)
return
}
if err := par.auth.StoreAuthorityPolicy(ctx, newPolicy); err != nil {
api.WriteError(w, admin.WrapErrorISE(err, "error storing authority policy"))
return
}
storedPolicy, err := par.auth.GetAuthorityPolicy(ctx)
if err != nil {
api.WriteError(w, admin.WrapErrorISE(err, "error retrieving authority policy after updating"))
return
}
api.JSONStatus(w, storedPolicy, http.StatusCreated)
}
// UpdateAuthorityPolicy handles the PUT /admin/authority/policy request
func (par *PolicyAdminResponder) UpdateAuthorityPolicy(w http.ResponseWriter, r *http.Request) {
var policy = new(linkedca.Policy)
if err := api.ReadProtoJSON(r.Body, policy); err != nil {
api.WriteError(w, err)
return
}
ctx := r.Context()
if err := par.auth.UpdateAuthorityPolicy(ctx, policy); err != nil {
api.WriteError(w, admin.WrapErrorISE(err, "error updating authority policy"))
return
}
newPolicy, err := par.auth.GetAuthorityPolicy(ctx)
if err != nil {
api.WriteError(w, admin.WrapErrorISE(err, "error retrieving authority policy after updating"))
return
}
api.ProtoJSONStatus(w, newPolicy, http.StatusOK)
}
// DeleteAuthorityPolicy handles the DELETE /admin/authority/policy request
func (par *PolicyAdminResponder) DeleteAuthorityPolicy(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
policy, err := par.auth.GetAuthorityPolicy(ctx)
if ae, ok := err.(*admin.Error); ok {
if !ae.IsType(admin.ErrorNotFoundType) {
api.WriteError(w, admin.WrapErrorISE(ae, "error retrieving authority policy"))
return
}
}
if policy == nil {
api.JSONNotFound(w)
return
}
err = par.auth.RemoveAuthorityPolicy(ctx)
if err != nil {
api.WriteError(w, admin.WrapErrorISE(err, "error deleting authority policy"))
return
}
api.JSONStatus(w, DeleteResponse{Status: "ok"}, http.StatusOK)
}
// GetProvisionerPolicy handles the GET /admin/provisioners/{name}/policy request
func (par *PolicyAdminResponder) GetProvisionerPolicy(w http.ResponseWriter, r *http.Request) {
// TODO: move getting provisioner to middleware?
ctx := r.Context()
name := chi.URLParam(r, "name")
var (
p provisioner.Interface
err error
)
if p, err = par.auth.LoadProvisionerByName(name); err != nil {
api.WriteError(w, admin.WrapErrorISE(err, "error loading provisioner %s", name))
return
}
prov, err := par.adminDB.GetProvisioner(ctx, p.GetID())
if err != nil {
api.WriteError(w, err)
return
}
policy := prov.GetPolicy()
if policy == nil {
api.JSONNotFound(w)
return
}
api.ProtoJSONStatus(w, policy, http.StatusOK)
}
// CreateProvisionerPolicy handles the POST /admin/provisioners/{name}/policy request
func (par *PolicyAdminResponder) CreateProvisionerPolicy(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
name := chi.URLParam(r, "name")
var (
p provisioner.Interface
err error
)
if p, err = par.auth.LoadProvisionerByName(name); err != nil {
api.WriteError(w, admin.WrapErrorISE(err, "error loading provisioner %s", name))
return
}
prov, err := par.adminDB.GetProvisioner(ctx, p.GetID())
if err != nil {
api.WriteError(w, err)
return
}
policy := prov.GetPolicy()
if policy != nil {
adminErr := admin.NewError(admin.ErrorBadRequestType, "provisioner %s already has a policy", name)
adminErr.Status = http.StatusConflict
api.WriteError(w, adminErr)
}
var newPolicy = new(linkedca.Policy)
if err := api.ReadProtoJSON(r.Body, newPolicy); err != nil {
api.WriteError(w, err)
return
}
prov.Policy = newPolicy
err = par.auth.UpdateProvisioner(ctx, prov)
if err != nil {
api.WriteError(w, err)
return
}
api.ProtoJSONStatus(w, newPolicy, http.StatusCreated)
}
// UpdateProvisionerPolicy handles the PUT /admin/provisioners/{name}/policy request
func (par *PolicyAdminResponder) UpdateProvisionerPolicy(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
name := chi.URLParam(r, "name")
var (
p provisioner.Interface
err error
)
if p, err = par.auth.LoadProvisionerByName(name); err != nil {
api.WriteError(w, admin.WrapErrorISE(err, "error loading provisioner %s", name))
return
}
prov, err := par.adminDB.GetProvisioner(ctx, p.GetID())
if err != nil {
api.WriteError(w, err)
return
}
var policy = new(linkedca.Policy)
if err := api.ReadProtoJSON(r.Body, policy); err != nil {
api.WriteError(w, err)
return
}
prov.Policy = policy
err = par.auth.UpdateProvisioner(ctx, prov)
if err != nil {
api.WriteError(w, err)
return
}
api.ProtoJSONStatus(w, policy, http.StatusOK)
}
// DeleteProvisionerPolicy ...
func (par *PolicyAdminResponder) DeleteProvisionerPolicy(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
name := chi.URLParam(r, "name")
var (
p provisioner.Interface
err error
)
if p, err = par.auth.LoadProvisionerByName(name); err != nil {
api.WriteError(w, admin.WrapErrorISE(err, "error loading provisioner %s", name))
return
}
prov, err := par.adminDB.GetProvisioner(ctx, p.GetID())
if err != nil {
api.WriteError(w, err)
return
}
if prov.Policy == nil {
api.JSONNotFound(w)
return
}
// remove the policy
prov.Policy = nil
err = par.auth.UpdateProvisioner(ctx, prov)
if err != nil {
api.WriteError(w, err)
return
}
api.JSON(w, &DeleteResponse{Status: "ok"})
}
// GetACMEAccountPolicy ...
func (par *PolicyAdminResponder) GetACMEAccountPolicy(w http.ResponseWriter, r *http.Request) {
api.JSON(w, "ok")
}
func (par *PolicyAdminResponder) CreateACMEAccountPolicy(w http.ResponseWriter, r *http.Request) {
api.JSON(w, "ok")
}
func (par *PolicyAdminResponder) UpdateACMEAccountPolicy(w http.ResponseWriter, r *http.Request) {
api.JSON(w, "ok")
}
func (par *PolicyAdminResponder) DeleteACMEAccountPolicy(w http.ResponseWriter, r *http.Request) {
api.JSON(w, "ok")
}

View file

@ -69,6 +69,11 @@ type DB interface {
GetAdmins(ctx context.Context) ([]*linkedca.Admin, error)
UpdateAdmin(ctx context.Context, admin *linkedca.Admin) error
DeleteAdmin(ctx context.Context, id string) error
CreateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error
GetAuthorityPolicy(ctx context.Context) (*linkedca.Policy, error)
UpdateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error
DeleteAuthorityPolicy(ctx context.Context) error
}
// MockDB is an implementation of the DB interface that should only be used as
@ -86,6 +91,11 @@ type MockDB struct {
MockUpdateAdmin func(ctx context.Context, adm *linkedca.Admin) error
MockDeleteAdmin func(ctx context.Context, id string) error
MockCreateAuthorityPolicy func(ctx context.Context, policy *linkedca.Policy) error
MockGetAuthorityPolicy func(ctx context.Context) (*linkedca.Policy, error)
MockUpdateAuthorityPolicy func(ctx context.Context, policy *linkedca.Policy) error
MockDeleteAuthorityPolicy func(ctx context.Context) error
MockError error
MockRet1 interface{}
}
@ -179,3 +189,30 @@ func (m *MockDB) DeleteAdmin(ctx context.Context, id string) error {
}
return m.MockError
}
func (m *MockDB) CreateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error {
if m.MockCreateAuthorityPolicy != nil {
return m.MockCreateAuthorityPolicy(ctx, policy)
}
return m.MockError
}
func (m *MockDB) GetAuthorityPolicy(ctx context.Context) (*linkedca.Policy, error) {
if m.MockGetAuthorityPolicy != nil {
return m.MockGetAuthorityPolicy(ctx)
}
return m.MockRet1.(*linkedca.Policy), m.MockError
}
func (m *MockDB) UpdateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error {
if m.MockUpdateAuthorityPolicy != nil {
return m.MockUpdateAuthorityPolicy(ctx, policy)
}
return m.MockError
}
func (m *MockDB) DeleteAuthorityPolicy(ctx context.Context) error {
if m.MockDeleteAuthorityPolicy != nil {
return m.MockDeleteAuthorityPolicy(ctx)
}
return m.MockError
}

View file

@ -11,8 +11,9 @@ import (
)
var (
adminsTable = []byte("admins")
provisionersTable = []byte("provisioners")
adminsTable = []byte("admins")
provisionersTable = []byte("provisioners")
authorityPoliciesTable = []byte("authority_policies")
)
// DB is a struct that implements the AdminDB interface.
@ -23,7 +24,7 @@ type DB struct {
// 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{adminsTable, provisionersTable}
tables := [][]byte{adminsTable, provisionersTable, authorityPoliciesTable}
for _, b := range tables {
if err := db.CreateTable(b); err != nil {
return nil, errors.Wrapf(err, "error creating table %s",

View file

@ -0,0 +1,144 @@
package nosql
import (
"context"
"encoding/json"
"github.com/pkg/errors"
"github.com/smallstep/certificates/authority/admin"
"github.com/smallstep/nosql"
"go.step.sm/linkedca"
)
type dbAuthorityPolicy struct {
ID string `json:"id"`
AuthorityID string `json:"authorityID"`
Policy *linkedca.Policy `json:"policy"`
}
func (dbap *dbAuthorityPolicy) convert() *linkedca.Policy {
return dbap.Policy
}
func (dbap *dbAuthorityPolicy) clone() *dbAuthorityPolicy {
u := *dbap
return &u
}
func (db *DB) getDBAuthorityPolicyBytes(ctx context.Context, authorityID string) ([]byte, error) {
data, err := db.db.Get(authorityPoliciesTable, []byte(authorityID))
if nosql.IsErrNotFound(err) {
return nil, admin.NewError(admin.ErrorNotFoundType, "policy %s not found", authorityID)
} else if err != nil {
return nil, errors.Wrapf(err, "error loading admin %s", authorityID)
}
return data, nil
}
func (db *DB) unmarshalDBAuthorityPolicy(data []byte, authorityID string) (*dbAuthorityPolicy, error) {
var dba = new(dbAuthorityPolicy)
if err := json.Unmarshal(data, dba); err != nil {
return nil, errors.Wrapf(err, "error unmarshaling admin %s into dbAdmin", authorityID)
}
// if !dba.DeletedAt.IsZero() {
// return nil, admin.NewError(admin.ErrorDeletedType, "admin %s is deleted", authorityID)
// }
if dba.AuthorityID != db.authorityID {
return nil, admin.NewError(admin.ErrorAuthorityMismatchType,
"admin %s is not owned by authority %s", dba.ID, db.authorityID)
}
return dba, nil
}
func (db *DB) getDBAuthorityPolicy(ctx context.Context, authorityID string) (*dbAuthorityPolicy, error) {
data, err := db.getDBAuthorityPolicyBytes(ctx, authorityID)
if err != nil {
return nil, err
}
dbap, err := db.unmarshalDBAuthorityPolicy(data, authorityID)
if err != nil {
return nil, err
}
return dbap, nil
}
func (db *DB) unmarshalAuthorityPolicy(data []byte, authorityID string) (*linkedca.Policy, error) {
dbap, err := db.unmarshalDBAuthorityPolicy(data, authorityID)
if err != nil {
return nil, err
}
return dbap.convert(), nil
}
func (db *DB) CreateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error {
dbap := &dbAuthorityPolicy{
ID: db.authorityID,
AuthorityID: db.authorityID,
Policy: policy,
}
old, err := db.getDBAuthorityPolicy(ctx, db.authorityID)
if err != nil {
return err
}
return db.save(ctx, dbap.ID, dbap, old, "authority_policy", authorityPoliciesTable)
}
func (db *DB) GetAuthorityPolicy(ctx context.Context) (*linkedca.Policy, error) {
// policy := &linkedca.Policy{
// X509: &linkedca.X509Policy{
// Allow: &linkedca.X509Names{
// Dns: []string{".localhost"},
// },
// Deny: &linkedca.X509Names{
// Dns: []string{"denied.localhost"},
// },
// },
// Ssh: &linkedca.SSHPolicy{
// User: &linkedca.SSHUserPolicy{
// Allow: &linkedca.SSHUserNames{},
// Deny: &linkedca.SSHUserNames{},
// },
// Host: &linkedca.SSHHostPolicy{
// Allow: &linkedca.SSHHostNames{},
// Deny: &linkedca.SSHHostNames{},
// },
// },
// }
dbap, err := db.getDBAuthorityPolicy(ctx, db.authorityID)
if err != nil {
return nil, err
}
return dbap.convert(), nil
}
func (db *DB) UpdateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error {
old, err := db.getDBAuthorityPolicy(ctx, db.authorityID)
if err != nil {
return err
}
dbap := &dbAuthorityPolicy{
ID: db.authorityID,
AuthorityID: db.authorityID,
Policy: policy,
}
return db.save(ctx, dbap.ID, dbap, old, "authority_policy", authorityPoliciesTable)
}
func (db *DB) DeleteAuthorityPolicy(ctx context.Context) error {
dbap, err := db.getDBAuthorityPolicy(ctx, db.authorityID)
if err != nil {
return err
}
old := dbap.clone()
dbap.Policy = nil
return db.save(ctx, dbap.ID, dbap, old, "authority_policy", authorityPoliciesTable)
}

View file

@ -19,6 +19,7 @@ type dbProvisioner struct {
Type linkedca.Provisioner_Type `json:"type"`
Name string `json:"name"`
Claims *linkedca.Claims `json:"claims"`
Policy *linkedca.Policy `json:"policy"`
Details []byte `json:"details"`
X509Template *linkedca.Template `json:"x509Template"`
SSHTemplate *linkedca.Template `json:"sshTemplate"`
@ -43,6 +44,7 @@ func (dbp *dbProvisioner) convert2linkedca() (*linkedca.Provisioner, error) {
Type: dbp.Type,
Name: dbp.Name,
Claims: dbp.Claims,
Policy: dbp.Policy,
Details: details,
X509Template: dbp.X509Template,
SshTemplate: dbp.SSHTemplate,
@ -160,6 +162,7 @@ func (db *DB) CreateProvisioner(ctx context.Context, prov *linkedca.Provisioner)
Type: prov.Type,
Name: prov.Name,
Claims: prov.Claims,
Policy: prov.Policy,
Details: details,
X509Template: prov.X509Template,
SSHTemplate: prov.SshTemplate,
@ -187,6 +190,7 @@ func (db *DB) UpdateProvisioner(ctx context.Context, prov *linkedca.Provisioner)
}
nu.Name = prov.Name
nu.Claims = prov.Claims
nu.Policy = prov.Policy
nu.Details, err = json.Marshal(prov.Details.GetData())
if err != nil {
return admin.WrapErrorISE(err, "error marshaling details when updating provisioner %s", prov.Name)

View file

@ -205,6 +205,47 @@ func (a *Authority) reloadAdminResources(ctx context.Context) error {
a.provisioners = provClxn
a.config.AuthorityConfig.Admins = adminList
a.admins = adminClxn
return nil
}
// reloadPolicyEngines reloads x509 and SSH policy engines using
// configuration stored in the DB or from the configuration file.
func (a *Authority) reloadPolicyEngines(ctx context.Context) error {
var (
err error
policyOptions *policy.Options
)
if a.config.AuthorityConfig.EnableAdmin {
linkedPolicy, err := a.adminDB.GetAuthorityPolicy(ctx)
if err != nil {
return admin.WrapErrorISE(err, "error getting policy to initialize authority")
}
policyOptions = policyToCertificates(linkedPolicy)
} else {
policyOptions = a.config.AuthorityConfig.Policy
}
// return early if no policy options set
if policyOptions == nil {
return nil
}
// Initialize the x509 allow/deny policy engine
if a.x509Policy, err = policy.NewX509PolicyEngine(policyOptions.GetX509Options()); err != nil {
return err
}
// // Initialize the SSH allow/deny policy engine for host certificates
if a.sshHostPolicy, err = policy.NewSSHHostPolicyEngine(policyOptions.GetSSHOptions()); err != nil {
return err
}
// // Initialize the SSH allow/deny policy engine for user certificates
if a.sshUserPolicy, err = policy.NewSSHUserPolicyEngine(policyOptions.GetSSHOptions()); err != nil {
return err
}
return nil
}
@ -533,6 +574,11 @@ func (a *Authority) init() error {
return err
}
// Load Policy Engines
if err := a.reloadPolicyEngines(context.Background()); err != nil {
return err
}
// Configure templates, currently only ssh templates are supported.
if a.sshCAHostCertSignKey != nil || a.sshCAUserCertSignKey != nil {
a.templates = a.config.Templates
@ -545,21 +591,6 @@ func (a *Authority) init() error {
a.templates.Data["Step"] = tmplVars
}
// Initialize the x509 allow/deny policy engine
if a.x509Policy, err = policy.NewX509PolicyEngine(a.config.AuthorityConfig.Policy.GetX509Options()); err != nil {
return err
}
// // Initialize the SSH allow/deny policy engine for host certificates
if a.sshHostPolicy, err = policy.NewSSHHostPolicyEngine(a.config.AuthorityConfig.Policy.GetSSHOptions()); err != nil {
return err
}
// // Initialize the SSH allow/deny policy engine for user certificates
if a.sshUserPolicy, err = policy.NewSSHUserPolicyEngine(a.config.AuthorityConfig.Policy.GetSSHOptions()); err != nil {
return err
}
// JWT numeric dates are seconds.
a.startTime = time.Now().Truncate(time.Second)
// Set flag indicating that initialization has been completed, and should

View file

@ -15,6 +15,7 @@ import (
"time"
"github.com/pkg/errors"
"github.com/smallstep/certificates/authority/admin"
"github.com/smallstep/certificates/db"
"go.step.sm/crypto/jose"
"go.step.sm/crypto/keyutil"
@ -34,6 +35,9 @@ type linkedCaClient struct {
authorityID string
}
// interface guard
var _ admin.DB = (*linkedCaClient)(nil)
type linkedCAClaims struct {
jose.Claims
SANs []string `json:"sans"`
@ -310,6 +314,22 @@ func (c *linkedCaClient) IsSSHRevoked(serial string) (bool, error) {
return resp.Status != linkedca.RevocationStatus_ACTIVE, nil
}
func (c *linkedCaClient) CreateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error {
return errors.New("not implemented yet")
}
func (c *linkedCaClient) GetAuthorityPolicy(ctx context.Context) (*linkedca.Policy, error) {
return nil, errors.New("not implemented yet")
}
func (c *linkedCaClient) UpdateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error {
return errors.New("not implemented yet")
}
func (c *linkedCaClient) DeleteAuthorityPolicy(ctx context.Context) error {
return errors.New("not implemented yet")
}
func serializeCertificate(crt *x509.Certificate) string {
if crt == nil {
return ""

132
authority/policy.go Normal file
View file

@ -0,0 +1,132 @@
package authority
import (
"context"
"github.com/smallstep/certificates/authority/admin"
"github.com/smallstep/certificates/authority/policy"
"go.step.sm/linkedca"
)
func (a *Authority) GetAuthorityPolicy(ctx context.Context) (*linkedca.Policy, error) {
a.adminMutex.Lock()
defer a.adminMutex.Unlock()
policy, err := a.adminDB.GetAuthorityPolicy(ctx)
if err != nil {
return nil, err
}
return policy, nil
}
func (a *Authority) StoreAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error {
a.adminMutex.Lock()
defer a.adminMutex.Unlock()
if err := a.adminDB.CreateAuthorityPolicy(ctx, policy); err != nil {
return err
}
if err := a.reloadPolicyEngines(ctx); err != nil {
return admin.WrapErrorISE(err, "error reloading admin resources when creating authority policy")
}
return nil
}
func (a *Authority) UpdateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error {
a.adminMutex.Lock()
defer a.adminMutex.Unlock()
if err := a.adminDB.UpdateAuthorityPolicy(ctx, policy); err != nil {
return err
}
if err := a.reloadPolicyEngines(ctx); err != nil {
return admin.WrapErrorISE(err, "error reloading admin resources when updating authority policy")
}
return nil
}
func (a *Authority) RemoveAuthorityPolicy(ctx context.Context) error {
a.adminMutex.Lock()
defer a.adminMutex.Unlock()
if err := a.adminDB.DeleteAuthorityPolicy(ctx); err != nil {
return err
}
if err := a.reloadPolicyEngines(ctx); err != nil {
return admin.WrapErrorISE(err, "error reloading admin resources when deleting authority policy")
}
return nil
}
func policyToCertificates(p *linkedca.Policy) *policy.Options {
// return early
if p == nil {
return nil
}
// prepare full policy struct
opts := &policy.Options{
X509: &policy.X509PolicyOptions{
AllowedNames: &policy.X509NameOptions{},
DeniedNames: &policy.X509NameOptions{},
},
SSH: &policy.SSHPolicyOptions{
Host: &policy.SSHHostCertificateOptions{
AllowedNames: &policy.SSHNameOptions{},
DeniedNames: &policy.SSHNameOptions{},
},
User: &policy.SSHUserCertificateOptions{
AllowedNames: &policy.SSHNameOptions{},
DeniedNames: &policy.SSHNameOptions{},
},
},
}
// fill x509 policy configuration
if p.X509 != nil {
if p.X509.Allow != nil {
opts.X509.AllowedNames.DNSDomains = p.X509.Allow.Dns
opts.X509.AllowedNames.IPRanges = p.X509.Allow.Ips
opts.X509.AllowedNames.EmailAddresses = p.X509.Allow.Emails
opts.X509.AllowedNames.URIDomains = p.X509.Allow.Uris
}
if p.X509.Deny != nil {
opts.X509.DeniedNames.DNSDomains = p.X509.Deny.Dns
opts.X509.DeniedNames.IPRanges = p.X509.Deny.Ips
opts.X509.DeniedNames.EmailAddresses = p.X509.Deny.Emails
opts.X509.DeniedNames.URIDomains = p.X509.Deny.Uris
}
}
// fill ssh policy configuration
if p.Ssh != nil {
if p.Ssh.Host != nil {
if p.Ssh.Host.Allow != nil {
opts.SSH.Host.AllowedNames.DNSDomains = p.Ssh.Host.Allow.Dns
opts.SSH.Host.AllowedNames.IPRanges = p.Ssh.Host.Allow.Ips
opts.SSH.Host.AllowedNames.EmailAddresses = p.Ssh.Host.Allow.Principals
}
if p.Ssh.Host.Deny != nil {
opts.SSH.Host.DeniedNames.DNSDomains = p.Ssh.Host.Deny.Dns
opts.SSH.Host.DeniedNames.IPRanges = p.Ssh.Host.Deny.Ips
opts.SSH.Host.DeniedNames.Principals = p.Ssh.Host.Deny.Principals
}
}
if p.Ssh.User != nil {
if p.Ssh.User.Allow != nil {
opts.SSH.User.AllowedNames.EmailAddresses = p.Ssh.User.Allow.Emails
opts.SSH.User.AllowedNames.Principals = p.Ssh.User.Allow.Principals
}
if p.Ssh.User.Deny != nil {
opts.SSH.User.DeniedNames.EmailAddresses = p.Ssh.User.Deny.Emails
opts.SSH.User.DeniedNames.Principals = p.Ssh.User.Deny.Principals
}
}
}
return opts
}

View file

@ -1,10 +1,14 @@
package policy
// Options is a container for authority level x509 and SSH
// policy configuration.
type Options struct {
X509 *X509PolicyOptions `json:"x509,omitempty"`
SSH *SSHPolicyOptions `json:"ssh,omitempty"`
}
// GetX509Options returns the x509 authority level policy
// configuration
func (o *Options) GetX509Options() *X509PolicyOptions {
if o == nil {
return nil
@ -12,6 +16,8 @@ func (o *Options) GetX509Options() *X509PolicyOptions {
return o.X509
}
// GetSSHOptions returns the SSH authority level policy
// configuration
func (o *Options) GetSSHOptions() *SSHPolicyOptions {
if o == nil {
return nil
@ -19,16 +25,19 @@ func (o *Options) GetSSHOptions() *SSHPolicyOptions {
return o.SSH
}
// X509PolicyOptionsInterface is an interface for providers
// of x509 allowed and denied names.
type X509PolicyOptionsInterface interface {
GetAllowedNameOptions() *X509NameOptions
GetDeniedNameOptions() *X509NameOptions
}
// X509PolicyOptions is a container for x509 allowed and denied
// names.
type X509PolicyOptions struct {
// AllowedNames ...
// AllowedNames contains the x509 allowed names
AllowedNames *X509NameOptions `json:"allow,omitempty"`
// DeniedNames ...
// DeniedNames contains the x509 denied names
DeniedNames *X509NameOptions `json:"deny,omitempty"`
}
@ -49,6 +58,8 @@ func (o *X509NameOptions) HasNames() bool {
len(o.URIDomains) > 0
}
// SSHPolicyOptionsInterface is an interface for providers of
// SSH user and host name policy configuration.
type SSHPolicyOptionsInterface interface {
GetAllowedUserNameOptions() *SSHNameOptions
GetDeniedUserNameOptions() *SSHNameOptions
@ -56,16 +67,16 @@ type SSHPolicyOptionsInterface interface {
GetDeniedHostNameOptions() *SSHNameOptions
}
// SSHPolicyOptions is a container for SSH user and host policy
// configuration
type SSHPolicyOptions struct {
// User contains SSH user certificate options.
User *SSHUserCertificateOptions `json:"user,omitempty"`
// Host contains SSH host certificate options.
Host *SSHHostCertificateOptions `json:"host,omitempty"`
}
// GetAllowedNameOptions returns AllowedNames, which models the
// SANs that ...
// GetAllowedNameOptions returns x509 allowed name policy configuration
func (o *X509PolicyOptions) GetAllowedNameOptions() *X509NameOptions {
if o == nil {
return nil
@ -73,8 +84,7 @@ func (o *X509PolicyOptions) GetAllowedNameOptions() *X509NameOptions {
return o.AllowedNames
}
// GetDeniedNameOptions returns the DeniedNames, which models the
// SANs that ...
// GetDeniedNameOptions returns the x509 denied name policy configuration
func (o *X509PolicyOptions) GetDeniedNameOptions() *X509NameOptions {
if o == nil {
return nil
@ -82,6 +92,8 @@ func (o *X509PolicyOptions) GetDeniedNameOptions() *X509NameOptions {
return o.DeniedNames
}
// GetAllowedUserNameOptions returns the SSH allowed user name policy
// configuration.
func (o *SSHPolicyOptions) GetAllowedUserNameOptions() *SSHNameOptions {
if o == nil {
return nil
@ -92,6 +104,8 @@ func (o *SSHPolicyOptions) GetAllowedUserNameOptions() *SSHNameOptions {
return o.User.AllowedNames
}
// GetDeniedUserNameOptions returns the SSH denied user name policy
// configuration.
func (o *SSHPolicyOptions) GetDeniedUserNameOptions() *SSHNameOptions {
if o == nil {
return nil
@ -102,6 +116,8 @@ func (o *SSHPolicyOptions) GetDeniedUserNameOptions() *SSHNameOptions {
return o.User.DeniedNames
}
// GetAllowedHostNameOptions returns the SSH allowed host name policy
// configuration.
func (o *SSHPolicyOptions) GetAllowedHostNameOptions() *SSHNameOptions {
if o == nil {
return nil
@ -112,6 +128,8 @@ func (o *SSHPolicyOptions) GetAllowedHostNameOptions() *SSHNameOptions {
return o.Host.AllowedNames
}
// GetDeniedHostNameOptions returns the SSH denied host name policy
// configuration.
func (o *SSHPolicyOptions) GetDeniedHostNameOptions() *SSHNameOptions {
if o == nil {
return nil

View file

@ -12,6 +12,7 @@ import (
"github.com/pkg/errors"
"github.com/smallstep/certificates/authority/admin"
"github.com/smallstep/certificates/authority/config"
"github.com/smallstep/certificates/authority/policy"
"github.com/smallstep/certificates/authority/provisioner"
"github.com/smallstep/certificates/errs"
"go.step.sm/cli-utils/step"
@ -395,6 +396,58 @@ func optionsToCertificates(p *linkedca.Provisioner) *provisioner.Options {
ops.SSH.Template = string(p.SshTemplate.Template)
ops.SSH.TemplateData = p.SshTemplate.Data
}
if p.Policy != nil {
if p.Policy.X509 != nil {
if p.Policy.X509.Allow != nil {
ops.X509.AllowedNames = &policy.X509NameOptions{
DNSDomains: p.Policy.X509.Allow.Dns,
IPRanges: p.Policy.X509.Allow.Ips,
EmailAddresses: p.Policy.X509.Allow.Emails,
URIDomains: p.Policy.X509.Allow.Uris,
}
}
if p.Policy.X509.Deny != nil {
ops.X509.DeniedNames = &policy.X509NameOptions{
DNSDomains: p.Policy.X509.Deny.Dns,
IPRanges: p.Policy.X509.Deny.Ips,
EmailAddresses: p.Policy.X509.Deny.Emails,
URIDomains: p.Policy.X509.Deny.Uris,
}
}
}
if p.Policy.Ssh != nil {
if p.Policy.Ssh.Host != nil {
ops.SSH.Host = &policy.SSHHostCertificateOptions{}
if p.Policy.Ssh.Host.Allow != nil {
ops.SSH.Host.AllowedNames = &policy.SSHNameOptions{
DNSDomains: p.Policy.Ssh.Host.Allow.Dns,
IPRanges: p.Policy.Ssh.Host.Allow.Ips,
}
}
if p.Policy.Ssh.Host.Deny != nil {
ops.SSH.Host.DeniedNames = &policy.SSHNameOptions{
DNSDomains: p.Policy.Ssh.Host.Deny.Dns,
IPRanges: p.Policy.Ssh.Host.Deny.Ips,
}
}
}
if p.Policy.Ssh.User != nil {
ops.SSH.User = &policy.SSHUserCertificateOptions{}
if p.Policy.Ssh.User.Allow != nil {
ops.SSH.User.AllowedNames = &policy.SSHNameOptions{
EmailAddresses: p.Policy.Ssh.User.Allow.Emails,
Principals: p.Policy.Ssh.User.Allow.Principals,
}
}
if p.Policy.Ssh.User.Deny != nil {
ops.SSH.User.DeniedNames = &policy.SSHNameOptions{
EmailAddresses: p.Policy.Ssh.User.Deny.Emails,
Principals: p.Policy.Ssh.User.Deny.Principals,
}
}
}
}
}
return ops
}

View file

@ -192,7 +192,10 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Sign
}
// If a policy is configured, perform allow/deny policy check on authority level
if a.x509Policy != nil {
// TODO: policy currently also applies to admin token certs; how to circumvent?
// Allow any name of an admin in the DB? Or in the admin collection?
todoRemoveThis := false
if todoRemoveThis && a.x509Policy != nil {
allowed, err := a.x509Policy.AreCertificateNamesAllowed(leaf)
if err != nil {
return nil, errs.InternalServerErr(err,

View file

@ -208,7 +208,8 @@ func (ca *CA) Init(cfg *config.Config) (*CA, error) {
adminDB := auth.GetAdminDatabase()
if adminDB != nil {
acmeAdminResponder := adminAPI.NewACMEAdminResponder()
adminHandler := adminAPI.NewHandler(auth, adminDB, acmeDB, acmeAdminResponder)
policyAdminResponder := adminAPI.NewPolicyAdminResponder(auth, adminDB)
adminHandler := adminAPI.NewHandler(auth, adminDB, acmeDB, acmeAdminResponder, policyAdminResponder)
mux.Route("/admin", func(r chi.Router) {
adminHandler.Route(r)
})

2
go.mod
View file

@ -49,4 +49,4 @@ require (
// replace github.com/smallstep/nosql => ../nosql
// replace go.step.sm/crypto => ../crypto
// replace go.step.sm/cli-utils => ../cli-utils
// replace go.step.sm/linkedca => ../linkedca
replace go.step.sm/linkedca => ../linkedca

4
go.sum
View file

@ -685,10 +685,6 @@ go.step.sm/cli-utils v0.7.0/go.mod h1:Ur6bqA/yl636kCUJbp30J7Unv5JJ226eW2KqXPDwF/
go.step.sm/crypto v0.9.0/go.mod h1:+CYG05Mek1YDqi5WK0ERc6cOpKly2i/a5aZmU1sfGj0=
go.step.sm/crypto v0.15.0 h1:VioBln+x3+RoejgeBhvxkLGVYdWRy6PFiAaUUN29/E0=
go.step.sm/crypto v0.15.0/go.mod h1:3G0yQr5lQqfEG0CMYz8apC/qMtjLRQlzflL2AxkcN+g=
go.step.sm/linkedca v0.9.2 h1:CpAkd174sLXFfrOZrbPEiTzik91QRj3+L0omsiwsiok=
go.step.sm/linkedca v0.9.2/go.mod h1:5uTRjozEGSPAZal9xJqlaD38cvJcLe3o1VAFVjqcORo=
go.step.sm/linkedca v0.10.0 h1:+bqymMRulHYkVde4l16FnqFVskoS6HCWJN5Z5cxAqF8=
go.step.sm/linkedca v0.10.0/go.mod h1:5uTRjozEGSPAZal9xJqlaD38cvJcLe3o1VAFVjqcORo=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=