forked from TrueCloudLab/certificates
Provisioner webhooks (#1001)
This commit is contained in:
parent
6fe0fc852a
commit
7101fbb0ee
53 changed files with 2965 additions and 117 deletions
|
@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
|
|||
- Added support for ACME device-attest-01 challenge.
|
||||
- Added name constraints evaluation and enforcement when issuing or renewing
|
||||
X.509 certificates.
|
||||
- Added provisioner webhooks for augmenting template data and authorizing certificate requests before signing.
|
||||
|
||||
## [0.22.1] - 2022-08-31
|
||||
### Fixed
|
||||
|
|
|
@ -194,6 +194,14 @@ func (o *Order) Finalize(ctx context.Context, db DB, csr *x509.CertificateReques
|
|||
if err != nil {
|
||||
return WrapErrorISE(err, "error retrieving authorization options from ACME provisioner")
|
||||
}
|
||||
// Unlike most of the provisioners, ACME's AuthorizeSign method doesn't
|
||||
// define the templates, and the template data used in WebHooks is not
|
||||
// available.
|
||||
for _, signOp := range signOps {
|
||||
if wc, ok := signOp.(*provisioner.WebhookController); ok {
|
||||
wc.TemplateData = data
|
||||
}
|
||||
}
|
||||
|
||||
templateOptions, err := provisioner.CustomTemplateOptions(p.GetOptions(), data, defaultTemplate)
|
||||
if err != nil {
|
||||
|
|
|
@ -4,41 +4,47 @@ import (
|
|||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/smallstep/certificates/acme"
|
||||
"github.com/smallstep/certificates/api"
|
||||
"github.com/smallstep/certificates/authority"
|
||||
"github.com/smallstep/certificates/authority/admin"
|
||||
)
|
||||
|
||||
// Handler is the Admin API request handler.
|
||||
type Handler struct {
|
||||
acmeResponder ACMEAdminResponder
|
||||
policyResponder PolicyAdminResponder
|
||||
}
|
||||
|
||||
// Route traffic and implement the Router interface.
|
||||
//
|
||||
// Deprecated: use Route(r api.Router, acmeResponder ACMEAdminResponder, policyResponder PolicyAdminResponder)
|
||||
func (h *Handler) Route(r api.Router) {
|
||||
Route(r, h.acmeResponder, h.policyResponder)
|
||||
}
|
||||
|
||||
// NewHandler returns a new Authority Config Handler.
|
||||
//
|
||||
// Deprecated: use Route(r api.Router, acmeResponder ACMEAdminResponder, policyResponder PolicyAdminResponder)
|
||||
func NewHandler(auth adminAuthority, adminDB admin.DB, acmeDB acme.DB, acmeResponder ACMEAdminResponder, policyResponder PolicyAdminResponder) api.RouterHandler {
|
||||
return &Handler{
|
||||
acmeResponder: acmeResponder,
|
||||
policyResponder: policyResponder,
|
||||
}
|
||||
}
|
||||
|
||||
var mustAuthority = func(ctx context.Context) adminAuthority {
|
||||
return authority.MustFromContext(ctx)
|
||||
}
|
||||
|
||||
type router struct {
|
||||
acmeResponder ACMEAdminResponder
|
||||
policyResponder PolicyAdminResponder
|
||||
webhookResponder WebhookAdminResponder
|
||||
}
|
||||
|
||||
type RouterOption func(*router)
|
||||
|
||||
func WithACMEResponder(acmeResponder ACMEAdminResponder) RouterOption {
|
||||
return func(r *router) {
|
||||
r.acmeResponder = acmeResponder
|
||||
}
|
||||
}
|
||||
|
||||
func WithPolicyResponder(policyResponder PolicyAdminResponder) RouterOption {
|
||||
return func(r *router) {
|
||||
r.policyResponder = policyResponder
|
||||
}
|
||||
}
|
||||
|
||||
func WithWebhookResponder(webhookResponder WebhookAdminResponder) RouterOption {
|
||||
return func(r *router) {
|
||||
r.webhookResponder = webhookResponder
|
||||
}
|
||||
}
|
||||
|
||||
// Route traffic and implement the Router interface.
|
||||
func Route(r api.Router, acmeResponder ACMEAdminResponder, policyResponder PolicyAdminResponder) {
|
||||
func Route(r api.Router, options ...RouterOption) {
|
||||
router := &router{}
|
||||
for _, fn := range options {
|
||||
fn(router)
|
||||
}
|
||||
|
||||
authnz := func(next http.HandlerFunc) http.HandlerFunc {
|
||||
return extractAuthorizeTokenAdmin(requireAPIEnabled(next))
|
||||
}
|
||||
|
@ -67,6 +73,10 @@ func Route(r api.Router, acmeResponder ACMEAdminResponder, policyResponder Polic
|
|||
return authnz(disabledInStandalone(loadProvisionerByName(requireEABEnabled(loadExternalAccountKey(next)))))
|
||||
}
|
||||
|
||||
webhookMiddleware := func(next http.HandlerFunc) http.HandlerFunc {
|
||||
return authnz(loadProvisionerByName(next))
|
||||
}
|
||||
|
||||
// Provisioners
|
||||
r.MethodFunc("GET", "/provisioners/{name}", authnz(GetProvisioner))
|
||||
r.MethodFunc("GET", "/provisioners", authnz(GetProvisioners))
|
||||
|
@ -82,36 +92,42 @@ func Route(r api.Router, acmeResponder ACMEAdminResponder, policyResponder Polic
|
|||
r.MethodFunc("DELETE", "/admins/{id}", authnz(DeleteAdmin))
|
||||
|
||||
// ACME responder
|
||||
if acmeResponder != nil {
|
||||
if router.acmeResponder != nil {
|
||||
// ACME External Account Binding Keys
|
||||
r.MethodFunc("GET", "/acme/eab/{provisionerName}/{reference}", acmeEABMiddleware(acmeResponder.GetExternalAccountKeys))
|
||||
r.MethodFunc("GET", "/acme/eab/{provisionerName}", acmeEABMiddleware(acmeResponder.GetExternalAccountKeys))
|
||||
r.MethodFunc("POST", "/acme/eab/{provisionerName}", acmeEABMiddleware(acmeResponder.CreateExternalAccountKey))
|
||||
r.MethodFunc("DELETE", "/acme/eab/{provisionerName}/{id}", acmeEABMiddleware(acmeResponder.DeleteExternalAccountKey))
|
||||
r.MethodFunc("GET", "/acme/eab/{provisionerName}/{reference}", acmeEABMiddleware(router.acmeResponder.GetExternalAccountKeys))
|
||||
r.MethodFunc("GET", "/acme/eab/{provisionerName}", acmeEABMiddleware(router.acmeResponder.GetExternalAccountKeys))
|
||||
r.MethodFunc("POST", "/acme/eab/{provisionerName}", acmeEABMiddleware(router.acmeResponder.CreateExternalAccountKey))
|
||||
r.MethodFunc("DELETE", "/acme/eab/{provisionerName}/{id}", acmeEABMiddleware(router.acmeResponder.DeleteExternalAccountKey))
|
||||
}
|
||||
|
||||
// Policy responder
|
||||
if policyResponder != nil {
|
||||
if router.policyResponder != nil {
|
||||
// Policy - Authority
|
||||
r.MethodFunc("GET", "/policy", authorityPolicyMiddleware(policyResponder.GetAuthorityPolicy))
|
||||
r.MethodFunc("POST", "/policy", authorityPolicyMiddleware(policyResponder.CreateAuthorityPolicy))
|
||||
r.MethodFunc("PUT", "/policy", authorityPolicyMiddleware(policyResponder.UpdateAuthorityPolicy))
|
||||
r.MethodFunc("DELETE", "/policy", authorityPolicyMiddleware(policyResponder.DeleteAuthorityPolicy))
|
||||
r.MethodFunc("GET", "/policy", authorityPolicyMiddleware(router.policyResponder.GetAuthorityPolicy))
|
||||
r.MethodFunc("POST", "/policy", authorityPolicyMiddleware(router.policyResponder.CreateAuthorityPolicy))
|
||||
r.MethodFunc("PUT", "/policy", authorityPolicyMiddleware(router.policyResponder.UpdateAuthorityPolicy))
|
||||
r.MethodFunc("DELETE", "/policy", authorityPolicyMiddleware(router.policyResponder.DeleteAuthorityPolicy))
|
||||
|
||||
// Policy - Provisioner
|
||||
r.MethodFunc("GET", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(policyResponder.GetProvisionerPolicy))
|
||||
r.MethodFunc("POST", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(policyResponder.CreateProvisionerPolicy))
|
||||
r.MethodFunc("PUT", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(policyResponder.UpdateProvisionerPolicy))
|
||||
r.MethodFunc("DELETE", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(policyResponder.DeleteProvisionerPolicy))
|
||||
r.MethodFunc("GET", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(router.policyResponder.GetProvisionerPolicy))
|
||||
r.MethodFunc("POST", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(router.policyResponder.CreateProvisionerPolicy))
|
||||
r.MethodFunc("PUT", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(router.policyResponder.UpdateProvisionerPolicy))
|
||||
r.MethodFunc("DELETE", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(router.policyResponder.DeleteProvisionerPolicy))
|
||||
|
||||
// Policy - ACME Account
|
||||
r.MethodFunc("GET", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(policyResponder.GetACMEAccountPolicy))
|
||||
r.MethodFunc("GET", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(policyResponder.GetACMEAccountPolicy))
|
||||
r.MethodFunc("POST", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(policyResponder.CreateACMEAccountPolicy))
|
||||
r.MethodFunc("POST", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(policyResponder.CreateACMEAccountPolicy))
|
||||
r.MethodFunc("PUT", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(policyResponder.UpdateACMEAccountPolicy))
|
||||
r.MethodFunc("PUT", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(policyResponder.UpdateACMEAccountPolicy))
|
||||
r.MethodFunc("DELETE", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(policyResponder.DeleteACMEAccountPolicy))
|
||||
r.MethodFunc("DELETE", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(policyResponder.DeleteACMEAccountPolicy))
|
||||
r.MethodFunc("GET", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(router.policyResponder.GetACMEAccountPolicy))
|
||||
r.MethodFunc("GET", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(router.policyResponder.GetACMEAccountPolicy))
|
||||
r.MethodFunc("POST", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(router.policyResponder.CreateACMEAccountPolicy))
|
||||
r.MethodFunc("POST", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(router.policyResponder.CreateACMEAccountPolicy))
|
||||
r.MethodFunc("PUT", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(router.policyResponder.UpdateACMEAccountPolicy))
|
||||
r.MethodFunc("PUT", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(router.policyResponder.UpdateACMEAccountPolicy))
|
||||
r.MethodFunc("DELETE", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(router.policyResponder.DeleteACMEAccountPolicy))
|
||||
r.MethodFunc("DELETE", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(router.policyResponder.DeleteACMEAccountPolicy))
|
||||
}
|
||||
|
||||
if router.webhookResponder != nil {
|
||||
r.MethodFunc("POST", "/provisioners/{provisionerName}/webhooks", webhookMiddleware(router.webhookResponder.CreateProvisionerWebhook))
|
||||
r.MethodFunc("PUT", "/provisioners/{provisionerName}/webhooks/{webhookName}", webhookMiddleware(router.webhookResponder.UpdateProvisionerWebhook))
|
||||
r.MethodFunc("DELETE", "/provisioners/{provisionerName}/webhooks/{webhookName}", webhookMiddleware(router.webhookResponder.DeleteProvisionerWebhook))
|
||||
}
|
||||
}
|
||||
|
|
235
authority/admin/api/webhook.go
Normal file
235
authority/admin/api/webhook.go
Normal file
|
@ -0,0 +1,235 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/smallstep/certificates/api/read"
|
||||
"github.com/smallstep/certificates/api/render"
|
||||
"github.com/smallstep/certificates/authority/admin"
|
||||
"go.step.sm/crypto/randutil"
|
||||
"go.step.sm/linkedca"
|
||||
)
|
||||
|
||||
// WebhookAdminResponder is the interface responsible for writing webhook admin
|
||||
// responses.
|
||||
type WebhookAdminResponder interface {
|
||||
CreateProvisionerWebhook(w http.ResponseWriter, r *http.Request)
|
||||
UpdateProvisionerWebhook(w http.ResponseWriter, r *http.Request)
|
||||
DeleteProvisionerWebhook(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
// webhoookAdminResponder implements WebhookAdminResponder
|
||||
type webhookAdminResponder struct{}
|
||||
|
||||
// NewWebhookAdminResponder returns a new WebhookAdminResponder
|
||||
func NewWebhookAdminResponder() WebhookAdminResponder {
|
||||
return &webhookAdminResponder{}
|
||||
}
|
||||
|
||||
func validateWebhook(webhook *linkedca.Webhook) error {
|
||||
if webhook == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// name
|
||||
if webhook.Name == "" {
|
||||
return admin.NewError(admin.ErrorBadRequestType, "webhook name is required")
|
||||
}
|
||||
|
||||
// url
|
||||
parsedURL, err := url.Parse(webhook.Url)
|
||||
if err != nil {
|
||||
return admin.NewError(admin.ErrorBadRequestType, "webhook url is invalid")
|
||||
}
|
||||
if parsedURL.Host == "" {
|
||||
return admin.NewError(admin.ErrorBadRequestType, "webhook url is invalid")
|
||||
}
|
||||
if parsedURL.Scheme != "https" {
|
||||
return admin.NewError(admin.ErrorBadRequestType, "webhook url must use https")
|
||||
}
|
||||
if parsedURL.User != nil {
|
||||
return admin.NewError(admin.ErrorBadRequestType, "webhook url may not contain username or password")
|
||||
}
|
||||
|
||||
// kind
|
||||
switch webhook.Kind {
|
||||
case linkedca.Webhook_ENRICHING, linkedca.Webhook_AUTHORIZING:
|
||||
default:
|
||||
return admin.NewError(admin.ErrorBadRequestType, "webhook kind is invalid")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (war *webhookAdminResponder) CreateProvisionerWebhook(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
auth := mustAuthority(ctx)
|
||||
prov := linkedca.MustProvisionerFromContext(ctx)
|
||||
|
||||
var newWebhook = new(linkedca.Webhook)
|
||||
if err := read.ProtoJSON(r.Body, newWebhook); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := validateWebhook(newWebhook); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
if newWebhook.Secret != "" {
|
||||
err := admin.NewError(admin.ErrorBadRequestType, "webhook secret must not be set")
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
if newWebhook.Id != "" {
|
||||
err := admin.NewError(admin.ErrorBadRequestType, "webhook ID must not be set")
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
id, err := randutil.UUIDv4()
|
||||
if err != nil {
|
||||
render.Error(w, admin.WrapErrorISE(err, "error generating webhook id"))
|
||||
return
|
||||
}
|
||||
newWebhook.Id = id
|
||||
|
||||
// verify the name is unique
|
||||
for _, wh := range prov.Webhooks {
|
||||
if wh.Name == newWebhook.Name {
|
||||
err := admin.NewError(admin.ErrorConflictType, "provisioner %q already has a webhook with the name %q", prov.Name, newWebhook.Name)
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
secret, err := randutil.Bytes(64)
|
||||
if err != nil {
|
||||
render.Error(w, admin.WrapErrorISE(err, "error generating webhook secret"))
|
||||
return
|
||||
}
|
||||
newWebhook.Secret = base64.StdEncoding.EncodeToString(secret)
|
||||
|
||||
prov.Webhooks = append(prov.Webhooks, newWebhook)
|
||||
|
||||
if err := auth.UpdateProvisioner(ctx, prov); err != nil {
|
||||
if isBadRequest(err) {
|
||||
render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error creating provisioner webhook"))
|
||||
return
|
||||
}
|
||||
|
||||
render.Error(w, admin.WrapErrorISE(err, "error creating provisioner webhook"))
|
||||
return
|
||||
}
|
||||
|
||||
render.ProtoJSONStatus(w, newWebhook, http.StatusCreated)
|
||||
}
|
||||
|
||||
func (war *webhookAdminResponder) DeleteProvisionerWebhook(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
auth := mustAuthority(ctx)
|
||||
prov := linkedca.MustProvisionerFromContext(ctx)
|
||||
|
||||
webhookName := chi.URLParam(r, "webhookName")
|
||||
|
||||
found := false
|
||||
for i, wh := range prov.Webhooks {
|
||||
if wh.Name == webhookName {
|
||||
prov.Webhooks = append(prov.Webhooks[0:i], prov.Webhooks[i+1:]...)
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
render.JSONStatus(w, DeleteResponse{Status: "ok"}, http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
if err := auth.UpdateProvisioner(ctx, prov); err != nil {
|
||||
if isBadRequest(err) {
|
||||
render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error deleting provisioner webhook"))
|
||||
return
|
||||
}
|
||||
|
||||
render.Error(w, admin.WrapErrorISE(err, "error deleting provisioner webhook"))
|
||||
return
|
||||
}
|
||||
|
||||
render.JSONStatus(w, DeleteResponse{Status: "ok"}, http.StatusOK)
|
||||
}
|
||||
|
||||
func (war *webhookAdminResponder) UpdateProvisionerWebhook(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
auth := mustAuthority(ctx)
|
||||
prov := linkedca.MustProvisionerFromContext(ctx)
|
||||
|
||||
var newWebhook = new(linkedca.Webhook)
|
||||
if err := read.ProtoJSON(r.Body, newWebhook); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := validateWebhook(newWebhook); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
found := false
|
||||
for i, wh := range prov.Webhooks {
|
||||
if wh.Name != newWebhook.Name {
|
||||
continue
|
||||
}
|
||||
if newWebhook.Secret != "" && newWebhook.Secret != wh.Secret {
|
||||
err := admin.NewError(admin.ErrorBadRequestType, "webhook secret cannot be updated")
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
newWebhook.Secret = wh.Secret
|
||||
if newWebhook.Id != "" && newWebhook.Id != wh.Id {
|
||||
err := admin.NewError(admin.ErrorBadRequestType, "webhook ID cannot be updated")
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
newWebhook.Id = wh.Id
|
||||
prov.Webhooks[i] = newWebhook
|
||||
found = true
|
||||
break
|
||||
}
|
||||
if !found {
|
||||
msg := fmt.Sprintf("provisioner %q has no webhook with the name %q", prov.Name, newWebhook.Name)
|
||||
err := admin.NewError(admin.ErrorNotFoundType, msg)
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := auth.UpdateProvisioner(ctx, prov); err != nil {
|
||||
if isBadRequest(err) {
|
||||
render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error updating provisioner webhook"))
|
||||
return
|
||||
}
|
||||
|
||||
render.Error(w, admin.WrapErrorISE(err, "error updating provisioner webhook"))
|
||||
return
|
||||
}
|
||||
|
||||
// Return a copy without the signing secret. Include the client-supplied
|
||||
// auth secrets since those may have been updated in this request and we
|
||||
// should show in the response that they changed
|
||||
whResponse := &linkedca.Webhook{
|
||||
Id: newWebhook.Id,
|
||||
Name: newWebhook.Name,
|
||||
Url: newWebhook.Url,
|
||||
Kind: newWebhook.Kind,
|
||||
CertType: newWebhook.CertType,
|
||||
Auth: newWebhook.Auth,
|
||||
DisableTlsClientAuth: newWebhook.DisableTlsClientAuth,
|
||||
}
|
||||
render.ProtoJSONStatus(w, whResponse, http.StatusCreated)
|
||||
}
|
668
authority/admin/api/webhook_test.go
Normal file
668
authority/admin/api/webhook_test.go
Normal file
|
@ -0,0 +1,668 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/smallstep/certificates/authority"
|
||||
"github.com/smallstep/certificates/authority/admin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go.step.sm/linkedca"
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
)
|
||||
|
||||
// ignore secret and id since those are set by the server
|
||||
func assertEqualWebhook(t *testing.T, a, b *linkedca.Webhook) {
|
||||
assert.Equal(t, a.Name, b.Name)
|
||||
assert.Equal(t, a.Url, b.Url)
|
||||
assert.Equal(t, a.Kind, b.Kind)
|
||||
assert.Equal(t, a.CertType, b.CertType)
|
||||
assert.Equal(t, a.DisableTlsClientAuth, b.DisableTlsClientAuth)
|
||||
|
||||
assert.Equal(t, a.GetAuth(), b.GetAuth())
|
||||
}
|
||||
|
||||
func TestWebhookAdminResponder_CreateProvisionerWebhook(t *testing.T) {
|
||||
type test struct {
|
||||
auth adminAuthority
|
||||
body []byte
|
||||
ctx context.Context
|
||||
err *admin.Error
|
||||
response *linkedca.Webhook
|
||||
statusCode int
|
||||
}
|
||||
var tests = map[string]func(t *testing.T) test{
|
||||
"fail/existing-webhook": func(t *testing.T) test {
|
||||
webhook := &linkedca.Webhook{
|
||||
Name: "already-exists",
|
||||
Url: "https://example.com",
|
||||
}
|
||||
prov := &linkedca.Provisioner{
|
||||
Name: "provName",
|
||||
Webhooks: []*linkedca.Webhook{webhook},
|
||||
}
|
||||
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
|
||||
err := admin.NewError(admin.ErrorConflictType, `provisioner "provName" already has a webhook with the name "already-exists"`)
|
||||
err.Message = `provisioner "provName" already has a webhook with the name "already-exists"`
|
||||
body := []byte(`
|
||||
{
|
||||
"name": "already-exists",
|
||||
"url": "https://example.com",
|
||||
"kind": "ENRICHING"
|
||||
}`)
|
||||
return test{
|
||||
ctx: ctx,
|
||||
body: body,
|
||||
err: err,
|
||||
statusCode: 409,
|
||||
}
|
||||
},
|
||||
"fail/read.ProtoJSON": func(t *testing.T) test {
|
||||
prov := &linkedca.Provisioner{
|
||||
Name: "provName",
|
||||
}
|
||||
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
|
||||
adminErr := admin.NewError(admin.ErrorBadRequestType, "proto: syntax error (line 1:2): invalid value ?")
|
||||
adminErr.Message = "proto: syntax error (line 1:2): invalid value ?"
|
||||
body := []byte("{?}")
|
||||
return test{
|
||||
ctx: ctx,
|
||||
body: body,
|
||||
err: adminErr,
|
||||
statusCode: 400,
|
||||
}
|
||||
},
|
||||
"fail/missing-name": func(t *testing.T) test {
|
||||
prov := &linkedca.Provisioner{
|
||||
Name: "provName",
|
||||
}
|
||||
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
|
||||
adminErr := admin.NewError(admin.ErrorBadRequestType, "webhook name is required")
|
||||
adminErr.Message = "webhook name is required"
|
||||
body := []byte(`{"url": "https://example.com", "kind": "ENRICHING"}`)
|
||||
return test{
|
||||
ctx: ctx,
|
||||
body: body,
|
||||
err: adminErr,
|
||||
statusCode: 400,
|
||||
}
|
||||
},
|
||||
"fail/missing-url": func(t *testing.T) test {
|
||||
prov := &linkedca.Provisioner{
|
||||
Name: "provName",
|
||||
}
|
||||
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
|
||||
adminErr := admin.NewError(admin.ErrorBadRequestType, "webhook url is invalid")
|
||||
adminErr.Message = "webhook url is invalid"
|
||||
body := []byte(`{"name": "metadata", "kind": "ENRICHING"}`)
|
||||
return test{
|
||||
ctx: ctx,
|
||||
body: body,
|
||||
err: adminErr,
|
||||
statusCode: 400,
|
||||
}
|
||||
},
|
||||
"fail/relative-url": func(t *testing.T) test {
|
||||
prov := &linkedca.Provisioner{
|
||||
Name: "provName",
|
||||
}
|
||||
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
|
||||
adminErr := admin.NewError(admin.ErrorBadRequestType, "webhook url is invalid")
|
||||
adminErr.Message = "webhook url is invalid"
|
||||
body := []byte(`{"name": "metadata", "url": "example.com/path", "kind": "ENRICHING"}`)
|
||||
return test{
|
||||
ctx: ctx,
|
||||
body: body,
|
||||
err: adminErr,
|
||||
statusCode: 400,
|
||||
}
|
||||
},
|
||||
"fail/http-url": func(t *testing.T) test {
|
||||
prov := &linkedca.Provisioner{
|
||||
Name: "provName",
|
||||
}
|
||||
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
|
||||
adminErr := admin.NewError(admin.ErrorBadRequestType, "webhook url must use https")
|
||||
adminErr.Message = "webhook url must use https"
|
||||
body := []byte(`{"name": "metadata", "url": "http://example.com", "kind": "ENRICHING"}`)
|
||||
return test{
|
||||
ctx: ctx,
|
||||
body: body,
|
||||
err: adminErr,
|
||||
statusCode: 400,
|
||||
}
|
||||
},
|
||||
"fail/basic-auth-in-url": func(t *testing.T) test {
|
||||
prov := &linkedca.Provisioner{
|
||||
Name: "provName",
|
||||
}
|
||||
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
|
||||
adminErr := admin.NewError(admin.ErrorBadRequestType, "webhook url may not contain username or password")
|
||||
adminErr.Message = "webhook url may not contain username or password"
|
||||
body := []byte(`
|
||||
{
|
||||
"name": "metadata",
|
||||
"url": "https://user:pass@example.com",
|
||||
"kind": "ENRICHING"
|
||||
}`)
|
||||
return test{
|
||||
ctx: ctx,
|
||||
body: body,
|
||||
err: adminErr,
|
||||
statusCode: 400,
|
||||
}
|
||||
},
|
||||
"fail/secret-in-request": func(t *testing.T) test {
|
||||
prov := &linkedca.Provisioner{
|
||||
Name: "provName",
|
||||
}
|
||||
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
|
||||
adminErr := admin.NewError(admin.ErrorBadRequestType, "webhook secret must not be set")
|
||||
adminErr.Message = "webhook secret must not be set"
|
||||
body := []byte(`
|
||||
{
|
||||
"name": "metadata",
|
||||
"url": "https://example.com",
|
||||
"kind": "ENRICHING",
|
||||
"secret": "secret"
|
||||
}`)
|
||||
return test{
|
||||
ctx: ctx,
|
||||
body: body,
|
||||
err: adminErr,
|
||||
statusCode: 400,
|
||||
}
|
||||
},
|
||||
"fail/auth.UpdateProvisioner-error": func(t *testing.T) test {
|
||||
adm := &linkedca.Admin{
|
||||
Subject: "step",
|
||||
}
|
||||
prov := &linkedca.Provisioner{
|
||||
Name: "provName",
|
||||
}
|
||||
ctx := linkedca.NewContextWithAdmin(context.Background(), adm)
|
||||
ctx = linkedca.NewContextWithProvisioner(ctx, prov)
|
||||
adminErr := admin.NewError(admin.ErrorServerInternalType, "error creating provisioner webhook: force")
|
||||
adminErr.Message = "error creating provisioner webhook: force"
|
||||
body := []byte(`{"name": "metadata", "url": "https://example.com", "kind": "ENRICHING"}`)
|
||||
return test{
|
||||
ctx: ctx,
|
||||
auth: &mockAdminAuthority{
|
||||
MockUpdateProvisioner: func(ctx context.Context, nu *linkedca.Provisioner) error {
|
||||
return &authority.PolicyError{
|
||||
Typ: authority.StoreFailure,
|
||||
Err: errors.New("force"),
|
||||
}
|
||||
},
|
||||
},
|
||||
body: body,
|
||||
err: adminErr,
|
||||
statusCode: 500,
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) test {
|
||||
prov := &linkedca.Provisioner{
|
||||
Name: "provName",
|
||||
}
|
||||
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
|
||||
body := []byte(`{"name": "metadata", "url": "https://example.com", "kind": "ENRICHING", "certType": "X509"}`)
|
||||
return test{
|
||||
ctx: ctx,
|
||||
auth: &mockAdminAuthority{
|
||||
MockUpdateProvisioner: func(ctx context.Context, nu *linkedca.Provisioner) error {
|
||||
assert.Equal(t, linkedca.Webhook_X509, nu.Webhooks[0].CertType)
|
||||
return nil
|
||||
},
|
||||
},
|
||||
body: body,
|
||||
response: &linkedca.Webhook{
|
||||
Name: "metadata",
|
||||
Url: "https://example.com",
|
||||
Kind: linkedca.Webhook_ENRICHING,
|
||||
CertType: linkedca.Webhook_X509,
|
||||
},
|
||||
statusCode: 201,
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, prep := range tests {
|
||||
tc := prep(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
mockMustAuthority(t, tc.auth)
|
||||
ctx := admin.NewContext(tc.ctx, &admin.MockDB{})
|
||||
war := NewWebhookAdminResponder()
|
||||
|
||||
req := httptest.NewRequest("POST", "/foo", io.NopCloser(bytes.NewBuffer(tc.body)))
|
||||
req = req.WithContext(ctx)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
war.CreateProvisionerWebhook(w, req)
|
||||
res := w.Result()
|
||||
|
||||
assert.Equal(t, tc.statusCode, res.StatusCode)
|
||||
|
||||
if res.StatusCode >= 400 {
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
assert.NoError(t, err)
|
||||
|
||||
ae := testAdminError{}
|
||||
assert.NoError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))
|
||||
|
||||
assert.Equal(t, tc.err.Type, ae.Type)
|
||||
assert.Equal(t, tc.err.StatusCode(), res.StatusCode)
|
||||
assert.Equal(t, tc.err.Detail, ae.Detail)
|
||||
assert.Equal(t, []string{"application/json"}, res.Header["Content-Type"])
|
||||
|
||||
// when the error message starts with "proto", we expect it to have
|
||||
// a syntax error (in the tests). If the message doesn't start with "proto",
|
||||
// we expect a full string match.
|
||||
if strings.HasPrefix(tc.err.Message, "proto:") {
|
||||
assert.True(t, strings.Contains(ae.Message, "syntax error"))
|
||||
} else {
|
||||
assert.Equal(t, tc.err.Message, ae.Message)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
resp := &linkedca.Webhook{}
|
||||
body, err := io.ReadAll(res.Body)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, protojson.Unmarshal(body, resp))
|
||||
|
||||
assertEqualWebhook(t, tc.response, resp)
|
||||
assert.NotEmpty(t, resp.Secret)
|
||||
assert.NotEmpty(t, resp.Id)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebhookAdminResponder_DeleteProvisionerWebhook(t *testing.T) {
|
||||
type test struct {
|
||||
auth adminAuthority
|
||||
err *admin.Error
|
||||
statusCode int
|
||||
provisionerWebhooks []*linkedca.Webhook
|
||||
webhookName string
|
||||
}
|
||||
var tests = map[string]func(t *testing.T) test{
|
||||
"fail/auth.UpdateProvisioner-error": func(t *testing.T) test {
|
||||
adminErr := admin.NewError(admin.ErrorServerInternalType, "error deleting provisioner webhook: force")
|
||||
adminErr.Message = "error deleting provisioner webhook: force"
|
||||
return test{
|
||||
err: adminErr,
|
||||
auth: &mockAdminAuthority{
|
||||
MockUpdateProvisioner: func(ctx context.Context, nu *linkedca.Provisioner) error {
|
||||
return &authority.PolicyError{
|
||||
Typ: authority.StoreFailure,
|
||||
Err: errors.New("force"),
|
||||
}
|
||||
},
|
||||
},
|
||||
statusCode: 500,
|
||||
webhookName: "my-webhook",
|
||||
provisionerWebhooks: []*linkedca.Webhook{
|
||||
{Name: "my-webhook", Url: "https://example.com", Kind: linkedca.Webhook_ENRICHING},
|
||||
},
|
||||
}
|
||||
},
|
||||
"ok/not-found": func(t *testing.T) test {
|
||||
return test{
|
||||
statusCode: 200,
|
||||
webhookName: "no-exists",
|
||||
provisionerWebhooks: nil,
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) test {
|
||||
return test{
|
||||
statusCode: 200,
|
||||
webhookName: "exists",
|
||||
auth: &mockAdminAuthority{
|
||||
MockUpdateProvisioner: func(ctx context.Context, nu *linkedca.Provisioner) error {
|
||||
assert.Equal(t, nu.Webhooks, []*linkedca.Webhook{
|
||||
{Name: "my-2nd-webhook", Url: "https://example.com", Kind: linkedca.Webhook_ENRICHING},
|
||||
})
|
||||
return nil
|
||||
},
|
||||
},
|
||||
provisionerWebhooks: []*linkedca.Webhook{
|
||||
{Name: "exists", Url: "https.example.com", Kind: linkedca.Webhook_ENRICHING},
|
||||
{Name: "my-2nd-webhook", Url: "https://example.com", Kind: linkedca.Webhook_ENRICHING},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, prep := range tests {
|
||||
tc := prep(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
mockMustAuthority(t, tc.auth)
|
||||
|
||||
chiCtx := chi.NewRouteContext()
|
||||
chiCtx.URLParams.Add("webhookName", tc.webhookName)
|
||||
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
||||
prov := &linkedca.Provisioner{
|
||||
Name: "provName",
|
||||
Webhooks: tc.provisionerWebhooks,
|
||||
}
|
||||
ctx = linkedca.NewContextWithProvisioner(ctx, prov)
|
||||
ctx = admin.NewContext(ctx, &admin.MockDB{})
|
||||
req := httptest.NewRequest("DELETE", "/foo", nil).WithContext(ctx)
|
||||
|
||||
war := NewWebhookAdminResponder()
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
war.DeleteProvisionerWebhook(w, req)
|
||||
res := w.Result()
|
||||
|
||||
assert.Equal(t, tc.statusCode, res.StatusCode)
|
||||
|
||||
if res.StatusCode >= 400 {
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
assert.NoError(t, err)
|
||||
|
||||
ae := testAdminError{}
|
||||
assert.NoError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))
|
||||
|
||||
assert.Equal(t, tc.err.Type, ae.Type)
|
||||
assert.Equal(t, tc.err.StatusCode(), res.StatusCode)
|
||||
assert.Equal(t, tc.err.Detail, ae.Detail)
|
||||
assert.Equal(t, []string{"application/json"}, res.Header["Content-Type"])
|
||||
|
||||
// when the error message starts with "proto", we expect it to have
|
||||
// a syntax error (in the tests). If the message doesn't start with "proto",
|
||||
// we expect a full string match.
|
||||
if strings.HasPrefix(tc.err.Message, "proto:") {
|
||||
assert.True(t, strings.Contains(ae.Message, "syntax error"))
|
||||
} else {
|
||||
assert.Equal(t, tc.err.Message, ae.Message)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
assert.NoError(t, err)
|
||||
res.Body.Close()
|
||||
response := DeleteResponse{}
|
||||
assert.NoError(t, json.Unmarshal(bytes.TrimSpace(body), &response))
|
||||
assert.Equal(t, "ok", response.Status)
|
||||
assert.Equal(t, []string{"application/json"}, res.Header["Content-Type"])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebhookAdminResponder_UpdateProvisionerWebhook(t *testing.T) {
|
||||
type test struct {
|
||||
auth adminAuthority
|
||||
adminDB admin.DB
|
||||
body []byte
|
||||
ctx context.Context
|
||||
err *admin.Error
|
||||
response *linkedca.Webhook
|
||||
statusCode int
|
||||
}
|
||||
var tests = map[string]func(t *testing.T) test{
|
||||
"fail/not-found": func(t *testing.T) test {
|
||||
prov := &linkedca.Provisioner{
|
||||
Name: "provName",
|
||||
Webhooks: []*linkedca.Webhook{{Name: "exists", Url: "https://example.com", Kind: linkedca.Webhook_ENRICHING}},
|
||||
}
|
||||
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
|
||||
err := admin.NewError(admin.ErrorNotFoundType, `provisioner "provName" has no webhook with the name "no-exists"`)
|
||||
err.Message = `provisioner "provName" has no webhook with the name "no-exists"`
|
||||
body := []byte(`
|
||||
{
|
||||
"name": "no-exists",
|
||||
"url": "https://example.com",
|
||||
"kind": "ENRICHING"
|
||||
}`)
|
||||
return test{
|
||||
ctx: ctx,
|
||||
adminDB: &admin.MockDB{},
|
||||
body: body,
|
||||
err: err,
|
||||
statusCode: 404,
|
||||
}
|
||||
},
|
||||
"fail/read.ProtoJSON": func(t *testing.T) test {
|
||||
prov := &linkedca.Provisioner{
|
||||
Name: "provName",
|
||||
Webhooks: []*linkedca.Webhook{{Name: "my-webhook", Url: "https://example.com", Kind: linkedca.Webhook_ENRICHING}},
|
||||
}
|
||||
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
|
||||
adminErr := admin.NewError(admin.ErrorBadRequestType, "proto: syntax error (line 1:2): invalid value ?")
|
||||
adminErr.Message = "proto: syntax error (line 1:2): invalid value ?"
|
||||
body := []byte("{?}")
|
||||
return test{
|
||||
ctx: ctx,
|
||||
adminDB: &admin.MockDB{},
|
||||
body: body,
|
||||
err: adminErr,
|
||||
statusCode: 400,
|
||||
}
|
||||
},
|
||||
"fail/missing-name": func(t *testing.T) test {
|
||||
prov := &linkedca.Provisioner{
|
||||
Name: "provName",
|
||||
Webhooks: []*linkedca.Webhook{{Name: "my-webhook", Url: "https://example.com", Kind: linkedca.Webhook_ENRICHING}},
|
||||
}
|
||||
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
|
||||
adminErr := admin.NewError(admin.ErrorBadRequestType, "webhook name is required")
|
||||
adminErr.Message = "webhook name is required"
|
||||
body := []byte(`{"url": "https://example.com", "kind": "ENRICHING"}`)
|
||||
return test{
|
||||
ctx: ctx,
|
||||
adminDB: &admin.MockDB{},
|
||||
body: body,
|
||||
err: adminErr,
|
||||
statusCode: 400,
|
||||
}
|
||||
},
|
||||
"fail/missing-url": func(t *testing.T) test {
|
||||
prov := &linkedca.Provisioner{
|
||||
Name: "provName",
|
||||
Webhooks: []*linkedca.Webhook{{Name: "my-webhook", Url: "https://example.com", Kind: linkedca.Webhook_ENRICHING}},
|
||||
}
|
||||
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
|
||||
adminErr := admin.NewError(admin.ErrorBadRequestType, "webhook url is invalid")
|
||||
adminErr.Message = "webhook url is invalid"
|
||||
body := []byte(`{"name": "metadata", "kind": "ENRICHING"}`)
|
||||
return test{
|
||||
ctx: ctx,
|
||||
adminDB: &admin.MockDB{},
|
||||
body: body,
|
||||
err: adminErr,
|
||||
statusCode: 400,
|
||||
}
|
||||
},
|
||||
"fail/relative-url": func(t *testing.T) test {
|
||||
prov := &linkedca.Provisioner{
|
||||
Name: "provName",
|
||||
Webhooks: []*linkedca.Webhook{{Name: "my-webhook", Url: "https://example.com", Kind: linkedca.Webhook_ENRICHING}},
|
||||
}
|
||||
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
|
||||
adminErr := admin.NewError(admin.ErrorBadRequestType, "webhook url is invalid")
|
||||
adminErr.Message = "webhook url is invalid"
|
||||
body := []byte(`{"name": "metadata", "url": "example.com/path", "kind": "ENRICHING"}`)
|
||||
return test{
|
||||
ctx: ctx,
|
||||
adminDB: &admin.MockDB{},
|
||||
body: body,
|
||||
err: adminErr,
|
||||
statusCode: 400,
|
||||
}
|
||||
},
|
||||
"fail/http-url": func(t *testing.T) test {
|
||||
prov := &linkedca.Provisioner{
|
||||
Name: "provName",
|
||||
Webhooks: []*linkedca.Webhook{{Name: "my-webhook", Url: "https://example.com", Kind: linkedca.Webhook_ENRICHING}},
|
||||
}
|
||||
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
|
||||
adminErr := admin.NewError(admin.ErrorBadRequestType, "webhook url must use https")
|
||||
adminErr.Message = "webhook url must use https"
|
||||
body := []byte(`{"name": "metadata", "url": "http://example.com", "kind": "ENRICHING"}`)
|
||||
return test{
|
||||
ctx: ctx,
|
||||
adminDB: &admin.MockDB{},
|
||||
body: body,
|
||||
err: adminErr,
|
||||
statusCode: 400,
|
||||
}
|
||||
},
|
||||
"fail/basic-auth-in-url": func(t *testing.T) test {
|
||||
prov := &linkedca.Provisioner{
|
||||
Name: "provName",
|
||||
Webhooks: []*linkedca.Webhook{{Name: "my-webhook", Url: "https://example.com", Kind: linkedca.Webhook_ENRICHING}},
|
||||
}
|
||||
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
|
||||
adminErr := admin.NewError(admin.ErrorBadRequestType, "webhook url may not contain username or password")
|
||||
adminErr.Message = "webhook url may not contain username or password"
|
||||
body := []byte(`
|
||||
{
|
||||
"name": "my-webhook",
|
||||
"url": "https://user:pass@example.com",
|
||||
"kind": "ENRICHING"
|
||||
}`)
|
||||
return test{
|
||||
ctx: ctx,
|
||||
adminDB: &admin.MockDB{},
|
||||
body: body,
|
||||
err: adminErr,
|
||||
statusCode: 400,
|
||||
}
|
||||
},
|
||||
"fail/different-secret-in-request": func(t *testing.T) test {
|
||||
prov := &linkedca.Provisioner{
|
||||
Name: "provName",
|
||||
Webhooks: []*linkedca.Webhook{{Name: "my-webhook", Url: "https://example.com", Kind: linkedca.Webhook_ENRICHING, Secret: "c2VjcmV0"}},
|
||||
}
|
||||
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
|
||||
adminErr := admin.NewError(admin.ErrorBadRequestType, "webhook secret cannot be updated")
|
||||
adminErr.Message = "webhook secret cannot be updated"
|
||||
body := []byte(`
|
||||
{
|
||||
"name": "my-webhook",
|
||||
"url": "https://example.com",
|
||||
"kind": "ENRICHING",
|
||||
"secret": "secret"
|
||||
}`)
|
||||
return test{
|
||||
ctx: ctx,
|
||||
body: body,
|
||||
err: adminErr,
|
||||
statusCode: 400,
|
||||
}
|
||||
},
|
||||
"fail/auth.UpdateProvisioner-error": func(t *testing.T) test {
|
||||
prov := &linkedca.Provisioner{
|
||||
Name: "provName",
|
||||
Webhooks: []*linkedca.Webhook{{Name: "my-webhook", Url: "https://example.com", Kind: linkedca.Webhook_ENRICHING}},
|
||||
}
|
||||
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
|
||||
adminErr := admin.NewError(admin.ErrorServerInternalType, "error updating provisioner webhook: force")
|
||||
adminErr.Message = "error updating provisioner webhook: force"
|
||||
body := []byte(`{"name": "my-webhook", "url": "https://example.com", "kind": "ENRICHING"}`)
|
||||
return test{
|
||||
ctx: ctx,
|
||||
adminDB: &admin.MockDB{},
|
||||
auth: &mockAdminAuthority{
|
||||
MockUpdateProvisioner: func(ctx context.Context, nu *linkedca.Provisioner) error {
|
||||
return &authority.PolicyError{
|
||||
Typ: authority.StoreFailure,
|
||||
Err: errors.New("force"),
|
||||
}
|
||||
},
|
||||
},
|
||||
body: body,
|
||||
err: adminErr,
|
||||
statusCode: 500,
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) test {
|
||||
prov := &linkedca.Provisioner{
|
||||
Name: "provName",
|
||||
Webhooks: []*linkedca.Webhook{{Name: "my-webhook", Url: "https://example.com", Kind: linkedca.Webhook_ENRICHING}},
|
||||
}
|
||||
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
|
||||
body := []byte(`{"name": "my-webhook", "url": "https://example.com", "kind": "ENRICHING"}`)
|
||||
return test{
|
||||
ctx: ctx,
|
||||
adminDB: &admin.MockDB{},
|
||||
auth: &mockAdminAuthority{
|
||||
MockUpdateProvisioner: func(ctx context.Context, nu *linkedca.Provisioner) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
body: body,
|
||||
response: &linkedca.Webhook{
|
||||
Name: "my-webhook",
|
||||
Url: "https://example.com",
|
||||
Kind: linkedca.Webhook_ENRICHING,
|
||||
},
|
||||
statusCode: 201,
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, prep := range tests {
|
||||
tc := prep(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
mockMustAuthority(t, tc.auth)
|
||||
ctx := admin.NewContext(tc.ctx, tc.adminDB)
|
||||
war := NewWebhookAdminResponder()
|
||||
|
||||
req := httptest.NewRequest("PUT", "/foo", io.NopCloser(bytes.NewBuffer(tc.body)))
|
||||
req = req.WithContext(ctx)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
war.UpdateProvisionerWebhook(w, req)
|
||||
res := w.Result()
|
||||
|
||||
assert.Equal(t, tc.statusCode, res.StatusCode)
|
||||
|
||||
if res.StatusCode >= 400 {
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
assert.NoError(t, err)
|
||||
|
||||
ae := testAdminError{}
|
||||
assert.NoError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))
|
||||
|
||||
assert.Equal(t, tc.err.Type, ae.Type)
|
||||
assert.Equal(t, tc.err.StatusCode(), res.StatusCode)
|
||||
assert.Equal(t, tc.err.Detail, ae.Detail)
|
||||
assert.Equal(t, []string{"application/json"}, res.Header["Content-Type"])
|
||||
|
||||
// when the error message starts with "proto", we expect it to have
|
||||
// a syntax error (in the tests). If the message doesn't start with "proto",
|
||||
// we expect a full string match.
|
||||
if strings.HasPrefix(tc.err.Message, "proto:") {
|
||||
assert.True(t, strings.Contains(ae.Message, "syntax error"))
|
||||
} else {
|
||||
assert.Equal(t, tc.err.Message, ae.Message)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
resp := &linkedca.Webhook{}
|
||||
body, err := io.ReadAll(res.Body)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, protojson.Unmarshal(body, resp))
|
||||
|
||||
assertEqualWebhook(t, tc.response, resp)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -24,6 +24,24 @@ type dbProvisioner struct {
|
|||
SSHTemplate *linkedca.Template `json:"sshTemplate"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
DeletedAt time.Time `json:"deletedAt"`
|
||||
Webhooks []dbWebhook `json:"webhooks,omitempty"`
|
||||
}
|
||||
|
||||
type dbBasicAuth struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type dbWebhook struct {
|
||||
Name string `json:"name"`
|
||||
ID string `json:"id"`
|
||||
URL string `json:"url"`
|
||||
Kind string `json:"kind"`
|
||||
Secret string `json:"secret"`
|
||||
BearerToken string `json:"bearerToken,omitempty"`
|
||||
BasicAuth *dbBasicAuth `json:"basicAuth,omitempty"`
|
||||
DisableTLSClientAuth bool `json:"disableTLSClientAuth,omitempty"`
|
||||
CertType string `json:"certType,omitempty"`
|
||||
}
|
||||
|
||||
func (dbp *dbProvisioner) clone() *dbProvisioner {
|
||||
|
@ -48,6 +66,7 @@ func (dbp *dbProvisioner) convert2linkedca() (*linkedca.Provisioner, error) {
|
|||
SshTemplate: dbp.SSHTemplate,
|
||||
CreatedAt: timestamppb.New(dbp.CreatedAt),
|
||||
DeletedAt: timestamppb.New(dbp.DeletedAt),
|
||||
Webhooks: dbWebhooksToLinkedca(dbp.Webhooks),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -164,6 +183,7 @@ func (db *DB) CreateProvisioner(ctx context.Context, prov *linkedca.Provisioner)
|
|||
X509Template: prov.X509Template,
|
||||
SSHTemplate: prov.SshTemplate,
|
||||
CreatedAt: clock.Now(),
|
||||
Webhooks: linkedcaWebhooksToDB(prov.Webhooks),
|
||||
}
|
||||
|
||||
if err := db.save(ctx, prov.Id, dbp, nil, "provisioner", provisionersTable); err != nil {
|
||||
|
@ -193,6 +213,7 @@ func (db *DB) UpdateProvisioner(ctx context.Context, prov *linkedca.Provisioner)
|
|||
}
|
||||
nu.X509Template = prov.X509Template
|
||||
nu.SSHTemplate = prov.SshTemplate
|
||||
nu.Webhooks = linkedcaWebhooksToDB(prov.Webhooks)
|
||||
|
||||
return db.save(ctx, prov.Id, nu, old, "provisioner", provisionersTable)
|
||||
}
|
||||
|
@ -209,3 +230,70 @@ func (db *DB) DeleteProvisioner(ctx context.Context, id string) error {
|
|||
|
||||
return db.save(ctx, old.ID, nu, old, "provisioner", provisionersTable)
|
||||
}
|
||||
|
||||
func dbWebhooksToLinkedca(dbwhs []dbWebhook) []*linkedca.Webhook {
|
||||
if len(dbwhs) == 0 {
|
||||
return nil
|
||||
}
|
||||
lwhs := make([]*linkedca.Webhook, len(dbwhs))
|
||||
|
||||
for i, dbwh := range dbwhs {
|
||||
lwh := &linkedca.Webhook{
|
||||
Name: dbwh.Name,
|
||||
Id: dbwh.ID,
|
||||
Url: dbwh.URL,
|
||||
Kind: linkedca.Webhook_Kind(linkedca.Webhook_Kind_value[dbwh.Kind]),
|
||||
Secret: dbwh.Secret,
|
||||
DisableTlsClientAuth: dbwh.DisableTLSClientAuth,
|
||||
CertType: linkedca.Webhook_CertType(linkedca.Webhook_CertType_value[dbwh.CertType]),
|
||||
}
|
||||
if dbwh.BearerToken != "" {
|
||||
lwh.Auth = &linkedca.Webhook_BearerToken{
|
||||
BearerToken: &linkedca.BearerToken{
|
||||
BearerToken: dbwh.BearerToken,
|
||||
},
|
||||
}
|
||||
} else if dbwh.BasicAuth != nil && (dbwh.BasicAuth.Username != "" || dbwh.BasicAuth.Password != "") {
|
||||
lwh.Auth = &linkedca.Webhook_BasicAuth{
|
||||
BasicAuth: &linkedca.BasicAuth{
|
||||
Username: dbwh.BasicAuth.Username,
|
||||
Password: dbwh.BasicAuth.Password,
|
||||
},
|
||||
}
|
||||
}
|
||||
lwhs[i] = lwh
|
||||
}
|
||||
|
||||
return lwhs
|
||||
}
|
||||
|
||||
func linkedcaWebhooksToDB(lwhs []*linkedca.Webhook) []dbWebhook {
|
||||
if len(lwhs) == 0 {
|
||||
return nil
|
||||
}
|
||||
dbwhs := make([]dbWebhook, len(lwhs))
|
||||
|
||||
for i, lwh := range lwhs {
|
||||
dbwh := dbWebhook{
|
||||
Name: lwh.Name,
|
||||
ID: lwh.Id,
|
||||
URL: lwh.Url,
|
||||
Kind: lwh.Kind.String(),
|
||||
Secret: lwh.Secret,
|
||||
DisableTLSClientAuth: lwh.DisableTlsClientAuth,
|
||||
CertType: lwh.CertType.String(),
|
||||
}
|
||||
switch a := lwh.GetAuth().(type) {
|
||||
case *linkedca.Webhook_BearerToken:
|
||||
dbwh.BearerToken = a.BearerToken.BearerToken
|
||||
case *linkedca.Webhook_BasicAuth:
|
||||
dbwh.BasicAuth = &dbBasicAuth{
|
||||
Username: a.BasicAuth.Username,
|
||||
Password: a.BasicAuth.Password,
|
||||
}
|
||||
}
|
||||
dbwhs[i] = dbwh
|
||||
}
|
||||
|
||||
return dbwhs
|
||||
}
|
||||
|
|
|
@ -137,6 +137,7 @@ func TestDB_getDBProvisioner(t *testing.T) {
|
|||
}
|
||||
},
|
||||
"fail/deleted": func(t *testing.T) test {
|
||||
|
||||
now := clock.Now()
|
||||
dbp := &dbProvisioner{
|
||||
ID: provID,
|
||||
|
@ -210,6 +211,7 @@ func TestDB_getDBProvisioner(t *testing.T) {
|
|||
assert.Equals(t, dbp.Name, tc.dbp.Name)
|
||||
assert.Equals(t, dbp.CreatedAt, tc.dbp.CreatedAt)
|
||||
assert.Fatal(t, dbp.DeletedAt.IsZero())
|
||||
assert.Equals(t, dbp.Webhooks, tc.dbp.Webhooks)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -300,6 +302,7 @@ func TestDB_unmarshalDBProvisioner(t *testing.T) {
|
|||
assert.Equals(t, dbp.SSHTemplate, tc.dbp.SSHTemplate)
|
||||
assert.Equals(t, dbp.CreatedAt, tc.dbp.CreatedAt)
|
||||
assert.Fatal(t, dbp.DeletedAt.IsZero())
|
||||
assert.Equals(t, dbp.Webhooks, tc.dbp.Webhooks)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -353,6 +356,15 @@ func defaultDBP(t *testing.T) *dbProvisioner {
|
|||
Data: []byte("zap"),
|
||||
},
|
||||
CreatedAt: clock.Now(),
|
||||
Webhooks: []dbWebhook{
|
||||
{
|
||||
Name: "metadata",
|
||||
URL: "https://inventory.smallstep.com",
|
||||
Kind: linkedca.Webhook_ENRICHING.String(),
|
||||
Secret: "secret",
|
||||
BearerToken: "token",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -419,6 +431,7 @@ func TestDB_unmarshalProvisioner(t *testing.T) {
|
|||
assert.Equals(t, prov.Claims, tc.dbp.Claims)
|
||||
assert.Equals(t, prov.X509Template, tc.dbp.X509Template)
|
||||
assert.Equals(t, prov.SshTemplate, tc.dbp.SSHTemplate)
|
||||
assert.Equals(t, prov.Webhooks, dbWebhooksToLinkedca(tc.dbp.Webhooks))
|
||||
|
||||
retDetailsBytes, err := json.Marshal(prov.Details.GetData())
|
||||
assert.FatalError(t, err)
|
||||
|
@ -557,6 +570,7 @@ func TestDB_GetProvisioner(t *testing.T) {
|
|||
assert.Equals(t, prov.Claims, tc.dbp.Claims)
|
||||
assert.Equals(t, prov.X509Template, tc.dbp.X509Template)
|
||||
assert.Equals(t, prov.SshTemplate, tc.dbp.SSHTemplate)
|
||||
assert.Equals(t, prov.Webhooks, dbWebhooksToLinkedca(tc.dbp.Webhooks))
|
||||
|
||||
retDetailsBytes, err := json.Marshal(prov.Details.GetData())
|
||||
assert.FatalError(t, err)
|
||||
|
@ -629,6 +643,7 @@ func TestDB_DeleteProvisioner(t *testing.T) {
|
|||
assert.Equals(t, _dbp.SSHTemplate, dbp.SSHTemplate)
|
||||
assert.Equals(t, _dbp.CreatedAt, dbp.CreatedAt)
|
||||
assert.Equals(t, _dbp.Details, dbp.Details)
|
||||
assert.Equals(t, _dbp.Webhooks, dbp.Webhooks)
|
||||
|
||||
assert.True(t, _dbp.DeletedAt.Before(time.Now()))
|
||||
assert.True(t, _dbp.DeletedAt.After(time.Now().Add(-time.Minute)))
|
||||
|
@ -668,6 +683,7 @@ func TestDB_DeleteProvisioner(t *testing.T) {
|
|||
assert.Equals(t, _dbp.SSHTemplate, dbp.SSHTemplate)
|
||||
assert.Equals(t, _dbp.CreatedAt, dbp.CreatedAt)
|
||||
assert.Equals(t, _dbp.Details, dbp.Details)
|
||||
assert.Equals(t, _dbp.Webhooks, dbp.Webhooks)
|
||||
|
||||
assert.True(t, _dbp.DeletedAt.Before(time.Now()))
|
||||
assert.True(t, _dbp.DeletedAt.After(time.Now().Add(-time.Minute)))
|
||||
|
@ -819,6 +835,7 @@ func TestDB_GetProvisioners(t *testing.T) {
|
|||
assert.Equals(t, provs[0].Claims, fooProv.Claims)
|
||||
assert.Equals(t, provs[0].X509Template, fooProv.X509Template)
|
||||
assert.Equals(t, provs[0].SshTemplate, fooProv.SSHTemplate)
|
||||
assert.Equals(t, provs[0].Webhooks, dbWebhooksToLinkedca(fooProv.Webhooks))
|
||||
|
||||
retDetailsBytes, err := json.Marshal(provs[0].Details.GetData())
|
||||
assert.FatalError(t, err)
|
||||
|
@ -831,6 +848,7 @@ func TestDB_GetProvisioners(t *testing.T) {
|
|||
assert.Equals(t, provs[1].Claims, zapProv.Claims)
|
||||
assert.Equals(t, provs[1].X509Template, zapProv.X509Template)
|
||||
assert.Equals(t, provs[1].SshTemplate, zapProv.SSHTemplate)
|
||||
assert.Equals(t, provs[1].Webhooks, dbWebhooksToLinkedca(zapProv.Webhooks))
|
||||
|
||||
retDetailsBytes, err = json.Marshal(provs[1].Details.GetData())
|
||||
assert.FatalError(t, err)
|
||||
|
@ -895,6 +913,7 @@ func TestDB_CreateProvisioner(t *testing.T) {
|
|||
assert.Equals(t, _dbp.Claims, prov.Claims)
|
||||
assert.Equals(t, _dbp.X509Template, prov.X509Template)
|
||||
assert.Equals(t, _dbp.SSHTemplate, prov.SshTemplate)
|
||||
assert.Equals(t, _dbp.Webhooks, linkedcaWebhooksToDB(prov.Webhooks))
|
||||
|
||||
retDetailsBytes, err := json.Marshal(prov.Details.GetData())
|
||||
assert.FatalError(t, err)
|
||||
|
@ -932,6 +951,7 @@ func TestDB_CreateProvisioner(t *testing.T) {
|
|||
assert.Equals(t, _dbp.Claims, prov.Claims)
|
||||
assert.Equals(t, _dbp.X509Template, prov.X509Template)
|
||||
assert.Equals(t, _dbp.SSHTemplate, prov.SshTemplate)
|
||||
assert.Equals(t, _dbp.Webhooks, linkedcaWebhooksToDB(prov.Webhooks))
|
||||
|
||||
retDetailsBytes, err := json.Marshal(prov.Details.GetData())
|
||||
assert.FatalError(t, err)
|
||||
|
@ -1080,6 +1100,7 @@ func TestDB_UpdateProvisioner(t *testing.T) {
|
|||
assert.Equals(t, _dbp.Claims, prov.Claims)
|
||||
assert.Equals(t, _dbp.X509Template, prov.X509Template)
|
||||
assert.Equals(t, _dbp.SSHTemplate, prov.SshTemplate)
|
||||
assert.Equals(t, _dbp.Webhooks, linkedcaWebhooksToDB(prov.Webhooks))
|
||||
|
||||
retDetailsBytes, err := json.Marshal(prov.Details.GetData())
|
||||
assert.FatalError(t, err)
|
||||
|
@ -1141,6 +1162,12 @@ func TestDB_UpdateProvisioner(t *testing.T) {
|
|||
},
|
||||
},
|
||||
}
|
||||
prov.Webhooks = []*linkedca.Webhook{
|
||||
{
|
||||
Name: "users",
|
||||
Url: "https://example.com/users",
|
||||
},
|
||||
}
|
||||
|
||||
data, err := json.Marshal(dbp)
|
||||
assert.FatalError(t, err)
|
||||
|
@ -1168,6 +1195,7 @@ func TestDB_UpdateProvisioner(t *testing.T) {
|
|||
assert.Equals(t, _dbp.Claims, prov.Claims)
|
||||
assert.Equals(t, _dbp.X509Template, prov.X509Template)
|
||||
assert.Equals(t, _dbp.SSHTemplate, prov.SshTemplate)
|
||||
assert.Equals(t, _dbp.Webhooks, linkedcaWebhooksToDB(prov.Webhooks))
|
||||
|
||||
retDetailsBytes, err := json.Marshal(prov.Details.GetData())
|
||||
assert.FatalError(t, err)
|
||||
|
@ -1206,3 +1234,164 @@ func TestDB_UpdateProvisioner(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_linkedcaWebhooksToDB(t *testing.T) {
|
||||
type test struct {
|
||||
in []*linkedca.Webhook
|
||||
want []dbWebhook
|
||||
}
|
||||
var tests = map[string]test{
|
||||
"nil": {
|
||||
in: nil,
|
||||
want: nil,
|
||||
},
|
||||
"zero": {
|
||||
in: []*linkedca.Webhook{},
|
||||
want: nil,
|
||||
},
|
||||
"bearer": {
|
||||
in: []*linkedca.Webhook{
|
||||
{
|
||||
Name: "bearer",
|
||||
Url: "https://example.com",
|
||||
Kind: linkedca.Webhook_ENRICHING,
|
||||
Secret: "secret",
|
||||
Auth: &linkedca.Webhook_BearerToken{
|
||||
BearerToken: &linkedca.BearerToken{
|
||||
BearerToken: "token",
|
||||
},
|
||||
},
|
||||
DisableTlsClientAuth: true,
|
||||
CertType: linkedca.Webhook_X509,
|
||||
},
|
||||
},
|
||||
want: []dbWebhook{
|
||||
{
|
||||
Name: "bearer",
|
||||
URL: "https://example.com",
|
||||
Kind: "ENRICHING",
|
||||
Secret: "secret",
|
||||
BearerToken: "token",
|
||||
DisableTLSClientAuth: true,
|
||||
CertType: linkedca.Webhook_X509.String(),
|
||||
},
|
||||
},
|
||||
},
|
||||
"basic": {
|
||||
in: []*linkedca.Webhook{
|
||||
{
|
||||
Name: "basic",
|
||||
Url: "https://example.com",
|
||||
Kind: linkedca.Webhook_ENRICHING,
|
||||
Secret: "secret",
|
||||
Auth: &linkedca.Webhook_BasicAuth{
|
||||
BasicAuth: &linkedca.BasicAuth{
|
||||
Username: "user",
|
||||
Password: "pass",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []dbWebhook{
|
||||
{
|
||||
Name: "basic",
|
||||
URL: "https://example.com",
|
||||
Kind: "ENRICHING",
|
||||
Secret: "secret",
|
||||
BasicAuth: &dbBasicAuth{
|
||||
Username: "user",
|
||||
Password: "pass",
|
||||
},
|
||||
CertType: linkedca.Webhook_ALL.String(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got := linkedcaWebhooksToDB(tc.in)
|
||||
assert.Equals(t, tc.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_dbWebhooksToLinkedca(t *testing.T) {
|
||||
type test struct {
|
||||
in []dbWebhook
|
||||
want []*linkedca.Webhook
|
||||
}
|
||||
var tests = map[string]test{
|
||||
"nil": {
|
||||
in: nil,
|
||||
want: nil,
|
||||
},
|
||||
"zero": {
|
||||
in: []dbWebhook{},
|
||||
want: nil,
|
||||
},
|
||||
"bearer": {
|
||||
in: []dbWebhook{
|
||||
{
|
||||
Name: "bearer",
|
||||
ID: "69350cb6-6c31-4b5e-bf25-affd5053427d",
|
||||
URL: "https://example.com",
|
||||
Kind: "ENRICHING",
|
||||
Secret: "secret",
|
||||
BearerToken: "token",
|
||||
DisableTLSClientAuth: true,
|
||||
},
|
||||
},
|
||||
want: []*linkedca.Webhook{
|
||||
{
|
||||
Name: "bearer",
|
||||
Id: "69350cb6-6c31-4b5e-bf25-affd5053427d",
|
||||
Url: "https://example.com",
|
||||
Kind: linkedca.Webhook_ENRICHING,
|
||||
Secret: "secret",
|
||||
Auth: &linkedca.Webhook_BearerToken{
|
||||
BearerToken: &linkedca.BearerToken{
|
||||
BearerToken: "token",
|
||||
},
|
||||
},
|
||||
DisableTlsClientAuth: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
"basic": {
|
||||
in: []dbWebhook{
|
||||
{
|
||||
Name: "basic",
|
||||
ID: "69350cb6-6c31-4b5e-bf25-affd5053427d",
|
||||
URL: "https://example.com",
|
||||
Kind: "ENRICHING",
|
||||
Secret: "secret",
|
||||
BasicAuth: &dbBasicAuth{
|
||||
Username: "user",
|
||||
Password: "pass",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []*linkedca.Webhook{
|
||||
{
|
||||
Name: "basic",
|
||||
Id: "69350cb6-6c31-4b5e-bf25-affd5053427d",
|
||||
Url: "https://example.com",
|
||||
Kind: linkedca.Webhook_ENRICHING,
|
||||
Secret: "secret",
|
||||
Auth: &linkedca.Webhook_BasicAuth{
|
||||
BasicAuth: &linkedca.BasicAuth{
|
||||
Username: "user",
|
||||
Password: "pass",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got := dbWebhooksToLinkedca(tc.in)
|
||||
assert.Equals(t, tc.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"crypto/x509"
|
||||
"encoding/hex"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
@ -46,6 +47,7 @@ type Authority struct {
|
|||
adminDB admin.DB
|
||||
templates *templates.Templates
|
||||
linkedCAToken string
|
||||
webhookClient *http.Client
|
||||
|
||||
// X509 CA
|
||||
password []byte
|
||||
|
|
|
@ -491,7 +491,7 @@ func TestAuthority_authorizeSign(t *testing.T) {
|
|||
}
|
||||
} else {
|
||||
if assert.Nil(t, tc.err) {
|
||||
assert.Equals(t, 9, len(got)) // number of provisioner.SignOptions returned
|
||||
assert.Equals(t, 10, len(got)) // number of provisioner.SignOptions returned
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -1034,7 +1034,7 @@ func TestAuthority_authorizeSSHSign(t *testing.T) {
|
|||
}
|
||||
} else {
|
||||
if assert.Nil(t, tc.err) {
|
||||
assert.Len(t, 9, got) // number of provisioner.SignOptions returned
|
||||
assert.Len(t, 10, got) // number of provisioner.SignOptions returned
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"crypto"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"net/http"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
@ -85,6 +86,14 @@ func WithDatabase(d db.AuthDB) Option {
|
|||
}
|
||||
}
|
||||
|
||||
// WithWebhookClient sets the http.Client to be used for outbound requests.
|
||||
func WithWebhookClient(c *http.Client) Option {
|
||||
return func(a *Authority) error {
|
||||
a.webhookClient = c
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithGetIdentityFunc sets a custom function to retrieve the identity from
|
||||
// an external resource.
|
||||
func WithGetIdentityFunc(fn func(ctx context.Context, p provisioner.Interface, email string) (*provisioner.Identity, error)) Option {
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"go.step.sm/linkedca"
|
||||
)
|
||||
|
||||
// ACMEChallenge represents the supported acme challenges.
|
||||
|
@ -252,6 +253,7 @@ func (p *ACME) AuthorizeSign(ctx context.Context, token string) ([]SignOption, e
|
|||
defaultPublicKeyValidator{},
|
||||
newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()),
|
||||
newX509NamePolicyValidator(p.ctl.getPolicy().getX509()),
|
||||
p.ctl.newWebhookController(nil, linkedca.Webhook_X509),
|
||||
}
|
||||
|
||||
return opts, nil
|
||||
|
|
|
@ -269,7 +269,7 @@ func TestACME_AuthorizeSign(t *testing.T) {
|
|||
}
|
||||
} else {
|
||||
if assert.Nil(t, tc.err) && assert.NotNil(t, opts) {
|
||||
assert.Equals(t, 7, len(opts)) // number of SignOptions returned
|
||||
assert.Equals(t, 8, len(opts)) // number of SignOptions returned
|
||||
for _, o := range opts {
|
||||
switch v := o.(type) {
|
||||
case *ACME:
|
||||
|
@ -288,6 +288,8 @@ func TestACME_AuthorizeSign(t *testing.T) {
|
|||
assert.Equals(t, v.max, tc.p.ctl.Claimer.MaxTLSCertDuration())
|
||||
case *x509NamePolicyValidator:
|
||||
assert.Equals(t, nil, v.policyEngine)
|
||||
case *WebhookController:
|
||||
assert.Len(t, 0, v.webhooks)
|
||||
default:
|
||||
assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v))
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ import (
|
|||
"go.step.sm/crypto/jose"
|
||||
"go.step.sm/crypto/sshutil"
|
||||
"go.step.sm/crypto/x509util"
|
||||
"go.step.sm/linkedca"
|
||||
|
||||
"github.com/smallstep/certificates/errs"
|
||||
)
|
||||
|
@ -484,6 +485,7 @@ func (p *AWS) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er
|
|||
commonNameValidator(payload.Claims.Subject),
|
||||
newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()),
|
||||
newX509NamePolicyValidator(p.ctl.getPolicy().getX509()),
|
||||
p.ctl.newWebhookController(data, linkedca.Webhook_X509),
|
||||
), nil
|
||||
}
|
||||
|
||||
|
@ -765,5 +767,7 @@ func (p *AWS) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption,
|
|||
&sshCertDefaultValidator{},
|
||||
// Ensure that all principal names are allowed
|
||||
newSSHNamePolicyValidator(p.ctl.getPolicy().getSSHHost(), nil),
|
||||
// Call webhooks
|
||||
p.ctl.newWebhookController(data, linkedca.Webhook_SSH),
|
||||
), nil
|
||||
}
|
||||
|
|
|
@ -642,11 +642,11 @@ func TestAWS_AuthorizeSign(t *testing.T) {
|
|||
code int
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok", p1, args{t1, "foo.local"}, 8, http.StatusOK, false},
|
||||
{"ok", p2, args{t2, "instance-id"}, 12, http.StatusOK, false},
|
||||
{"ok", p2, args{t2Hostname, "ip-127-0-0-1.us-west-1.compute.internal"}, 12, http.StatusOK, false},
|
||||
{"ok", p2, args{t2PrivateIP, "127.0.0.1"}, 12, http.StatusOK, false},
|
||||
{"ok", p1, args{t4, "instance-id"}, 8, http.StatusOK, false},
|
||||
{"ok", p1, args{t1, "foo.local"}, 9, http.StatusOK, false},
|
||||
{"ok", p2, args{t2, "instance-id"}, 13, http.StatusOK, false},
|
||||
{"ok", p2, args{t2Hostname, "ip-127-0-0-1.us-west-1.compute.internal"}, 13, http.StatusOK, false},
|
||||
{"ok", p2, args{t2PrivateIP, "127.0.0.1"}, 13, http.StatusOK, false},
|
||||
{"ok", p1, args{t4, "instance-id"}, 9, http.StatusOK, false},
|
||||
{"fail account", p3, args{token: t3}, 0, http.StatusUnauthorized, true},
|
||||
{"fail token", p1, args{token: "token"}, 0, http.StatusUnauthorized, true},
|
||||
{"fail subject", p1, args{token: failSubject}, 0, http.StatusUnauthorized, true},
|
||||
|
@ -701,6 +701,8 @@ func TestAWS_AuthorizeSign(t *testing.T) {
|
|||
assert.Equals(t, []string(v), []string{"ip-127-0-0-1.us-west-1.compute.internal"})
|
||||
case *x509NamePolicyValidator:
|
||||
assert.Equals(t, nil, v.policyEngine)
|
||||
case *WebhookController:
|
||||
assert.Len(t, 0, v.webhooks)
|
||||
default:
|
||||
assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v))
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ import (
|
|||
"go.step.sm/crypto/jose"
|
||||
"go.step.sm/crypto/sshutil"
|
||||
"go.step.sm/crypto/x509util"
|
||||
"go.step.sm/linkedca"
|
||||
|
||||
"github.com/smallstep/certificates/errs"
|
||||
)
|
||||
|
@ -363,6 +364,7 @@ func (p *Azure) AuthorizeSign(ctx context.Context, token string) ([]SignOption,
|
|||
defaultPublicKeyValidator{},
|
||||
newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()),
|
||||
newX509NamePolicyValidator(p.ctl.getPolicy().getX509()),
|
||||
p.ctl.newWebhookController(data, linkedca.Webhook_X509),
|
||||
), nil
|
||||
}
|
||||
|
||||
|
@ -431,6 +433,8 @@ func (p *Azure) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOptio
|
|||
&sshCertDefaultValidator{},
|
||||
// Ensure that all principal names are allowed
|
||||
newSSHNamePolicyValidator(p.ctl.getPolicy().getSSHHost(), nil),
|
||||
// Call webhooks
|
||||
p.ctl.newWebhookController(data, linkedca.Webhook_SSH),
|
||||
), nil
|
||||
}
|
||||
|
||||
|
|
|
@ -474,11 +474,11 @@ func TestAzure_AuthorizeSign(t *testing.T) {
|
|||
code int
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok", p1, args{t1}, 7, http.StatusOK, false},
|
||||
{"ok", p2, args{t2}, 12, http.StatusOK, false},
|
||||
{"ok", p1, args{t11}, 7, http.StatusOK, false},
|
||||
{"ok", p5, args{t5}, 7, http.StatusOK, false},
|
||||
{"ok", p7, args{t7}, 7, http.StatusOK, false},
|
||||
{"ok", p1, args{t1}, 8, http.StatusOK, false},
|
||||
{"ok", p2, args{t2}, 13, http.StatusOK, false},
|
||||
{"ok", p1, args{t11}, 8, http.StatusOK, false},
|
||||
{"ok", p5, args{t5}, 8, http.StatusOK, false},
|
||||
{"ok", p7, args{t7}, 8, http.StatusOK, false},
|
||||
{"fail tenant", p3, args{t3}, 0, http.StatusUnauthorized, true},
|
||||
{"fail resource group", p4, args{t4}, 0, http.StatusUnauthorized, true},
|
||||
{"fail subscription", p6, args{t6}, 0, http.StatusUnauthorized, true},
|
||||
|
@ -530,6 +530,8 @@ func TestAzure_AuthorizeSign(t *testing.T) {
|
|||
assert.Equals(t, []string(v), []string{"virtualMachine"})
|
||||
case *x509NamePolicyValidator:
|
||||
assert.Equals(t, nil, v.policyEngine)
|
||||
case *WebhookController:
|
||||
assert.Len(t, 0, v.webhooks)
|
||||
default:
|
||||
assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v))
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/errs"
|
||||
"go.step.sm/linkedca"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
|
@ -23,6 +24,8 @@ type Controller struct {
|
|||
AuthorizeRenewFunc AuthorizeRenewFunc
|
||||
AuthorizeSSHRenewFunc AuthorizeSSHRenewFunc
|
||||
policy *policyEngine
|
||||
webhookClient *http.Client
|
||||
webhooks []*Webhook
|
||||
}
|
||||
|
||||
// NewController initializes a new provisioner controller.
|
||||
|
@ -43,6 +46,8 @@ func NewController(p Interface, claims *Claims, config Config, options *Options)
|
|||
AuthorizeRenewFunc: config.AuthorizeRenewFunc,
|
||||
AuthorizeSSHRenewFunc: config.AuthorizeSSHRenewFunc,
|
||||
policy: policy,
|
||||
webhookClient: config.WebhookClient,
|
||||
webhooks: options.GetWebhooks(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -72,6 +77,18 @@ func (c *Controller) AuthorizeSSHRenew(ctx context.Context, cert *ssh.Certificat
|
|||
return DefaultAuthorizeSSHRenew(ctx, c, cert)
|
||||
}
|
||||
|
||||
func (c *Controller) newWebhookController(templateData WebhookSetter, certType linkedca.Webhook_CertType) *WebhookController {
|
||||
client := c.webhookClient
|
||||
if client == nil {
|
||||
client = http.DefaultClient
|
||||
}
|
||||
return &WebhookController{
|
||||
TemplateData: templateData,
|
||||
client: client,
|
||||
webhooks: c.webhooks,
|
||||
}
|
||||
}
|
||||
|
||||
// Identity is the type representing an externally supplied identity that is used
|
||||
// by provisioners to populate certificate fields.
|
||||
type Identity struct {
|
||||
|
|
|
@ -18,6 +18,7 @@ import (
|
|||
"go.step.sm/crypto/jose"
|
||||
"go.step.sm/crypto/sshutil"
|
||||
"go.step.sm/crypto/x509util"
|
||||
"go.step.sm/linkedca"
|
||||
|
||||
"github.com/smallstep/certificates/errs"
|
||||
)
|
||||
|
@ -272,6 +273,7 @@ func (p *GCP) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er
|
|||
defaultPublicKeyValidator{},
|
||||
newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()),
|
||||
newX509NamePolicyValidator(p.ctl.getPolicy().getX509()),
|
||||
p.ctl.newWebhookController(data, linkedca.Webhook_X509),
|
||||
), nil
|
||||
}
|
||||
|
||||
|
@ -437,5 +439,7 @@ func (p *GCP) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption,
|
|||
&sshCertDefaultValidator{},
|
||||
// Ensure that all principal names are allowed
|
||||
newSSHNamePolicyValidator(p.ctl.getPolicy().getSSHHost(), nil),
|
||||
// Call webhooks
|
||||
p.ctl.newWebhookController(data, linkedca.Webhook_SSH),
|
||||
), nil
|
||||
}
|
||||
|
|
|
@ -516,9 +516,9 @@ func TestGCP_AuthorizeSign(t *testing.T) {
|
|||
code int
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok", p1, args{t1}, 7, http.StatusOK, false},
|
||||
{"ok", p2, args{t2}, 12, http.StatusOK, false},
|
||||
{"ok", p3, args{t3}, 7, http.StatusOK, false},
|
||||
{"ok", p1, args{t1}, 8, http.StatusOK, false},
|
||||
{"ok", p2, args{t2}, 13, http.StatusOK, false},
|
||||
{"ok", p3, args{t3}, 8, http.StatusOK, false},
|
||||
{"fail token", p1, args{"token"}, 0, http.StatusUnauthorized, true},
|
||||
{"fail key", p1, args{failKey}, 0, http.StatusUnauthorized, true},
|
||||
{"fail iss", p1, args{failIss}, 0, http.StatusUnauthorized, true},
|
||||
|
@ -573,6 +573,8 @@ func TestGCP_AuthorizeSign(t *testing.T) {
|
|||
assert.Equals(t, []string(v), []string{"instance-name.c.project-id.internal", "instance-name.zone.c.project-id.internal"})
|
||||
case *x509NamePolicyValidator:
|
||||
assert.Equals(t, nil, v.policyEngine)
|
||||
case *WebhookController:
|
||||
assert.Len(t, 0, v.webhooks)
|
||||
default:
|
||||
assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v))
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
"go.step.sm/crypto/jose"
|
||||
"go.step.sm/crypto/sshutil"
|
||||
"go.step.sm/crypto/x509util"
|
||||
"go.step.sm/linkedca"
|
||||
|
||||
"github.com/smallstep/certificates/errs"
|
||||
)
|
||||
|
@ -194,6 +195,7 @@ func (p *JWK) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er
|
|||
defaultSANsValidator(claims.SANs),
|
||||
newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()),
|
||||
newX509NamePolicyValidator(p.ctl.getPolicy().getX509()),
|
||||
p.ctl.newWebhookController(data, linkedca.Webhook_X509),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -278,6 +280,8 @@ func (p *JWK) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption,
|
|||
&sshCertDefaultValidator{},
|
||||
// Ensure that all principal names are allowed
|
||||
newSSHNamePolicyValidator(p.ctl.getPolicy().getSSHHost(), p.ctl.getPolicy().getSSHUser()),
|
||||
// Call webhooks
|
||||
p.ctl.newWebhookController(data, linkedca.Webhook_SSH),
|
||||
), nil
|
||||
}
|
||||
|
||||
|
|
|
@ -297,7 +297,7 @@ func TestJWK_AuthorizeSign(t *testing.T) {
|
|||
}
|
||||
} else {
|
||||
if assert.NotNil(t, got) {
|
||||
assert.Equals(t, 9, len(got))
|
||||
assert.Equals(t, 10, len(got))
|
||||
for _, o := range got {
|
||||
switch v := o.(type) {
|
||||
case *JWK:
|
||||
|
@ -319,6 +319,7 @@ func TestJWK_AuthorizeSign(t *testing.T) {
|
|||
assert.Equals(t, []string(v), tt.sans)
|
||||
case *x509NamePolicyValidator:
|
||||
assert.Equals(t, nil, v.policyEngine)
|
||||
case *WebhookController:
|
||||
default:
|
||||
assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v))
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import (
|
|||
"go.step.sm/crypto/pemutil"
|
||||
"go.step.sm/crypto/sshutil"
|
||||
"go.step.sm/crypto/x509util"
|
||||
"go.step.sm/linkedca"
|
||||
|
||||
"github.com/smallstep/certificates/errs"
|
||||
)
|
||||
|
@ -242,6 +243,7 @@ func (p *K8sSA) AuthorizeSign(ctx context.Context, token string) ([]SignOption,
|
|||
defaultPublicKeyValidator{},
|
||||
newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()),
|
||||
newX509NamePolicyValidator(p.ctl.getPolicy().getX509()),
|
||||
p.ctl.newWebhookController(data, linkedca.Webhook_X509),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -287,6 +289,8 @@ func (p *K8sSA) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOptio
|
|||
&sshCertDefaultValidator{},
|
||||
// Ensure that all principal names are allowed
|
||||
newSSHNamePolicyValidator(p.ctl.getPolicy().getSSHHost(), p.ctl.getPolicy().getSSHUser()),
|
||||
// Call webhooks
|
||||
p.ctl.newWebhookController(data, linkedca.Webhook_SSH),
|
||||
), nil
|
||||
}
|
||||
|
||||
|
|
|
@ -297,11 +297,13 @@ func TestK8sSA_AuthorizeSign(t *testing.T) {
|
|||
assert.Equals(t, v.max, tc.p.ctl.Claimer.MaxTLSCertDuration())
|
||||
case *x509NamePolicyValidator:
|
||||
assert.Equals(t, nil, v.policyEngine)
|
||||
case *WebhookController:
|
||||
assert.Len(t, 0, v.webhooks)
|
||||
default:
|
||||
assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v))
|
||||
}
|
||||
}
|
||||
assert.Equals(t, 7, len(opts))
|
||||
assert.Equals(t, 8, len(opts))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -368,7 +370,7 @@ func TestK8sSA_AuthorizeSSHSign(t *testing.T) {
|
|||
} else {
|
||||
if assert.Nil(t, tc.err) {
|
||||
if assert.NotNil(t, opts) {
|
||||
assert.Len(t, 8, opts)
|
||||
assert.Len(t, 9, opts)
|
||||
for _, o := range opts {
|
||||
switch v := o.(type) {
|
||||
case Interface:
|
||||
|
@ -384,6 +386,8 @@ func TestK8sSA_AuthorizeSSHSign(t *testing.T) {
|
|||
case *sshNamePolicyValidator:
|
||||
assert.Equals(t, nil, v.userPolicyEngine)
|
||||
assert.Equals(t, nil, v.hostPolicyEngine)
|
||||
case *WebhookController:
|
||||
assert.Len(t, 0, v.webhooks)
|
||||
default:
|
||||
assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v))
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import (
|
|||
"go.step.sm/crypto/sshutil"
|
||||
"go.step.sm/crypto/x25519"
|
||||
"go.step.sm/crypto/x509util"
|
||||
"go.step.sm/linkedca"
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
"github.com/smallstep/certificates/errs"
|
||||
|
@ -164,6 +165,7 @@ func (p *Nebula) AuthorizeSign(ctx context.Context, token string) ([]SignOption,
|
|||
defaultPublicKeyValidator{},
|
||||
newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()),
|
||||
newX509NamePolicyValidator(p.ctl.getPolicy().getX509()),
|
||||
p.ctl.newWebhookController(data, linkedca.Webhook_X509),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -262,6 +264,8 @@ func (p *Nebula) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOpti
|
|||
&sshCertDefaultValidator{},
|
||||
// Ensure that all principal names are allowed
|
||||
newSSHNamePolicyValidator(p.ctl.getPolicy().getSSHHost(), nil),
|
||||
// Call webhooks
|
||||
p.ctl.newWebhookController(data, linkedca.Webhook_SSH),
|
||||
), nil
|
||||
}
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ import (
|
|||
"go.step.sm/crypto/jose"
|
||||
"go.step.sm/crypto/sshutil"
|
||||
"go.step.sm/crypto/x509util"
|
||||
"go.step.sm/linkedca"
|
||||
|
||||
"github.com/smallstep/certificates/errs"
|
||||
)
|
||||
|
@ -356,6 +357,8 @@ func (o *OIDC) AuthorizeSign(ctx context.Context, token string) ([]SignOption, e
|
|||
defaultPublicKeyValidator{},
|
||||
newValidityValidator(o.ctl.Claimer.MinTLSCertDuration(), o.ctl.Claimer.MaxTLSCertDuration()),
|
||||
newX509NamePolicyValidator(o.ctl.getPolicy().getX509()),
|
||||
// webhooks
|
||||
o.ctl.newWebhookController(data, linkedca.Webhook_X509),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -460,6 +463,8 @@ func (o *OIDC) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption
|
|||
&sshCertDefaultValidator{},
|
||||
// Ensure that all principal names are allowed
|
||||
newSSHNamePolicyValidator(o.ctl.getPolicy().getSSHHost(), o.ctl.getPolicy().getSSHUser()),
|
||||
// Call webhooks
|
||||
o.ctl.newWebhookController(data, linkedca.Webhook_SSH),
|
||||
), nil
|
||||
}
|
||||
|
||||
|
|
|
@ -323,7 +323,7 @@ func TestOIDC_AuthorizeSign(t *testing.T) {
|
|||
assert.Equals(t, sc.StatusCode(), tt.code)
|
||||
assert.Nil(t, got)
|
||||
} else if assert.NotNil(t, got) {
|
||||
assert.Equals(t, 7, len(got))
|
||||
assert.Equals(t, 8, len(got))
|
||||
for _, o := range got {
|
||||
switch v := o.(type) {
|
||||
case *OIDC:
|
||||
|
@ -343,6 +343,8 @@ func TestOIDC_AuthorizeSign(t *testing.T) {
|
|||
assert.Equals(t, string(v), "name@smallstep.com")
|
||||
case *x509NamePolicyValidator:
|
||||
assert.Equals(t, nil, v.policyEngine)
|
||||
case *WebhookController:
|
||||
assert.Len(t, 0, v.webhooks)
|
||||
default:
|
||||
assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v))
|
||||
}
|
||||
|
|
|
@ -29,6 +29,9 @@ func (fn certificateOptionsFunc) Options(so SignOptions) []x509util.Option {
|
|||
type Options struct {
|
||||
X509 *X509Options `json:"x509,omitempty"`
|
||||
SSH *SSHOptions `json:"ssh,omitempty"`
|
||||
|
||||
// Webhooks is a list of webhooks that can augment template data
|
||||
Webhooks []*Webhook `json:"webhooks,omitempty"`
|
||||
}
|
||||
|
||||
// GetX509Options returns the X.509 options.
|
||||
|
@ -47,6 +50,14 @@ func (o *Options) GetSSHOptions() *SSHOptions {
|
|||
return o.SSH
|
||||
}
|
||||
|
||||
// GetWebhooks returns the webhooks options.
|
||||
func (o *Options) GetWebhooks() []*Webhook {
|
||||
if o == nil {
|
||||
return nil
|
||||
}
|
||||
return o.Webhooks
|
||||
}
|
||||
|
||||
// X509Options contains specific options for X.509 certificates.
|
||||
type X509Options struct {
|
||||
// Template contains a X.509 certificate template. It can be a JSON template
|
||||
|
|
|
@ -68,6 +68,36 @@ func TestOptions_GetSSHOptions(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestOptions_GetWebhooks(t *testing.T) {
|
||||
type fields struct {
|
||||
o *Options
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want []*Webhook
|
||||
}{
|
||||
{"ok", fields{&Options{Webhooks: []*Webhook{
|
||||
{Name: "foo"},
|
||||
{Name: "bar"},
|
||||
}}},
|
||||
[]*Webhook{
|
||||
{Name: "foo"},
|
||||
{Name: "bar"},
|
||||
},
|
||||
},
|
||||
{"nil", fields{&Options{}}, nil},
|
||||
{"nilOptions", fields{nil}, nil},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := tt.fields.o.GetWebhooks(); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Options.GetWebhooks() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvisionerX509Options_HasTemplate(t *testing.T) {
|
||||
type fields struct {
|
||||
Template string
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"crypto/x509"
|
||||
"encoding/json"
|
||||
stderrors "errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
|
@ -222,6 +223,8 @@ type Config struct {
|
|||
// AuthorizeSSHRenewFunc is a function that returns nil if a given SSH
|
||||
// certificate can be renewed.
|
||||
AuthorizeSSHRenewFunc AuthorizeSSHRenewFunc
|
||||
// WebhookClient is an http client to use in webhook request
|
||||
WebhookClient *http.Client
|
||||
}
|
||||
|
||||
type provisioner struct {
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"go.step.sm/linkedca"
|
||||
)
|
||||
|
||||
// SCEP is the SCEP provisioner type, an entity that can authorize the
|
||||
|
@ -128,6 +129,7 @@ func (s *SCEP) AuthorizeSign(ctx context.Context, token string) ([]SignOption, e
|
|||
newPublicKeyMinimumLengthValidator(s.MinimumPublicKeyLength),
|
||||
newValidityValidator(s.ctl.Claimer.MinTLSCertDuration(), s.ctl.Claimer.MaxTLSCertDuration()),
|
||||
newX509NamePolicyValidator(s.ctl.getPolicy().getX509()),
|
||||
s.ctl.newWebhookController(nil, linkedca.Webhook_X509),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -69,6 +69,8 @@ func signSSHCertificate(key crypto.PublicKey, opts SignSSHOptions, signOpts []Si
|
|||
if err := o.Valid(opts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// call webhooks
|
||||
case *WebhookController:
|
||||
default:
|
||||
return nil, fmt.Errorf("signSSH: invalid extra option type %T", o)
|
||||
}
|
||||
|
|
14
authority/provisioner/testdata/certs/foo.crt
vendored
Normal file
14
authority/provisioner/testdata/certs/foo.crt
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIICIDCCAcagAwIBAgIQTL7pKDl8mFzRziotXbgjEjAKBggqhkjOPQQDAjAnMSUw
|
||||
IwYDVQQDExxFeGFtcGxlIEluYy4gSW50ZXJtZWRpYXRlIENBMB4XDTE5MDMyMjIy
|
||||
MjkyOVoXDTE5MDMyMzIyMjkyOVowHDEaMBgGA1UEAxMRZm9vLnNtYWxsc3RlcC5j
|
||||
b20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQbptfDonFaeUPiTr52wl9r3dcz
|
||||
greolwDRmsgyFgnr1EuKH56WRcgH1gjfL0pybFlO3PdgBukR4u+sveq343OAo4He
|
||||
MIHbMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUH
|
||||
AwIwHQYDVR0OBBYEFP9pHiVlsx5mr4L2QirOb1G9Mo4jMB8GA1UdIwQYMBaAFKEe
|
||||
9IdMyaHdURMjoJce7FN9HC9wMBwGA1UdEQQVMBOCEWZvby5zbWFsbHN0ZXAuY29t
|
||||
MEwGDCsGAQQBgqRkxihAAQQ8MDoCAQEECHN0ZXAtY2xpBCs0VUVMSng4ZTBhUzlt
|
||||
MENIM2ZaMEVCN0Q1YVVQSUNiNzU5ekFMSEZlanZjMAoGCCqGSM49BAMCA0gAMEUC
|
||||
IDxtNo1BX/4Sbf/+k1n+v//kh8ETr3clPvhjcyfvBIGTAiEAiT0kvbkPdCCnmHIw
|
||||
lhpgBwT5YReZzBwIYXyKyJXc07M=
|
||||
-----END CERTIFICATE-----
|
5
authority/provisioner/testdata/secrets/foo.key
vendored
Normal file
5
authority/provisioner/testdata/secrets/foo.key
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEIJmnxm3N/ahRA2PWeZhRGJUKPU1lI44WcE4P1bynIim6oAoGCCqGSM49
|
||||
AwEHoUQDQgAEG6bXw6JxWnlD4k6+dsJfa93XM4K3qJcA0ZrIMhYJ69RLih+elkXI
|
||||
B9YI3y9KcmxZTtz3YAbpEeLvrL3qt+NzgA==
|
||||
-----END EC PRIVATE KEY-----
|
209
authority/provisioner/webhook.go
Normal file
209
authority/provisioner/webhook.go
Normal file
|
@ -0,0 +1,209 @@
|
|||
package provisioner
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/templates"
|
||||
"github.com/smallstep/certificates/webhook"
|
||||
"go.step.sm/linkedca"
|
||||
)
|
||||
|
||||
var ErrWebhookDenied = errors.New("webhook server did not allow request")
|
||||
|
||||
type WebhookSetter interface {
|
||||
SetWebhook(string, any)
|
||||
}
|
||||
|
||||
type WebhookController struct {
|
||||
client *http.Client
|
||||
webhooks []*Webhook
|
||||
certType linkedca.Webhook_CertType
|
||||
TemplateData WebhookSetter
|
||||
}
|
||||
|
||||
// Enrich fetches data from remote servers and adds returned data to the
|
||||
// templateData
|
||||
func (wc *WebhookController) Enrich(req *webhook.RequestBody) error {
|
||||
if wc == nil {
|
||||
return nil
|
||||
}
|
||||
for _, wh := range wc.webhooks {
|
||||
if wh.Kind != linkedca.Webhook_ENRICHING.String() {
|
||||
continue
|
||||
}
|
||||
if !wc.isCertTypeOK(wh) {
|
||||
continue
|
||||
}
|
||||
resp, err := wh.Do(wc.client, req, wc.TemplateData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !resp.Allow {
|
||||
return ErrWebhookDenied
|
||||
}
|
||||
wc.TemplateData.SetWebhook(wh.Name, resp.Data)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Authorize checks that all remote servers allow the request
|
||||
func (wc *WebhookController) Authorize(req *webhook.RequestBody) error {
|
||||
if wc == nil {
|
||||
return nil
|
||||
}
|
||||
for _, wh := range wc.webhooks {
|
||||
if wh.Kind != linkedca.Webhook_AUTHORIZING.String() {
|
||||
continue
|
||||
}
|
||||
if !wc.isCertTypeOK(wh) {
|
||||
continue
|
||||
}
|
||||
resp, err := wh.Do(wc.client, req, wc.TemplateData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !resp.Allow {
|
||||
return ErrWebhookDenied
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (wc *WebhookController) isCertTypeOK(wh *Webhook) bool {
|
||||
if wc.certType == linkedca.Webhook_ALL {
|
||||
return true
|
||||
}
|
||||
if wh.CertType == linkedca.Webhook_ALL.String() || wh.CertType == "" {
|
||||
return true
|
||||
}
|
||||
return wc.certType.String() == wh.CertType
|
||||
}
|
||||
|
||||
type Webhook struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
Kind string `json:"kind"`
|
||||
DisableTLSClientAuth bool `json:"disableTLSClientAuth,omitempty"`
|
||||
CertType string `json:"certType"`
|
||||
Secret string `json:"-"`
|
||||
BearerToken string `json:"-"`
|
||||
BasicAuth struct {
|
||||
Username string
|
||||
Password string
|
||||
} `json:"-"`
|
||||
}
|
||||
|
||||
func (w *Webhook) Do(client *http.Client, reqBody *webhook.RequestBody, data any) (*webhook.ResponseBody, error) {
|
||||
tmpl, err := template.New("url").Funcs(templates.StepFuncMap()).Parse(w.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buf := &bytes.Buffer{}
|
||||
if err := tmpl.Execute(buf, data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
url := buf.String()
|
||||
|
||||
/*
|
||||
Sending the token to the webhook server is a security risk. A K8sSA
|
||||
token can be reused multiple times. The webhook can misuse it to get
|
||||
fake certificates. A webhook can misuse any other token to get its own
|
||||
certificate before responding.
|
||||
switch tmpl := data.(type) {
|
||||
case x509util.TemplateData:
|
||||
reqBody.Token = tmpl[x509util.TokenKey]
|
||||
case sshutil.TemplateData:
|
||||
reqBody.Token = tmpl[sshutil.TokenKey]
|
||||
}
|
||||
*/
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||
defer cancel()
|
||||
|
||||
reqBody.Timestamp = time.Now()
|
||||
|
||||
reqBytes, err := json.Marshal(reqBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
retries := 1
|
||||
retry:
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(reqBytes))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
secret, err := base64.StdEncoding.DecodeString(w.Secret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sig := hmac.New(sha256.New, secret).Sum(reqBytes)
|
||||
req.Header.Set("X-Smallstep-Signature", hex.EncodeToString(sig))
|
||||
req.Header.Set("X-Smallstep-Webhook-ID", w.ID)
|
||||
|
||||
if w.BearerToken != "" {
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", w.BearerToken))
|
||||
} else if w.BasicAuth.Username != "" || w.BasicAuth.Password != "" {
|
||||
req.SetBasicAuth(w.BasicAuth.Username, w.BasicAuth.Password)
|
||||
}
|
||||
|
||||
if w.DisableTLSClientAuth {
|
||||
transport, ok := client.Transport.(*http.Transport)
|
||||
if !ok {
|
||||
return nil, errors.New("client transport is not a *http.Transport")
|
||||
}
|
||||
transport = transport.Clone()
|
||||
tlsConfig := transport.TLSClientConfig.Clone()
|
||||
tlsConfig.GetClientCertificate = nil
|
||||
tlsConfig.Certificates = nil
|
||||
transport.TLSClientConfig = tlsConfig
|
||||
client = &http.Client{
|
||||
Transport: transport,
|
||||
}
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
return nil, err
|
||||
} else if retries > 0 {
|
||||
retries--
|
||||
time.Sleep(time.Second)
|
||||
goto retry
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
if err := resp.Body.Close(); err != nil {
|
||||
log.Printf("Failed to close body of response from %s", w.URL)
|
||||
}
|
||||
}()
|
||||
if resp.StatusCode >= 500 && retries > 0 {
|
||||
retries--
|
||||
time.Sleep(time.Second)
|
||||
goto retry
|
||||
}
|
||||
if resp.StatusCode >= 400 {
|
||||
return nil, fmt.Errorf("Webhook server responded with %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
respBody := &webhook.ResponseBody{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(respBody); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return respBody, nil
|
||||
}
|
473
authority/provisioner/webhook_test.go
Normal file
473
authority/provisioner/webhook_test.go
Normal file
|
@ -0,0 +1,473 @@
|
|||
package provisioner
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/assert"
|
||||
"github.com/smallstep/certificates/webhook"
|
||||
"go.step.sm/crypto/x509util"
|
||||
"go.step.sm/linkedca"
|
||||
)
|
||||
|
||||
func TestWebhookController_isCertTypeOK(t *testing.T) {
|
||||
type test struct {
|
||||
wc *WebhookController
|
||||
wh *Webhook
|
||||
want bool
|
||||
}
|
||||
tests := map[string]test{
|
||||
"all/all": {
|
||||
wc: &WebhookController{certType: linkedca.Webhook_ALL},
|
||||
wh: &Webhook{CertType: linkedca.Webhook_ALL.String()},
|
||||
want: true,
|
||||
},
|
||||
"all/x509": {
|
||||
wc: &WebhookController{certType: linkedca.Webhook_ALL},
|
||||
wh: &Webhook{CertType: linkedca.Webhook_X509.String()},
|
||||
want: true,
|
||||
},
|
||||
"all/ssh": {
|
||||
wc: &WebhookController{certType: linkedca.Webhook_ALL},
|
||||
wh: &Webhook{CertType: linkedca.Webhook_SSH.String()},
|
||||
want: true,
|
||||
},
|
||||
`all/""`: {
|
||||
wc: &WebhookController{certType: linkedca.Webhook_ALL},
|
||||
wh: &Webhook{},
|
||||
want: true,
|
||||
},
|
||||
"x509/all": {
|
||||
wc: &WebhookController{certType: linkedca.Webhook_X509},
|
||||
wh: &Webhook{CertType: linkedca.Webhook_ALL.String()},
|
||||
want: true,
|
||||
},
|
||||
"x509/x509": {
|
||||
wc: &WebhookController{certType: linkedca.Webhook_X509},
|
||||
wh: &Webhook{CertType: linkedca.Webhook_X509.String()},
|
||||
want: true,
|
||||
},
|
||||
"x509/ssh": {
|
||||
wc: &WebhookController{certType: linkedca.Webhook_X509},
|
||||
wh: &Webhook{CertType: linkedca.Webhook_SSH.String()},
|
||||
want: false,
|
||||
},
|
||||
`x509/""`: {
|
||||
wc: &WebhookController{certType: linkedca.Webhook_X509},
|
||||
wh: &Webhook{},
|
||||
want: true,
|
||||
},
|
||||
"ssh/all": {
|
||||
wc: &WebhookController{certType: linkedca.Webhook_SSH},
|
||||
wh: &Webhook{CertType: linkedca.Webhook_ALL.String()},
|
||||
want: true,
|
||||
},
|
||||
"ssh/x509": {
|
||||
wc: &WebhookController{certType: linkedca.Webhook_SSH},
|
||||
wh: &Webhook{CertType: linkedca.Webhook_X509.String()},
|
||||
want: false,
|
||||
},
|
||||
"ssh/ssh": {
|
||||
wc: &WebhookController{certType: linkedca.Webhook_SSH},
|
||||
wh: &Webhook{CertType: linkedca.Webhook_SSH.String()},
|
||||
want: true,
|
||||
},
|
||||
`ssh/""`: {
|
||||
wc: &WebhookController{certType: linkedca.Webhook_SSH},
|
||||
wh: &Webhook{},
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert.Equals(t, test.want, test.wc.isCertTypeOK(test.wh))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebhookController_Enrich(t *testing.T) {
|
||||
type test struct {
|
||||
ctl *WebhookController
|
||||
req *webhook.RequestBody
|
||||
responses []*webhook.ResponseBody
|
||||
expectErr bool
|
||||
expectTemplateData any
|
||||
}
|
||||
tests := map[string]test{
|
||||
"ok/no enriching webhooks": {
|
||||
ctl: &WebhookController{
|
||||
client: http.DefaultClient,
|
||||
webhooks: []*Webhook{{Name: "people", Kind: "AUTHORIZING"}},
|
||||
TemplateData: nil,
|
||||
},
|
||||
req: &webhook.RequestBody{},
|
||||
responses: nil,
|
||||
expectErr: false,
|
||||
expectTemplateData: nil,
|
||||
},
|
||||
"ok/one webhook": {
|
||||
ctl: &WebhookController{
|
||||
client: http.DefaultClient,
|
||||
webhooks: []*Webhook{{Name: "people", Kind: "ENRICHING"}},
|
||||
TemplateData: x509util.TemplateData{},
|
||||
},
|
||||
req: &webhook.RequestBody{},
|
||||
responses: []*webhook.ResponseBody{{Allow: true, Data: map[string]any{"role": "bar"}}},
|
||||
expectErr: false,
|
||||
expectTemplateData: x509util.TemplateData{"Webhooks": map[string]any{"people": map[string]any{"role": "bar"}}},
|
||||
},
|
||||
"ok/two webhooks": {
|
||||
ctl: &WebhookController{
|
||||
client: http.DefaultClient,
|
||||
webhooks: []*Webhook{
|
||||
{Name: "people", Kind: "ENRICHING"},
|
||||
{Name: "devices", Kind: "ENRICHING"},
|
||||
},
|
||||
TemplateData: x509util.TemplateData{},
|
||||
},
|
||||
req: &webhook.RequestBody{},
|
||||
responses: []*webhook.ResponseBody{
|
||||
{Allow: true, Data: map[string]any{"role": "bar"}},
|
||||
{Allow: true, Data: map[string]any{"serial": "123"}},
|
||||
},
|
||||
expectErr: false,
|
||||
expectTemplateData: x509util.TemplateData{
|
||||
"Webhooks": map[string]any{
|
||||
"devices": map[string]any{"serial": "123"},
|
||||
"people": map[string]any{"role": "bar"},
|
||||
},
|
||||
},
|
||||
},
|
||||
"ok/x509 only": {
|
||||
ctl: &WebhookController{
|
||||
client: http.DefaultClient,
|
||||
webhooks: []*Webhook{
|
||||
{Name: "people", Kind: "ENRICHING", CertType: linkedca.Webhook_SSH.String()},
|
||||
{Name: "devices", Kind: "ENRICHING"},
|
||||
},
|
||||
TemplateData: x509util.TemplateData{},
|
||||
certType: linkedca.Webhook_X509,
|
||||
},
|
||||
req: &webhook.RequestBody{},
|
||||
responses: []*webhook.ResponseBody{
|
||||
{Allow: true, Data: map[string]any{"role": "bar"}},
|
||||
{Allow: true, Data: map[string]any{"serial": "123"}},
|
||||
},
|
||||
expectErr: false,
|
||||
expectTemplateData: x509util.TemplateData{
|
||||
"Webhooks": map[string]any{
|
||||
"devices": map[string]any{"serial": "123"},
|
||||
},
|
||||
},
|
||||
},
|
||||
"deny": {
|
||||
ctl: &WebhookController{
|
||||
client: http.DefaultClient,
|
||||
webhooks: []*Webhook{{Name: "people", Kind: "ENRICHING"}},
|
||||
TemplateData: x509util.TemplateData{},
|
||||
},
|
||||
req: &webhook.RequestBody{},
|
||||
responses: []*webhook.ResponseBody{{Allow: false}},
|
||||
expectErr: true,
|
||||
expectTemplateData: x509util.TemplateData{},
|
||||
},
|
||||
}
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
for i, wh := range test.ctl.webhooks {
|
||||
var j = i
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
err := json.NewEncoder(w).Encode(test.responses[j])
|
||||
assert.FatalError(t, err)
|
||||
}))
|
||||
// nolint: gocritic // defer in loop isn't a memory leak
|
||||
defer ts.Close()
|
||||
wh.URL = ts.URL
|
||||
}
|
||||
|
||||
err := test.ctl.Enrich(test.req)
|
||||
if (err != nil) != test.expectErr {
|
||||
t.Fatalf("Got err %v, want %v", err, test.expectErr)
|
||||
}
|
||||
assert.Equals(t, test.expectTemplateData, test.ctl.TemplateData)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebhookController_Authorize(t *testing.T) {
|
||||
type test struct {
|
||||
ctl *WebhookController
|
||||
req *webhook.RequestBody
|
||||
responses []*webhook.ResponseBody
|
||||
expectErr bool
|
||||
}
|
||||
tests := map[string]test{
|
||||
"ok/no enriching webhooks": {
|
||||
ctl: &WebhookController{
|
||||
client: http.DefaultClient,
|
||||
webhooks: []*Webhook{{Name: "people", Kind: "ENRICHING"}},
|
||||
},
|
||||
req: &webhook.RequestBody{},
|
||||
responses: nil,
|
||||
expectErr: false,
|
||||
},
|
||||
"ok": {
|
||||
ctl: &WebhookController{
|
||||
client: http.DefaultClient,
|
||||
webhooks: []*Webhook{{Name: "people", Kind: "AUTHORIZING"}},
|
||||
},
|
||||
req: &webhook.RequestBody{},
|
||||
responses: []*webhook.ResponseBody{{Allow: true}},
|
||||
expectErr: false,
|
||||
},
|
||||
"ok/ssh only": {
|
||||
ctl: &WebhookController{
|
||||
client: http.DefaultClient,
|
||||
webhooks: []*Webhook{{Name: "people", Kind: "AUTHORIZING", CertType: linkedca.Webhook_X509.String()}},
|
||||
certType: linkedca.Webhook_SSH,
|
||||
},
|
||||
req: &webhook.RequestBody{},
|
||||
responses: []*webhook.ResponseBody{{Allow: false}},
|
||||
expectErr: false,
|
||||
},
|
||||
"deny": {
|
||||
ctl: &WebhookController{
|
||||
client: http.DefaultClient,
|
||||
webhooks: []*Webhook{{Name: "people", Kind: "AUTHORIZING"}},
|
||||
},
|
||||
req: &webhook.RequestBody{},
|
||||
responses: []*webhook.ResponseBody{{Allow: false}},
|
||||
expectErr: true,
|
||||
},
|
||||
}
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
for i, wh := range test.ctl.webhooks {
|
||||
var j = i
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
err := json.NewEncoder(w).Encode(test.responses[j])
|
||||
assert.FatalError(t, err)
|
||||
}))
|
||||
// nolint: gocritic // defer in loop isn't a memory leak
|
||||
defer ts.Close()
|
||||
wh.URL = ts.URL
|
||||
}
|
||||
|
||||
err := test.ctl.Authorize(test.req)
|
||||
if (err != nil) != test.expectErr {
|
||||
t.Fatalf("Got err %v, want %v", err, test.expectErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebhook_Do(t *testing.T) {
|
||||
csr := parseCertificateRequest(t, "testdata/certs/ecdsa.csr")
|
||||
type test struct {
|
||||
webhook Webhook
|
||||
dataArg any
|
||||
webhookResponse webhook.ResponseBody
|
||||
expectPath string
|
||||
errStatusCode int
|
||||
serverErrMsg string
|
||||
expectErr error
|
||||
// expectToken any
|
||||
}
|
||||
tests := map[string]test{
|
||||
"ok": {
|
||||
webhook: Webhook{
|
||||
ID: "abc123",
|
||||
Secret: "c2VjcmV0Cg==",
|
||||
},
|
||||
webhookResponse: webhook.ResponseBody{
|
||||
Data: map[string]interface{}{"role": "dba"},
|
||||
},
|
||||
},
|
||||
"ok/bearer": {
|
||||
webhook: Webhook{
|
||||
ID: "abc123",
|
||||
Secret: "c2VjcmV0Cg==",
|
||||
BearerToken: "mytoken",
|
||||
},
|
||||
webhookResponse: webhook.ResponseBody{
|
||||
Data: map[string]interface{}{"role": "dba"},
|
||||
},
|
||||
},
|
||||
"ok/basic": {
|
||||
webhook: Webhook{
|
||||
ID: "abc123",
|
||||
Secret: "c2VjcmV0Cg==",
|
||||
BasicAuth: struct {
|
||||
Username string
|
||||
Password string
|
||||
}{
|
||||
Username: "myuser",
|
||||
Password: "mypass",
|
||||
},
|
||||
},
|
||||
webhookResponse: webhook.ResponseBody{
|
||||
Data: map[string]interface{}{"role": "dba"},
|
||||
},
|
||||
},
|
||||
"ok/templated-url": {
|
||||
webhook: Webhook{
|
||||
ID: "abc123",
|
||||
// scheme, host, port will come from test server
|
||||
URL: "/users/{{ .username }}?region={{ .region }}",
|
||||
Secret: "c2VjcmV0Cg==",
|
||||
},
|
||||
dataArg: map[string]interface{}{"username": "areed", "region": "central"},
|
||||
webhookResponse: webhook.ResponseBody{
|
||||
Data: map[string]interface{}{"role": "dba"},
|
||||
},
|
||||
expectPath: "/users/areed?region=central",
|
||||
},
|
||||
/*
|
||||
"ok/token from ssh template": {
|
||||
webhook: Webhook{
|
||||
ID: "abc123",
|
||||
Secret: "c2VjcmV0Cg==",
|
||||
},
|
||||
webhookResponse: webhook.ResponseBody{
|
||||
Data: map[string]interface{}{"role": "dba"},
|
||||
},
|
||||
dataArg: sshutil.TemplateData{sshutil.TokenKey: "token"},
|
||||
expectToken: "token",
|
||||
},
|
||||
"ok/token from x509 template": {
|
||||
webhook: Webhook{
|
||||
ID: "abc123",
|
||||
Secret: "c2VjcmV0Cg==",
|
||||
},
|
||||
webhookResponse: webhook.ResponseBody{
|
||||
Data: map[string]interface{}{"role": "dba"},
|
||||
},
|
||||
dataArg: x509util.TemplateData{sshutil.TokenKey: "token"},
|
||||
expectToken: "token",
|
||||
},
|
||||
*/
|
||||
"ok/allow": {
|
||||
webhook: Webhook{
|
||||
ID: "abc123",
|
||||
Secret: "c2VjcmV0Cg==",
|
||||
},
|
||||
webhookResponse: webhook.ResponseBody{
|
||||
Allow: true,
|
||||
},
|
||||
},
|
||||
"fail/404": {
|
||||
webhook: Webhook{
|
||||
ID: "abc123",
|
||||
Secret: "c2VjcmV0Cg==",
|
||||
},
|
||||
webhookResponse: webhook.ResponseBody{
|
||||
Data: map[string]interface{}{"role": "dba"},
|
||||
},
|
||||
errStatusCode: 404,
|
||||
serverErrMsg: "item not found",
|
||||
expectErr: errors.New("Webhook server responded with 404"),
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.Header.Get("X-Smallstep-Webhook-ID")
|
||||
assert.Equals(t, tc.webhook.ID, id)
|
||||
|
||||
sig, err := hex.DecodeString(r.Header.Get("X-Smallstep-Signature"))
|
||||
assert.FatalError(t, err)
|
||||
|
||||
body, err := io.ReadAll(r.Body)
|
||||
assert.FatalError(t, err)
|
||||
|
||||
secret, err := base64.StdEncoding.DecodeString(tc.webhook.Secret)
|
||||
assert.FatalError(t, err)
|
||||
mac := hmac.New(sha256.New, secret).Sum(body)
|
||||
assert.True(t, hmac.Equal(sig, mac))
|
||||
|
||||
switch {
|
||||
case tc.webhook.BearerToken != "":
|
||||
ah := fmt.Sprintf("Bearer %s", tc.webhook.BearerToken)
|
||||
assert.Equals(t, ah, r.Header.Get("Authorization"))
|
||||
case tc.webhook.BasicAuth.Username != "" || tc.webhook.BasicAuth.Password != "":
|
||||
whReq, err := http.NewRequest("", "", http.NoBody)
|
||||
assert.FatalError(t, err)
|
||||
whReq.SetBasicAuth(tc.webhook.BasicAuth.Username, tc.webhook.BasicAuth.Password)
|
||||
ah := whReq.Header.Get("Authorization")
|
||||
assert.Equals(t, ah, whReq.Header.Get("Authorization"))
|
||||
default:
|
||||
assert.Equals(t, "", r.Header.Get("Authorization"))
|
||||
}
|
||||
|
||||
if tc.expectPath != "" {
|
||||
assert.Equals(t, tc.expectPath, r.URL.Path+"?"+r.URL.RawQuery)
|
||||
}
|
||||
|
||||
if tc.errStatusCode != 0 {
|
||||
http.Error(w, tc.serverErrMsg, tc.errStatusCode)
|
||||
return
|
||||
}
|
||||
|
||||
reqBody := new(webhook.RequestBody)
|
||||
err = json.Unmarshal(body, reqBody)
|
||||
assert.FatalError(t, err)
|
||||
// assert.Equals(t, tc.expectToken, reqBody.Token)
|
||||
|
||||
err = json.NewEncoder(w).Encode(tc.webhookResponse)
|
||||
assert.FatalError(t, err)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
tc.webhook.URL = ts.URL + tc.webhook.URL
|
||||
|
||||
reqBody, err := webhook.NewRequestBody(webhook.WithX509CertificateRequest(csr))
|
||||
assert.FatalError(t, err)
|
||||
got, err := tc.webhook.Do(http.DefaultClient, reqBody, tc.dataArg)
|
||||
if tc.expectErr != nil {
|
||||
assert.Equals(t, tc.expectErr.Error(), err.Error())
|
||||
return
|
||||
}
|
||||
assert.FatalError(t, err)
|
||||
|
||||
assert.Equals(t, got, &tc.webhookResponse)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("disableTLSClientAuth", func(t *testing.T) {
|
||||
ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte("{}"))
|
||||
}))
|
||||
ts.TLS.ClientAuth = tls.RequireAnyClientCert
|
||||
wh := Webhook{
|
||||
URL: ts.URL,
|
||||
}
|
||||
cert, err := tls.LoadX509KeyPair("testdata/certs/foo.crt", "testdata/secrets/foo.key")
|
||||
assert.FatalError(t, err)
|
||||
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
transport.TLSClientConfig = &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
Certificates: []tls.Certificate{cert},
|
||||
}
|
||||
client := &http.Client{
|
||||
Transport: transport,
|
||||
}
|
||||
reqBody, err := webhook.NewRequestBody(webhook.WithX509CertificateRequest(csr))
|
||||
assert.FatalError(t, err)
|
||||
_, err = wh.Do(client, reqBody, nil)
|
||||
assert.FatalError(t, err)
|
||||
|
||||
wh.DisableTLSClientAuth = true
|
||||
_, err = wh.Do(client, reqBody, nil)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
|
@ -12,6 +12,7 @@ import (
|
|||
"go.step.sm/crypto/jose"
|
||||
"go.step.sm/crypto/sshutil"
|
||||
"go.step.sm/crypto/x509util"
|
||||
"go.step.sm/linkedca"
|
||||
|
||||
"github.com/smallstep/certificates/errs"
|
||||
)
|
||||
|
@ -245,6 +246,7 @@ func (p *X5C) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er
|
|||
defaultPublicKeyValidator{},
|
||||
newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()),
|
||||
newX509NamePolicyValidator(p.ctl.getPolicy().getX509()),
|
||||
p.ctl.newWebhookController(data, linkedca.Webhook_X509),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -332,5 +334,7 @@ func (p *X5C) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption,
|
|||
&sshCertDefaultValidator{},
|
||||
// Ensure that all principal names are allowed
|
||||
newSSHNamePolicyValidator(p.ctl.getPolicy().getSSHHost(), p.ctl.getPolicy().getSSHUser()),
|
||||
// Call webhooks
|
||||
p.ctl.newWebhookController(data, linkedca.Webhook_SSH),
|
||||
), nil
|
||||
}
|
||||
|
|
|
@ -468,7 +468,7 @@ func TestX5C_AuthorizeSign(t *testing.T) {
|
|||
} else {
|
||||
if assert.Nil(t, tc.err) {
|
||||
if assert.NotNil(t, opts) {
|
||||
assert.Equals(t, 9, len(opts))
|
||||
assert.Equals(t, 10, len(opts))
|
||||
for _, o := range opts {
|
||||
switch v := o.(type) {
|
||||
case *X5C:
|
||||
|
@ -493,6 +493,8 @@ func TestX5C_AuthorizeSign(t *testing.T) {
|
|||
assert.Equals(t, v.max, tc.p.ctl.Claimer.MaxTLSCertDuration())
|
||||
case *x509NamePolicyValidator:
|
||||
assert.Equals(t, nil, v.policyEngine)
|
||||
case *WebhookController:
|
||||
assert.Len(t, 0, v.webhooks)
|
||||
default:
|
||||
assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v))
|
||||
}
|
||||
|
@ -794,15 +796,17 @@ func TestX5C_AuthorizeSSHSign(t *testing.T) {
|
|||
assert.Equals(t, nil, v.userPolicyEngine)
|
||||
assert.Equals(t, nil, v.hostPolicyEngine)
|
||||
case *sshDefaultPublicKeyValidator, *sshCertDefaultValidator, sshCertificateOptionsFunc:
|
||||
case *WebhookController:
|
||||
assert.Len(t, 0, v.webhooks)
|
||||
default:
|
||||
assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v))
|
||||
}
|
||||
tot++
|
||||
}
|
||||
if len(tc.claims.Step.SSH.CertType) > 0 {
|
||||
assert.Equals(t, tot, 11)
|
||||
assert.Equals(t, tot, 12)
|
||||
} else {
|
||||
assert.Equals(t, tot, 9)
|
||||
assert.Equals(t, tot, 10)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -144,6 +144,7 @@ func (a *Authority) generateProvisionerConfig(ctx context.Context) (provisioner.
|
|||
GetIdentityFunc: a.getIdentityFunc,
|
||||
AuthorizeRenewFunc: a.authorizeRenewFunc,
|
||||
AuthorizeSSHRenewFunc: a.authorizeSSHRenewFunc,
|
||||
WebhookClient: a.webhookClient,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -493,9 +494,63 @@ func optionsToCertificates(p *linkedca.Provisioner) *provisioner.Options {
|
|||
}
|
||||
}
|
||||
}
|
||||
for _, wh := range p.Webhooks {
|
||||
whCert := webhookToCertificates(wh)
|
||||
ops.Webhooks = append(ops.Webhooks, whCert)
|
||||
}
|
||||
return ops
|
||||
}
|
||||
|
||||
func webhookToCertificates(wh *linkedca.Webhook) *provisioner.Webhook {
|
||||
pwh := &provisioner.Webhook{
|
||||
ID: wh.Id,
|
||||
Name: wh.Name,
|
||||
URL: wh.Url,
|
||||
Kind: wh.Kind.String(),
|
||||
Secret: wh.Secret,
|
||||
DisableTLSClientAuth: wh.DisableTlsClientAuth,
|
||||
CertType: wh.CertType.String(),
|
||||
}
|
||||
|
||||
switch a := wh.GetAuth().(type) {
|
||||
case *linkedca.Webhook_BearerToken:
|
||||
pwh.BearerToken = a.BearerToken.BearerToken
|
||||
case *linkedca.Webhook_BasicAuth:
|
||||
pwh.BasicAuth.Username = a.BasicAuth.Username
|
||||
pwh.BasicAuth.Password = a.BasicAuth.Password
|
||||
}
|
||||
|
||||
return pwh
|
||||
}
|
||||
|
||||
func provisionerWebhookToLinkedca(pwh *provisioner.Webhook) *linkedca.Webhook {
|
||||
lwh := &linkedca.Webhook{
|
||||
Id: pwh.ID,
|
||||
Name: pwh.Name,
|
||||
Url: pwh.URL,
|
||||
Kind: linkedca.Webhook_Kind(linkedca.Webhook_Kind_value[pwh.Kind]),
|
||||
Secret: pwh.Secret,
|
||||
DisableTlsClientAuth: pwh.DisableTLSClientAuth,
|
||||
CertType: linkedca.Webhook_CertType(linkedca.Webhook_CertType_value[pwh.CertType]),
|
||||
}
|
||||
if pwh.BearerToken != "" {
|
||||
lwh.Auth = &linkedca.Webhook_BearerToken{
|
||||
BearerToken: &linkedca.BearerToken{
|
||||
BearerToken: pwh.BearerToken,
|
||||
},
|
||||
}
|
||||
} else if pwh.BasicAuth.Username != "" || pwh.BasicAuth.Password != "" {
|
||||
lwh.Auth = &linkedca.Webhook_BasicAuth{
|
||||
BasicAuth: &linkedca.BasicAuth{
|
||||
Username: pwh.BasicAuth.Username,
|
||||
Password: pwh.BasicAuth.Password,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return lwh
|
||||
}
|
||||
|
||||
func durationsToCertificates(d *linkedca.Durations) (min, max, def *provisioner.Duration, err error) {
|
||||
if len(d.Min) > 0 {
|
||||
min, err = provisioner.NewDuration(d.Min)
|
||||
|
@ -621,12 +676,12 @@ func claimsToLinkedca(c *provisioner.Claims) *linkedca.Claims {
|
|||
return lc
|
||||
}
|
||||
|
||||
func provisionerOptionsToLinkedca(p *provisioner.Options) (*linkedca.Template, *linkedca.Template, error) {
|
||||
func provisionerOptionsToLinkedca(p *provisioner.Options) (*linkedca.Template, *linkedca.Template, []*linkedca.Webhook, error) {
|
||||
var err error
|
||||
var x509Template, sshTemplate *linkedca.Template
|
||||
|
||||
if p == nil {
|
||||
return nil, nil, nil
|
||||
return nil, nil, nil, nil
|
||||
}
|
||||
|
||||
if p.X509 != nil && p.X509.HasTemplate() {
|
||||
|
@ -640,7 +695,7 @@ func provisionerOptionsToLinkedca(p *provisioner.Options) (*linkedca.Template, *
|
|||
} else if p.X509.TemplateFile != "" {
|
||||
filename := step.Abs(p.X509.TemplateFile)
|
||||
if x509Template.Template, err = os.ReadFile(filename); err != nil {
|
||||
return nil, nil, errors.Wrap(err, "error reading x509 template")
|
||||
return nil, nil, nil, errors.Wrap(err, "error reading x509 template")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -656,12 +711,17 @@ func provisionerOptionsToLinkedca(p *provisioner.Options) (*linkedca.Template, *
|
|||
} else if p.SSH.TemplateFile != "" {
|
||||
filename := step.Abs(p.SSH.TemplateFile)
|
||||
if sshTemplate.Template, err = os.ReadFile(filename); err != nil {
|
||||
return nil, nil, errors.Wrap(err, "error reading ssh template")
|
||||
return nil, nil, nil, errors.Wrap(err, "error reading ssh template")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return x509Template, sshTemplate, nil
|
||||
var webhooks []*linkedca.Webhook
|
||||
for _, pwh := range p.Webhooks {
|
||||
webhooks = append(webhooks, provisionerWebhookToLinkedca(pwh))
|
||||
}
|
||||
|
||||
return x509Template, sshTemplate, webhooks, nil
|
||||
}
|
||||
|
||||
func provisionerPEMToLinkedca(b []byte) [][]byte {
|
||||
|
@ -879,7 +939,7 @@ func ProvisionerToCertificates(p *linkedca.Provisioner) (provisioner.Interface,
|
|||
func ProvisionerToLinkedca(p provisioner.Interface) (*linkedca.Provisioner, error) {
|
||||
switch p := p.(type) {
|
||||
case *provisioner.JWK:
|
||||
x509Template, sshTemplate, err := provisionerOptionsToLinkedca(p.Options)
|
||||
x509Template, sshTemplate, webhooks, err := provisionerOptionsToLinkedca(p.Options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -902,9 +962,10 @@ func ProvisionerToLinkedca(p provisioner.Interface) (*linkedca.Provisioner, erro
|
|||
Claims: claimsToLinkedca(p.Claims),
|
||||
X509Template: x509Template,
|
||||
SshTemplate: sshTemplate,
|
||||
Webhooks: webhooks,
|
||||
}, nil
|
||||
case *provisioner.OIDC:
|
||||
x509Template, sshTemplate, err := provisionerOptionsToLinkedca(p.Options)
|
||||
x509Template, sshTemplate, webhooks, err := provisionerOptionsToLinkedca(p.Options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -929,9 +990,10 @@ func ProvisionerToLinkedca(p provisioner.Interface) (*linkedca.Provisioner, erro
|
|||
Claims: claimsToLinkedca(p.Claims),
|
||||
X509Template: x509Template,
|
||||
SshTemplate: sshTemplate,
|
||||
Webhooks: webhooks,
|
||||
}, nil
|
||||
case *provisioner.GCP:
|
||||
x509Template, sshTemplate, err := provisionerOptionsToLinkedca(p.Options)
|
||||
x509Template, sshTemplate, webhooks, err := provisionerOptionsToLinkedca(p.Options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -953,9 +1015,10 @@ func ProvisionerToLinkedca(p provisioner.Interface) (*linkedca.Provisioner, erro
|
|||
Claims: claimsToLinkedca(p.Claims),
|
||||
X509Template: x509Template,
|
||||
SshTemplate: sshTemplate,
|
||||
Webhooks: webhooks,
|
||||
}, nil
|
||||
case *provisioner.AWS:
|
||||
x509Template, sshTemplate, err := provisionerOptionsToLinkedca(p.Options)
|
||||
x509Template, sshTemplate, webhooks, err := provisionerOptionsToLinkedca(p.Options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -976,9 +1039,10 @@ func ProvisionerToLinkedca(p provisioner.Interface) (*linkedca.Provisioner, erro
|
|||
Claims: claimsToLinkedca(p.Claims),
|
||||
X509Template: x509Template,
|
||||
SshTemplate: sshTemplate,
|
||||
Webhooks: webhooks,
|
||||
}, nil
|
||||
case *provisioner.Azure:
|
||||
x509Template, sshTemplate, err := provisionerOptionsToLinkedca(p.Options)
|
||||
x509Template, sshTemplate, webhooks, err := provisionerOptionsToLinkedca(p.Options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -1002,9 +1066,10 @@ func ProvisionerToLinkedca(p provisioner.Interface) (*linkedca.Provisioner, erro
|
|||
Claims: claimsToLinkedca(p.Claims),
|
||||
X509Template: x509Template,
|
||||
SshTemplate: sshTemplate,
|
||||
Webhooks: webhooks,
|
||||
}, nil
|
||||
case *provisioner.ACME:
|
||||
x509Template, sshTemplate, err := provisionerOptionsToLinkedca(p.Options)
|
||||
x509Template, sshTemplate, webhooks, err := provisionerOptionsToLinkedca(p.Options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -1025,9 +1090,10 @@ func ProvisionerToLinkedca(p provisioner.Interface) (*linkedca.Provisioner, erro
|
|||
Claims: claimsToLinkedca(p.Claims),
|
||||
X509Template: x509Template,
|
||||
SshTemplate: sshTemplate,
|
||||
Webhooks: webhooks,
|
||||
}, nil
|
||||
case *provisioner.X5C:
|
||||
x509Template, sshTemplate, err := provisionerOptionsToLinkedca(p.Options)
|
||||
x509Template, sshTemplate, webhooks, err := provisionerOptionsToLinkedca(p.Options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -1045,9 +1111,10 @@ func ProvisionerToLinkedca(p provisioner.Interface) (*linkedca.Provisioner, erro
|
|||
Claims: claimsToLinkedca(p.Claims),
|
||||
X509Template: x509Template,
|
||||
SshTemplate: sshTemplate,
|
||||
Webhooks: webhooks,
|
||||
}, nil
|
||||
case *provisioner.K8sSA:
|
||||
x509Template, sshTemplate, err := provisionerOptionsToLinkedca(p.Options)
|
||||
x509Template, sshTemplate, webhooks, err := provisionerOptionsToLinkedca(p.Options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -1065,6 +1132,7 @@ func ProvisionerToLinkedca(p provisioner.Interface) (*linkedca.Provisioner, erro
|
|||
Claims: claimsToLinkedca(p.Claims),
|
||||
X509Template: x509Template,
|
||||
SshTemplate: sshTemplate,
|
||||
Webhooks: webhooks,
|
||||
}, nil
|
||||
case *provisioner.SSHPOP:
|
||||
return &linkedca.Provisioner{
|
||||
|
@ -1079,7 +1147,7 @@ func ProvisionerToLinkedca(p provisioner.Interface) (*linkedca.Provisioner, erro
|
|||
Claims: claimsToLinkedca(p.Claims),
|
||||
}, nil
|
||||
case *provisioner.SCEP:
|
||||
x509Template, sshTemplate, err := provisionerOptionsToLinkedca(p.Options)
|
||||
x509Template, sshTemplate, webhooks, err := provisionerOptionsToLinkedca(p.Options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -1102,9 +1170,10 @@ func ProvisionerToLinkedca(p provisioner.Interface) (*linkedca.Provisioner, erro
|
|||
Claims: claimsToLinkedca(p.Claims),
|
||||
X509Template: x509Template,
|
||||
SshTemplate: sshTemplate,
|
||||
Webhooks: webhooks,
|
||||
}, nil
|
||||
case *provisioner.Nebula:
|
||||
x509Template, sshTemplate, err := provisionerOptionsToLinkedca(p.Options)
|
||||
x509Template, sshTemplate, webhooks, err := provisionerOptionsToLinkedca(p.Options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -1122,6 +1191,7 @@ func ProvisionerToLinkedca(p provisioner.Interface) (*linkedca.Provisioner, erro
|
|||
Claims: claimsToLinkedca(p.Claims),
|
||||
X509Template: x509Template,
|
||||
SshTemplate: sshTemplate,
|
||||
Webhooks: webhooks,
|
||||
}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("provisioner %s not implemented", p.GetType())
|
||||
|
|
|
@ -16,6 +16,7 @@ import (
|
|||
"github.com/smallstep/certificates/db"
|
||||
"go.step.sm/crypto/jose"
|
||||
"go.step.sm/crypto/keyutil"
|
||||
"go.step.sm/linkedca"
|
||||
)
|
||||
|
||||
func TestGetEncryptedKey(t *testing.T) {
|
||||
|
@ -251,3 +252,82 @@ func TestAuthority_LoadProvisionerByCertificate(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvisionerWebhookToLinkedca(t *testing.T) {
|
||||
type test struct {
|
||||
lwh *linkedca.Webhook
|
||||
pwh *provisioner.Webhook
|
||||
}
|
||||
tests := map[string]test{
|
||||
"empty": test{
|
||||
lwh: &linkedca.Webhook{},
|
||||
pwh: &provisioner.Webhook{Kind: "NO_KIND", CertType: "ALL"},
|
||||
},
|
||||
"enriching ssh basic auth": test{
|
||||
lwh: &linkedca.Webhook{
|
||||
Id: "abc123",
|
||||
Name: "people",
|
||||
Url: "https://localhost",
|
||||
Kind: linkedca.Webhook_ENRICHING,
|
||||
Secret: "secret",
|
||||
Auth: &linkedca.Webhook_BasicAuth{
|
||||
BasicAuth: &linkedca.BasicAuth{
|
||||
Username: "user",
|
||||
Password: "pass",
|
||||
},
|
||||
},
|
||||
DisableTlsClientAuth: true,
|
||||
CertType: linkedca.Webhook_SSH,
|
||||
},
|
||||
pwh: &provisioner.Webhook{
|
||||
ID: "abc123",
|
||||
Name: "people",
|
||||
URL: "https://localhost",
|
||||
Kind: "ENRICHING",
|
||||
Secret: "secret",
|
||||
BasicAuth: struct {
|
||||
Username string
|
||||
Password string
|
||||
}{
|
||||
Username: "user",
|
||||
Password: "pass",
|
||||
},
|
||||
DisableTLSClientAuth: true,
|
||||
CertType: "SSH",
|
||||
},
|
||||
},
|
||||
"authorizing x509 bearer auth": test{
|
||||
lwh: &linkedca.Webhook{
|
||||
Id: "abc123",
|
||||
Name: "people",
|
||||
Url: "https://localhost",
|
||||
Kind: linkedca.Webhook_AUTHORIZING,
|
||||
Secret: "secret",
|
||||
Auth: &linkedca.Webhook_BearerToken{
|
||||
BearerToken: &linkedca.BearerToken{
|
||||
BearerToken: "tkn",
|
||||
},
|
||||
},
|
||||
CertType: linkedca.Webhook_X509,
|
||||
},
|
||||
pwh: &provisioner.Webhook{
|
||||
ID: "abc123",
|
||||
Name: "people",
|
||||
URL: "https://localhost",
|
||||
Kind: "AUTHORIZING",
|
||||
Secret: "secret",
|
||||
BearerToken: "tkn",
|
||||
CertType: "X509",
|
||||
},
|
||||
},
|
||||
}
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
gotLWH := provisionerWebhookToLinkedca(test.pwh)
|
||||
assert.Equals(t, test.lwh, gotLWH)
|
||||
|
||||
gotPWH := webhookToCertificates(test.lwh)
|
||||
assert.Equals(t, test.pwh, gotPWH)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ import (
|
|||
"github.com/smallstep/certificates/db"
|
||||
"github.com/smallstep/certificates/errs"
|
||||
"github.com/smallstep/certificates/templates"
|
||||
"github.com/smallstep/certificates/webhook"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -161,6 +162,7 @@ func (a *Authority) SignSSH(ctx context.Context, key ssh.PublicKey, opts provisi
|
|||
opts.Backdate = a.config.AuthorityConfig.Backdate.Duration
|
||||
|
||||
var prov provisioner.Interface
|
||||
var webhookCtl webhookController
|
||||
for _, op := range signOpts {
|
||||
switch o := op.(type) {
|
||||
// Capture current provisioner
|
||||
|
@ -185,6 +187,10 @@ func (a *Authority) SignSSH(ctx context.Context, key ssh.PublicKey, opts provisi
|
|||
return nil, errs.BadRequestErr(err, "error validating ssh certificate options")
|
||||
}
|
||||
|
||||
// call webhooks
|
||||
case webhookController:
|
||||
webhookCtl = o
|
||||
|
||||
default:
|
||||
return nil, errs.InternalServer("authority.SignSSH: invalid extra option type %T", o)
|
||||
}
|
||||
|
@ -198,6 +204,14 @@ func (a *Authority) SignSSH(ctx context.Context, key ssh.PublicKey, opts provisi
|
|||
Key: key,
|
||||
}
|
||||
|
||||
// Call enriching webhooks
|
||||
if err := callEnrichingWebhooksSSH(webhookCtl, cr); err != nil {
|
||||
return nil, errs.ApplyOptions(
|
||||
errs.ForbiddenErr(err, err.Error()),
|
||||
errs.WithKeyVal("signOptions", signOpts),
|
||||
)
|
||||
}
|
||||
|
||||
// Create certificate from template.
|
||||
certificate, err := sshutil.NewCertificate(cr, certOptions...)
|
||||
if err != nil {
|
||||
|
@ -262,6 +276,13 @@ func (a *Authority) SignSSH(ctx context.Context, key ssh.PublicKey, opts provisi
|
|||
)
|
||||
}
|
||||
|
||||
// Send certificate to webhooks for authorization
|
||||
if err := callAuthorizingWebhooksSSH(webhookCtl, certificate, certTpl); err != nil {
|
||||
return nil, errs.ApplyOptions(
|
||||
errs.ForbiddenErr(err, "authority.SignSSH: error signing certificate"),
|
||||
)
|
||||
}
|
||||
|
||||
// Sign certificate.
|
||||
cert, err := sshutil.CreateCertificate(certTpl, signer)
|
||||
if err != nil {
|
||||
|
@ -631,3 +652,37 @@ func (a *Authority) getAddUserCommand(principal string) string {
|
|||
}
|
||||
return strings.ReplaceAll(cmd, "<principal>", principal)
|
||||
}
|
||||
|
||||
func callEnrichingWebhooksSSH(webhookCtl webhookController, cr sshutil.CertificateRequest) error {
|
||||
if webhookCtl == nil {
|
||||
return nil
|
||||
}
|
||||
whEnrichReq, err := webhook.NewRequestBody(
|
||||
webhook.WithSSHCertificateRequest(cr),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := webhookCtl.Enrich(whEnrichReq); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func callAuthorizingWebhooksSSH(webhookCtl webhookController, cert *sshutil.Certificate, certTpl *ssh.Certificate) error {
|
||||
if webhookCtl == nil {
|
||||
return nil
|
||||
}
|
||||
whAuthBody, err := webhook.NewRequestBody(
|
||||
webhook.WithSSHCertificate(cert, certTpl),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := webhookCtl.Authorize(whAuthBody); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -178,6 +178,17 @@ func TestAuthority_SignSSH(t *testing.T) {
|
|||
}`},
|
||||
}, sshutil.CreateTemplateData(sshutil.UserCert, "key-id", []string{"user"}))
|
||||
assert.FatalError(t, err)
|
||||
enrichTemplateData := sshutil.CreateTemplateData(sshutil.UserCert, "key-id", []string{"user"})
|
||||
enrichTemplate, err := provisioner.TemplateSSHOptions(&provisioner.Options{
|
||||
SSH: &provisioner.SSHOptions{Template: `{
|
||||
"type": "{{ .Type }}",
|
||||
"keyId": "{{ .KeyID }}",
|
||||
"principals": {{ toJson .Webhooks.people.role }},
|
||||
"extensions": {{ set .Extensions "login@github.com" .Insecure.User.username | toJson }},
|
||||
"criticalOptions": {{ toJson .CriticalOptions }}
|
||||
}`},
|
||||
}, enrichTemplateData)
|
||||
assert.FatalError(t, err)
|
||||
userFailTemplate, err := provisioner.TemplateSSHOptions(&provisioner.Options{
|
||||
SSH: &provisioner.SSHOptions{Template: `{{ fail "an error"}}`},
|
||||
}, sshutil.CreateTemplateData(sshutil.UserCert, "key-id", []string{"user"}))
|
||||
|
@ -255,6 +266,7 @@ func TestAuthority_SignSSH(t *testing.T) {
|
|||
{"ok-opts-validator", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, sshTestOptionsValidator("")}}, want{CertType: ssh.UserCert}, false},
|
||||
{"ok-opts-modifier", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, sshTestOptionsModifier("")}}, want{CertType: ssh.UserCert}, false},
|
||||
{"ok-custom-template", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userCustomTemplate, userOptions}}, want{CertType: ssh.UserCert, Principals: []string{"user", "admin"}}, false},
|
||||
{"ok-enrich-template", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{enrichTemplate, userOptions, &mockWebhookController{templateData: enrichTemplateData, respData: map[string]any{"people": map[string]any{"role": []string{"user", "eng"}}}}}}, want{CertType: ssh.UserCert, Principals: []string{"user", "eng"}}, false},
|
||||
{"ok-user-policy", fields{signer, signer, userPolicy}, args{pub, provisioner.SignSSHOptions{CertType: "user", Principals: []string{"user"}}, []provisioner.SignOption{userTemplateWithUser}}, want{CertType: ssh.UserCert, Principals: []string{"user"}}, false},
|
||||
{"ok-host-policy", fields{signer, signer, hostPolicy}, args{pub, provisioner.SignSSHOptions{CertType: "host", Principals: []string{"foo.test.com", "bar.test.com"}}, []provisioner.SignOption{hostTemplateWithHosts}}, want{CertType: ssh.HostCert, Principals: []string{"foo.test.com", "bar.test.com"}}, false},
|
||||
{"fail-opts-type", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{CertType: "foo"}, []provisioner.SignOption{userTemplate}}, want{}, true},
|
||||
|
@ -275,6 +287,8 @@ func TestAuthority_SignSSH(t *testing.T) {
|
|||
{"fail-host-policy", fields{signer, signer, hostPolicy}, args{pub, provisioner.SignSSHOptions{CertType: "host", Principals: []string{"example.com"}}, []provisioner.SignOption{hostTemplateWithExampleDotCom}}, want{}, true},
|
||||
{"fail-host-policy-with-user-cert", fields{signer, signer, hostPolicy}, args{pub, provisioner.SignSSHOptions{CertType: "user", Principals: []string{"user"}}, []provisioner.SignOption{userTemplateWithUser}}, want{}, true},
|
||||
{"fail-host-policy-with-bad-host", fields{signer, signer, hostPolicy}, args{pub, provisioner.SignSSHOptions{CertType: "host", Principals: []string{"example.com"}}, []provisioner.SignOption{badHostTemplate}}, want{}, true},
|
||||
{"fail-enriching-webhooks", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, &mockWebhookController{enrichErr: provisioner.ErrWebhookDenied}}}, want{}, true},
|
||||
{"fail-authorizing-webhooks", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, &mockWebhookController{authorizeErr: provisioner.ErrWebhookDenied}}}, want{}, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
|
|
@ -28,6 +28,7 @@ import (
|
|||
casapi "github.com/smallstep/certificates/cas/apiv1"
|
||||
"github.com/smallstep/certificates/db"
|
||||
"github.com/smallstep/certificates/errs"
|
||||
"github.com/smallstep/certificates/webhook"
|
||||
)
|
||||
|
||||
// GetTLSOptions returns the tls options configured.
|
||||
|
@ -93,7 +94,8 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Sign
|
|||
|
||||
var prov provisioner.Interface
|
||||
var pInfo *casapi.ProvisionerInfo
|
||||
var attData provisioner.AttestationData
|
||||
var attData *provisioner.AttestationData
|
||||
var webhookCtl webhookController
|
||||
for _, op := range extraOpts {
|
||||
switch k := op.(type) {
|
||||
// Capture current provisioner
|
||||
|
@ -131,14 +133,25 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Sign
|
|||
|
||||
// Extra information from ACME attestations.
|
||||
case provisioner.AttestationData:
|
||||
attData = k
|
||||
// TODO(mariano,areed): remove me once attData is used.
|
||||
_ = attData
|
||||
attData = &k
|
||||
|
||||
// Capture the provisioner's webhook controller
|
||||
case webhookController:
|
||||
webhookCtl = k
|
||||
|
||||
default:
|
||||
return nil, errs.InternalServer("authority.Sign; invalid extra option type %T", append([]interface{}{k}, opts...)...)
|
||||
}
|
||||
}
|
||||
|
||||
if err := callEnrichingWebhooksX509(webhookCtl, attData, csr); err != nil {
|
||||
return nil, errs.ApplyOptions(
|
||||
errs.ForbiddenErr(err, err.Error()),
|
||||
errs.WithKeyVal("csr", csr),
|
||||
errs.WithKeyVal("signOptions", signOpts),
|
||||
)
|
||||
}
|
||||
|
||||
cert, err := x509util.NewCertificate(csr, certOptions...)
|
||||
if err != nil {
|
||||
var te *x509util.TemplateError
|
||||
|
@ -223,6 +236,14 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Sign
|
|||
)
|
||||
}
|
||||
|
||||
// Send certificate to webhooks for authorization
|
||||
if err := callAuthorizingWebhooksX509(webhookCtl, cert, leaf, attData); err != nil {
|
||||
return nil, errs.ApplyOptions(
|
||||
errs.ForbiddenErr(err, "error creating certificate"),
|
||||
opts...,
|
||||
)
|
||||
}
|
||||
|
||||
// Sign certificate
|
||||
lifetime := leaf.NotAfter.Sub(leaf.NotBefore.Add(signOpts.Backdate))
|
||||
resp, err := a.x509CAService.CreateCertificate(&casapi.CreateCertificateRequest{
|
||||
|
@ -699,3 +720,51 @@ func templatingError(err error) error {
|
|||
}
|
||||
return errors.Wrap(cause, "error applying certificate template")
|
||||
}
|
||||
|
||||
func callEnrichingWebhooksX509(webhookCtl webhookController, attData *provisioner.AttestationData, csr *x509.CertificateRequest) error {
|
||||
if webhookCtl == nil {
|
||||
return nil
|
||||
}
|
||||
var attested *webhook.AttestationData
|
||||
if attData != nil {
|
||||
attested = &webhook.AttestationData{
|
||||
PermanentIdentifier: attData.PermanentIdentifier,
|
||||
}
|
||||
}
|
||||
whEnrichReq, err := webhook.NewRequestBody(
|
||||
webhook.WithX509CertificateRequest(csr),
|
||||
webhook.WithAttestationData(attested),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := webhookCtl.Enrich(whEnrichReq); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func callAuthorizingWebhooksX509(webhookCtl webhookController, cert *x509util.Certificate, leaf *x509.Certificate, attData *provisioner.AttestationData) error {
|
||||
if webhookCtl == nil {
|
||||
return nil
|
||||
}
|
||||
var attested *webhook.AttestationData
|
||||
if attData != nil {
|
||||
attested = &webhook.AttestationData{
|
||||
PermanentIdentifier: attData.PermanentIdentifier,
|
||||
}
|
||||
}
|
||||
whAuthBody, err := webhook.NewRequestBody(
|
||||
webhook.WithX509Certificate(cert, leaf),
|
||||
webhook.WithAttestationData(attested),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := webhookCtl.Authorize(whAuthBody); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -547,6 +547,36 @@ ZYtQ9Ot36qc=
|
|||
code: http.StatusForbidden,
|
||||
}
|
||||
},
|
||||
"fail enriching webhooks": func(t *testing.T) *signTest {
|
||||
csr := getCSR(t, priv)
|
||||
csr.Raw = []byte("foo")
|
||||
return &signTest{
|
||||
auth: a,
|
||||
csr: csr,
|
||||
extensionsCount: 7,
|
||||
extraOpts: append(extraOpts, &mockWebhookController{
|
||||
enrichErr: provisioner.ErrWebhookDenied,
|
||||
}),
|
||||
signOpts: signOpts,
|
||||
err: provisioner.ErrWebhookDenied,
|
||||
code: http.StatusForbidden,
|
||||
}
|
||||
},
|
||||
"fail authorizing webhooks": func(t *testing.T) *signTest {
|
||||
csr := getCSR(t, priv)
|
||||
csr.Raw = []byte("foo")
|
||||
return &signTest{
|
||||
auth: a,
|
||||
csr: csr,
|
||||
extensionsCount: 7,
|
||||
extraOpts: append(extraOpts, &mockWebhookController{
|
||||
authorizeErr: provisioner.ErrWebhookDenied,
|
||||
}),
|
||||
signOpts: signOpts,
|
||||
err: provisioner.ErrWebhookDenied,
|
||||
code: http.StatusForbidden,
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) *signTest {
|
||||
csr := getCSR(t, priv)
|
||||
_a := testAuthority(t)
|
||||
|
@ -634,6 +664,48 @@ ZYtQ9Ot36qc=
|
|||
extensionsCount: 6,
|
||||
}
|
||||
},
|
||||
"ok with enriching webhook": func(t *testing.T) *signTest {
|
||||
csr := getCSR(t, priv)
|
||||
testAuthority := testAuthority(t)
|
||||
testAuthority.config.AuthorityConfig.Template = a.config.AuthorityConfig.Template
|
||||
p, ok := testAuthority.provisioners.Load("step-cli:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc")
|
||||
if !ok {
|
||||
t.Fatal("provisioner not found")
|
||||
}
|
||||
p.(*provisioner.JWK).Options = &provisioner.Options{
|
||||
X509: &provisioner.X509Options{Template: `{
|
||||
"subject": {"commonName": {{ toJson .Webhooks.people.role }} },
|
||||
"dnsNames": {{ toJson .Insecure.CR.DNSNames }},
|
||||
"keyUsage": ["digitalSignature"],
|
||||
"extKeyUsage": ["serverAuth","clientAuth"]
|
||||
}`},
|
||||
}
|
||||
testExtraOpts, err := testAuthority.Authorize(ctx, token)
|
||||
assert.FatalError(t, err)
|
||||
testAuthority.db = &db.MockAuthDB{
|
||||
MStoreCertificate: func(crt *x509.Certificate) error {
|
||||
assert.Equals(t, crt.Subject.CommonName, "smallstep test")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
for i, o := range testExtraOpts {
|
||||
if wc, ok := o.(*provisioner.WebhookController); ok {
|
||||
testExtraOpts[i] = &mockWebhookController{
|
||||
templateData: wc.TemplateData,
|
||||
respData: map[string]any{"people": map[string]any{"role": "smallstep test"}},
|
||||
}
|
||||
}
|
||||
}
|
||||
return &signTest{
|
||||
auth: testAuthority,
|
||||
csr: csr,
|
||||
extraOpts: testExtraOpts,
|
||||
signOpts: signOpts,
|
||||
notBefore: signOpts.NotBefore.Time().Truncate(time.Second),
|
||||
notAfter: signOpts.NotAfter.Time().Truncate(time.Second),
|
||||
extensionsCount: 6,
|
||||
}
|
||||
},
|
||||
"ok/csr with no template critical SAN extension": func(t *testing.T) *signTest {
|
||||
csr := getCSR(t, priv, func(csr *x509.CertificateRequest) {
|
||||
csr.Subject = pkix.Name{}
|
||||
|
|
8
authority/webhook.go
Normal file
8
authority/webhook.go
Normal file
|
@ -0,0 +1,8 @@
|
|||
package authority
|
||||
|
||||
import "github.com/smallstep/certificates/webhook"
|
||||
|
||||
type webhookController interface {
|
||||
Enrich(*webhook.RequestBody) error
|
||||
Authorize(*webhook.RequestBody) error
|
||||
}
|
27
authority/webhook_test.go
Normal file
27
authority/webhook_test.go
Normal file
|
@ -0,0 +1,27 @@
|
|||
package authority
|
||||
|
||||
import (
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
"github.com/smallstep/certificates/webhook"
|
||||
)
|
||||
|
||||
type mockWebhookController struct {
|
||||
enrichErr error
|
||||
authorizeErr error
|
||||
templateData provisioner.WebhookSetter
|
||||
respData map[string]any
|
||||
}
|
||||
|
||||
var _ webhookController = &mockWebhookController{}
|
||||
|
||||
func (wc *mockWebhookController) Enrich(req *webhook.RequestBody) error {
|
||||
for key, data := range wc.respData {
|
||||
wc.templateData.SetWebhook(key, data)
|
||||
}
|
||||
|
||||
return wc.enrichErr
|
||||
}
|
||||
|
||||
func (wc *mockWebhookController) Authorize(req *webhook.RequestBody) error {
|
||||
return wc.authorizeErr
|
||||
}
|
|
@ -1101,6 +1101,103 @@ retry:
|
|||
return nil
|
||||
}
|
||||
|
||||
func (c *AdminClient) CreateProvisionerWebhook(provisionerName string, wh *linkedca.Webhook) (*linkedca.Webhook, error) {
|
||||
var retried bool
|
||||
body, err := protojson.Marshal(wh)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error marshaling request: %w", err)
|
||||
}
|
||||
u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "provisioners", provisionerName, "webhooks")})
|
||||
tok, err := c.generateAdminToken(u)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error generating admin token: %w", err)
|
||||
}
|
||||
retry:
|
||||
req, err := http.NewRequest(http.MethodPost, u.String(), bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating POST %s request failed: %w", u, err)
|
||||
}
|
||||
req.Header.Add("Authorization", tok)
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("client POST %s failed: %w", u, err)
|
||||
}
|
||||
if resp.StatusCode >= 400 {
|
||||
if !retried && c.retryOnError(resp) {
|
||||
retried = true
|
||||
goto retry
|
||||
}
|
||||
return nil, readAdminError(resp.Body)
|
||||
}
|
||||
var webhook = new(linkedca.Webhook)
|
||||
if err := readProtoJSON(resp.Body, webhook); err != nil {
|
||||
return nil, fmt.Errorf("error reading %s: %w", u, err)
|
||||
}
|
||||
return webhook, nil
|
||||
}
|
||||
|
||||
func (c *AdminClient) UpdateProvisionerWebhook(provisionerName string, wh *linkedca.Webhook) (*linkedca.Webhook, error) {
|
||||
var retried bool
|
||||
body, err := protojson.Marshal(wh)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error marshaling request: %w", err)
|
||||
}
|
||||
u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "provisioners", provisionerName, "webhooks", wh.Name)})
|
||||
tok, err := c.generateAdminToken(u)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error generating admin token: %w", err)
|
||||
}
|
||||
retry:
|
||||
req, err := http.NewRequest(http.MethodPut, u.String(), bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating PUT %s request failed: %w", u, err)
|
||||
}
|
||||
req.Header.Add("Authorization", tok)
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("client PUT %s failed: %w", u, err)
|
||||
}
|
||||
if resp.StatusCode >= 400 {
|
||||
if !retried && c.retryOnError(resp) {
|
||||
retried = true
|
||||
goto retry
|
||||
}
|
||||
return nil, readAdminError(resp.Body)
|
||||
}
|
||||
var webhook = new(linkedca.Webhook)
|
||||
if err := readProtoJSON(resp.Body, webhook); err != nil {
|
||||
return nil, fmt.Errorf("error reading %s: %w", u, err)
|
||||
}
|
||||
return webhook, nil
|
||||
}
|
||||
|
||||
func (c *AdminClient) DeleteProvisionerWebhook(provisionerName, webhookName string) error {
|
||||
var retried bool
|
||||
u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "provisioners", provisionerName, "webhooks", webhookName)})
|
||||
tok, err := c.generateAdminToken(u)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error generating admin token: %w", err)
|
||||
}
|
||||
retry:
|
||||
req, err := http.NewRequest(http.MethodDelete, u.String(), http.NoBody)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating DELETE %s request failed: %w", u, err)
|
||||
}
|
||||
req.Header.Add("Authorization", tok)
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("client DELETE %s failed: %w", u, err)
|
||||
}
|
||||
if resp.StatusCode >= 400 {
|
||||
if !retried && c.retryOnError(resp) {
|
||||
retried = true
|
||||
goto retry
|
||||
}
|
||||
return readAdminError(resp.Body)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func readAdminError(r io.ReadCloser) error {
|
||||
// TODO: not all errors can be read (i.e. 404); seems to be a bigger issue
|
||||
defer r.Close()
|
||||
|
|
57
ca/ca.go
57
ca/ca.go
|
@ -156,17 +156,22 @@ func (ca *CA) Init(cfg *config.Config) (*CA, error) {
|
|||
opts = append(opts, authority.WithDatabase(ca.opts.database))
|
||||
}
|
||||
|
||||
webhookTransport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
opts = append(opts, authority.WithWebhookClient(&http.Client{Transport: webhookTransport}))
|
||||
|
||||
auth, err := authority.New(cfg, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ca.auth = auth
|
||||
|
||||
tlsConfig, err := ca.getTLSConfig(auth)
|
||||
tlsConfig, clientTLSConfig, err := ca.getTLSConfig(auth)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
webhookTransport.TLSClientConfig = clientTLSConfig
|
||||
|
||||
// Using chi as the main router
|
||||
mux := chi.NewRouter()
|
||||
handler := http.Handler(mux)
|
||||
|
@ -220,8 +225,14 @@ func (ca *CA) Init(cfg *config.Config) (*CA, error) {
|
|||
if adminDB != nil {
|
||||
acmeAdminResponder := adminAPI.NewACMEAdminResponder()
|
||||
policyAdminResponder := adminAPI.NewPolicyAdminResponder()
|
||||
webhookAdminResponder := adminAPI.NewWebhookAdminResponder()
|
||||
mux.Route("/admin", func(r chi.Router) {
|
||||
adminAPI.Route(r, acmeAdminResponder, policyAdminResponder)
|
||||
adminAPI.Route(
|
||||
r,
|
||||
adminAPI.WithACMEResponder(acmeAdminResponder),
|
||||
adminAPI.WithPolicyResponder(policyAdminResponder),
|
||||
adminAPI.WithWebhookResponder(webhookAdminResponder),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -456,13 +467,13 @@ func (ca *CA) Reload() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// getTLSConfig returns a TLSConfig for the CA server with a self-renewing
|
||||
// server certificate.
|
||||
func (ca *CA) getTLSConfig(auth *authority.Authority) (*tls.Config, error) {
|
||||
// get TLSConfig returns separate TLSConfigs for server and client with the
|
||||
// same self-renewing certificate.
|
||||
func (ca *CA) getTLSConfig(auth *authority.Authority) (*tls.Config, *tls.Config, error) {
|
||||
// Create initial TLS certificate
|
||||
tlsCrt, err := auth.GetTLSCertificate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Start tls renewer with the new certificate.
|
||||
|
@ -473,15 +484,15 @@ func (ca *CA) getTLSConfig(auth *authority.Authority) (*tls.Config, error) {
|
|||
|
||||
ca.renewer, err = NewTLSRenewer(tlsCrt, auth.GetTLSCertificate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
ca.renewer.Run()
|
||||
|
||||
var tlsConfig *tls.Config
|
||||
var serverTLSConfig *tls.Config
|
||||
if ca.config.TLS != nil {
|
||||
tlsConfig = ca.config.TLS.TLSConfig()
|
||||
serverTLSConfig = ca.config.TLS.TLSConfig()
|
||||
} else {
|
||||
tlsConfig = &tls.Config{
|
||||
serverTLSConfig = &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
}
|
||||
}
|
||||
|
@ -493,13 +504,24 @@ func (ca *CA) getTLSConfig(auth *authority.Authority) (*tls.Config, error) {
|
|||
// first entry in the Certificates attribute; by setting the attribute to
|
||||
// empty we are implicitly forcing GetCertificate to be the only mechanism
|
||||
// by which the server can find it's own leaf Certificate.
|
||||
tlsConfig.Certificates = []tls.Certificate{}
|
||||
tlsConfig.GetCertificate = ca.renewer.GetCertificateForCA
|
||||
serverTLSConfig.Certificates = []tls.Certificate{}
|
||||
|
||||
clientTLSConfig := serverTLSConfig.Clone()
|
||||
|
||||
serverTLSConfig.GetCertificate = ca.renewer.GetCertificateForCA
|
||||
clientTLSConfig.GetClientCertificate = ca.renewer.GetClientCertificate
|
||||
|
||||
// initialize a certificate pool with root CA certificates to trust when doing mTLS.
|
||||
certPool := x509.NewCertPool()
|
||||
// initialize a certificate pool with root CA certificates to trust when connecting
|
||||
// to webhook servers
|
||||
rootCAsPool, err := x509.SystemCertPool()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
for _, crt := range auth.GetRootCertificates() {
|
||||
certPool.AddCert(crt)
|
||||
rootCAsPool.AddCert(crt)
|
||||
}
|
||||
|
||||
// adding the intermediate CA certificates to the pool will allow clients that
|
||||
|
@ -509,16 +531,19 @@ func (ca *CA) getTLSConfig(auth *authority.Authority) (*tls.Config, error) {
|
|||
for _, certBytes := range intermediates {
|
||||
cert, err := x509.ParseCertificate(certBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
certPool.AddCert(cert)
|
||||
rootCAsPool.AddCert(cert)
|
||||
}
|
||||
|
||||
// Add support for mutual tls to renew certificates
|
||||
tlsConfig.ClientAuth = tls.VerifyClientCertIfGiven
|
||||
tlsConfig.ClientCAs = certPool
|
||||
serverTLSConfig.ClientAuth = tls.VerifyClientCertIfGiven
|
||||
serverTLSConfig.ClientCAs = certPool
|
||||
|
||||
return tlsConfig, nil
|
||||
clientTLSConfig.RootCAs = rootCAsPool
|
||||
|
||||
return serverTLSConfig, clientTLSConfig, nil
|
||||
}
|
||||
|
||||
// shouldServeSCEPEndpoints returns if the CA should be
|
||||
|
|
9
go.mod
9
go.mod
|
@ -6,7 +6,7 @@ require (
|
|||
cloud.google.com/go v0.102.1
|
||||
cloud.google.com/go/security v1.7.0
|
||||
github.com/Azure/azure-sdk-for-go v65.0.0+incompatible // indirect
|
||||
github.com/Azure/go-autorest/autorest v0.11.27 // indirect
|
||||
github.com/Azure/go-autorest/autorest v0.11.28 // indirect
|
||||
github.com/Azure/go-autorest/autorest/azure/auth v0.5.11 // indirect
|
||||
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
|
||||
github.com/Masterminds/sprig/v3 v3.2.2
|
||||
|
@ -22,7 +22,7 @@ require (
|
|||
github.com/golang/mock v1.6.0
|
||||
github.com/google/go-cmp v0.5.8
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/googleapis/gax-go/v2 v2.4.0
|
||||
github.com/googleapis/gax-go/v2 v2.5.1
|
||||
github.com/hashicorp/vault/api v1.3.1
|
||||
github.com/hashicorp/vault/api/auth/approle v0.1.1
|
||||
github.com/hashicorp/vault/api/auth/kubernetes v0.1.0
|
||||
|
@ -42,13 +42,13 @@ require (
|
|||
github.com/urfave/cli v1.22.4
|
||||
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352
|
||||
go.step.sm/cli-utils v0.7.4
|
||||
go.step.sm/crypto v0.19.0
|
||||
go.step.sm/crypto v0.19.1-0.20220929182301-ae99d3fe3185
|
||||
go.step.sm/linkedca v0.19.0-rc.2
|
||||
golang.org/x/crypto v0.0.0-20220919173607-35f4265a4bc0
|
||||
golang.org/x/net v0.0.0-20220927171203-f486391704dc
|
||||
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec // indirect
|
||||
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect
|
||||
google.golang.org/api v0.96.0
|
||||
google.golang.org/api v0.97.0
|
||||
google.golang.org/genproto v0.0.0-20220929141241-1ce7b20da813
|
||||
google.golang.org/grpc v1.49.0
|
||||
google.golang.org/protobuf v1.28.1
|
||||
|
@ -147,6 +147,7 @@ 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
|
||||
|
||||
|
|
16
go.sum
16
go.sum
|
@ -73,8 +73,8 @@ github.com/Azure/azure-sdk-for-go v65.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9mo
|
|||
github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs=
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
|
||||
github.com/Azure/go-autorest/autorest v0.11.24/go.mod h1:G6kyRlFnTuSbEYkQGawPfsCswgme4iYf6rfSKUDzbCc=
|
||||
github.com/Azure/go-autorest/autorest v0.11.27 h1:F3R3q42aWytozkV8ihzcgMO4OA4cuqr3bNlsEuF6//A=
|
||||
github.com/Azure/go-autorest/autorest v0.11.27/go.mod h1:7l8ybrIdUmGqZMTD0sRtAr8NvbHjfofbf8RSP2q7w7U=
|
||||
github.com/Azure/go-autorest/autorest v0.11.28 h1:ndAExarwr5Y+GaHE6VCaY1kyS/HwwGGyuimVhWsHOEM=
|
||||
github.com/Azure/go-autorest/autorest v0.11.28/go.mod h1:MrkzG3Y3AH668QyF9KRk5neJnGgmhQ6krbhR8Q5eMvA=
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.18 h1:kLnPsRjzZZUF3K5REu/Kc+qMQrvuza2bwSnNdhmzLfQ=
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.18/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ=
|
||||
github.com/Azure/go-autorest/autorest/azure/auth v0.5.11 h1:P6bYXFoao05z5uhOQzbC3Qd8JqF3jUoocoTeIxkp2cA=
|
||||
|
@ -362,8 +362,9 @@ github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pf
|
|||
github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM=
|
||||
github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM=
|
||||
github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM=
|
||||
github.com/googleapis/gax-go/v2 v2.4.0 h1:dS9eYAjhrE2RjmzYw2XAPvcXfmcQLtFEQWn0CR82awk=
|
||||
github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c=
|
||||
github.com/googleapis/gax-go/v2 v2.5.1 h1:kBRZU0PSuI7PspsSb/ChWoVResUcwNVIdpB049pKTiw=
|
||||
github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo=
|
||||
github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gordonklaus/ineffassign v0.0.0-20200309095847-7953dde2c7bf/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU=
|
||||
|
@ -783,8 +784,8 @@ go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqe
|
|||
go.step.sm/cli-utils v0.7.4 h1:oI7PStZqlvjPZ0u2EB4lN7yZ4R3ShTotdGL/L84Oorg=
|
||||
go.step.sm/cli-utils v0.7.4/go.mod h1:taSsY8haLmXoXM3ZkywIyRmVij/4Aj0fQbNTlJvv71I=
|
||||
go.step.sm/crypto v0.9.0/go.mod h1:+CYG05Mek1YDqi5WK0ERc6cOpKly2i/a5aZmU1sfGj0=
|
||||
go.step.sm/crypto v0.19.0 h1:WxjUDeTDpuPZ1IR3v6c4jc6WdlQlS5IYYQBhfnG5uW0=
|
||||
go.step.sm/crypto v0.19.0/go.mod h1:qZ+pNU1nV+THwP7TPTNCRMRr9xrRURhETTAK7U5psfw=
|
||||
go.step.sm/crypto v0.19.1-0.20220929182301-ae99d3fe3185 h1:W+UhojTrFZngWTudpP3n9vPs4UNLudVSkKrWZuZg/RU=
|
||||
go.step.sm/crypto v0.19.1-0.20220929182301-ae99d3fe3185/go.mod h1:972LarNeN9dgx4+zkF3fHCnTWLXzuQSIOdMaGeIslUY=
|
||||
go.step.sm/linkedca v0.19.0-rc.2 h1:IcPqZ5y7MZNq1+VbYQcKoQEvX80NKRncU1WFCDyY+So=
|
||||
go.step.sm/linkedca v0.19.0-rc.2/go.mod h1:MCZmPIdzElEofZbiw4eyUHayXgFTwa94cNAV34aJ5ew=
|
||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
|
@ -818,6 +819,7 @@ golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5y
|
|||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220919173607-35f4265a4bc0 h1:a5Yg6ylndHHYJqIPrdq0AhvR6KTvDTAvgBtaidhEevY=
|
||||
golang.org/x/crypto v0.0.0-20220919173607-35f4265a4bc0/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
|
@ -1160,8 +1162,8 @@ google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69
|
|||
google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw=
|
||||
google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg=
|
||||
google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o=
|
||||
google.golang.org/api v0.96.0 h1:F60cuQPJq7K7FzsxMYHAUJSiXh2oKctHxBMbDygxhfM=
|
||||
google.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s=
|
||||
google.golang.org/api v0.97.0 h1:x/vEL1XDF/2V4xzdNgFPaKHluRESo2aTsL7QzHnBtGQ=
|
||||
google.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
|
|
|
@ -281,6 +281,14 @@ func (a *Authority) SignCSR(ctx context.Context, csr *x509.CertificateRequest, m
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("error retrieving authorization options from SCEP provisioner: %w", err)
|
||||
}
|
||||
// Unlike most of the provisioners, scep's AuthorizeSign method doesn't
|
||||
// define the templates, and the template data used in WebHooks is not
|
||||
// available.
|
||||
for _, signOp := range signOps {
|
||||
if wc, ok := signOp.(*provisioner.WebhookController); ok {
|
||||
wc.TemplateData = data
|
||||
}
|
||||
}
|
||||
|
||||
opts := provisioner.SignOptions{}
|
||||
templateOptions, err := provisioner.TemplateOptions(p.GetOptions(), data)
|
||||
|
|
97
webhook/options.go
Normal file
97
webhook/options.go
Normal file
|
@ -0,0 +1,97 @@
|
|||
package webhook
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
|
||||
"go.step.sm/crypto/sshutil"
|
||||
"go.step.sm/crypto/x509util"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
type RequestBodyOption func(*RequestBody) error
|
||||
|
||||
func NewRequestBody(options ...RequestBodyOption) (*RequestBody, error) {
|
||||
rb := &RequestBody{}
|
||||
|
||||
for _, fn := range options {
|
||||
if err := fn(rb); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return rb, nil
|
||||
}
|
||||
|
||||
func WithX509CertificateRequest(cr *x509.CertificateRequest) RequestBodyOption {
|
||||
return func(rb *RequestBody) error {
|
||||
rb.X509CertificateRequest = &X509CertificateRequest{
|
||||
CertificateRequest: x509util.NewCertificateRequestFromX509(cr),
|
||||
PublicKeyAlgorithm: cr.PublicKeyAlgorithm.String(),
|
||||
Raw: cr.Raw,
|
||||
}
|
||||
if cr.PublicKey != nil {
|
||||
key, err := x509.MarshalPKIXPublicKey(cr.PublicKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rb.X509CertificateRequest.PublicKey = key
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithX509Certificate(cert *x509util.Certificate, leaf *x509.Certificate) RequestBodyOption {
|
||||
return func(rb *RequestBody) error {
|
||||
rb.X509Certificate = &X509Certificate{
|
||||
Certificate: cert,
|
||||
PublicKeyAlgorithm: leaf.PublicKeyAlgorithm.String(),
|
||||
NotBefore: leaf.NotBefore,
|
||||
NotAfter: leaf.NotAfter,
|
||||
}
|
||||
if leaf.PublicKey != nil {
|
||||
key, err := x509.MarshalPKIXPublicKey(leaf.PublicKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rb.X509Certificate.PublicKey = key
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithAttestationData(data *AttestationData) RequestBodyOption {
|
||||
return func(rb *RequestBody) error {
|
||||
rb.AttestationData = data
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithSSHCertificateRequest(cr sshutil.CertificateRequest) RequestBodyOption {
|
||||
return func(rb *RequestBody) error {
|
||||
rb.SSHCertificateRequest = &SSHCertificateRequest{
|
||||
Type: cr.Type,
|
||||
KeyID: cr.KeyID,
|
||||
Principals: cr.Principals,
|
||||
}
|
||||
if cr.Key != nil {
|
||||
rb.SSHCertificateRequest.PublicKey = cr.Key.Marshal()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithSSHCertificate(cert *sshutil.Certificate, certTpl *ssh.Certificate) RequestBodyOption {
|
||||
return func(rb *RequestBody) error {
|
||||
rb.SSHCertificate = &SSHCertificate{
|
||||
Certificate: cert,
|
||||
ValidBefore: certTpl.ValidBefore,
|
||||
ValidAfter: certTpl.ValidAfter,
|
||||
}
|
||||
if certTpl.Key != nil {
|
||||
rb.SSHCertificate.PublicKey = certTpl.Key.Marshal()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
116
webhook/options_test.go
Normal file
116
webhook/options_test.go
Normal file
|
@ -0,0 +1,116 @@
|
|||
package webhook
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/smallstep/assert"
|
||||
"go.step.sm/crypto/sshutil"
|
||||
"go.step.sm/crypto/x509util"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
func TestNewRequestBody(t *testing.T) {
|
||||
t1 := time.Now()
|
||||
t2 := t1.Add(time.Hour)
|
||||
|
||||
type test struct {
|
||||
options []RequestBodyOption
|
||||
want *RequestBody
|
||||
wantErr bool
|
||||
}
|
||||
tests := map[string]test{
|
||||
"Permanent Identifier": {
|
||||
options: []RequestBodyOption{WithAttestationData(&AttestationData{PermanentIdentifier: "mydevice123"})},
|
||||
want: &RequestBody{
|
||||
AttestationData: &AttestationData{
|
||||
PermanentIdentifier: "mydevice123",
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
"X509 Certificate Request": {
|
||||
options: []RequestBodyOption{
|
||||
WithX509CertificateRequest(&x509.CertificateRequest{
|
||||
PublicKeyAlgorithm: x509.ECDSA,
|
||||
Subject: pkix.Name{CommonName: "foo"},
|
||||
Raw: []byte("csr der"),
|
||||
}),
|
||||
},
|
||||
want: &RequestBody{
|
||||
X509CertificateRequest: &X509CertificateRequest{
|
||||
CertificateRequest: &x509util.CertificateRequest{
|
||||
PublicKeyAlgorithm: x509.ECDSA,
|
||||
Subject: x509util.Subject{CommonName: "foo"},
|
||||
},
|
||||
PublicKeyAlgorithm: "ECDSA",
|
||||
Raw: []byte("csr der"),
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
"X509 Certificate": {
|
||||
options: []RequestBodyOption{
|
||||
WithX509Certificate(&x509util.Certificate{}, &x509.Certificate{
|
||||
NotBefore: t1,
|
||||
NotAfter: t2,
|
||||
PublicKeyAlgorithm: x509.ECDSA,
|
||||
}),
|
||||
},
|
||||
want: &RequestBody{
|
||||
X509Certificate: &X509Certificate{
|
||||
Certificate: &x509util.Certificate{},
|
||||
PublicKeyAlgorithm: "ECDSA",
|
||||
NotBefore: t1,
|
||||
NotAfter: t2,
|
||||
},
|
||||
},
|
||||
},
|
||||
"SSH Certificate Request": {
|
||||
options: []RequestBodyOption{
|
||||
WithSSHCertificateRequest(sshutil.CertificateRequest{
|
||||
Type: "User",
|
||||
KeyID: "key1",
|
||||
Principals: []string{"areed", "other"},
|
||||
})},
|
||||
want: &RequestBody{
|
||||
SSHCertificateRequest: &SSHCertificateRequest{
|
||||
Type: "User",
|
||||
KeyID: "key1",
|
||||
Principals: []string{"areed", "other"},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
"SSH Certificate": {
|
||||
options: []RequestBodyOption{
|
||||
WithSSHCertificate(
|
||||
&sshutil.Certificate{},
|
||||
&ssh.Certificate{
|
||||
ValidAfter: uint64(t1.Unix()),
|
||||
ValidBefore: uint64(t2.Unix()),
|
||||
},
|
||||
),
|
||||
},
|
||||
want: &RequestBody{
|
||||
SSHCertificate: &SSHCertificate{
|
||||
Certificate: &sshutil.Certificate{},
|
||||
ValidAfter: uint64(t1.Unix()),
|
||||
ValidBefore: uint64(t2.Unix()),
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got, err := NewRequestBody(test.options...)
|
||||
if (err != nil) != test.wantErr {
|
||||
t.Fatalf("Got err %v, wanted %t", err, test.wantErr)
|
||||
}
|
||||
assert.Equals(t, test.want, got)
|
||||
})
|
||||
}
|
||||
}
|
71
webhook/types.go
Normal file
71
webhook/types.go
Normal file
|
@ -0,0 +1,71 @@
|
|||
package webhook
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"go.step.sm/crypto/sshutil"
|
||||
"go.step.sm/crypto/x509util"
|
||||
)
|
||||
|
||||
// ResponseBody is the body returned by webhook servers.
|
||||
type ResponseBody struct {
|
||||
Data any `json:"data"`
|
||||
Allow bool `json:"allow"`
|
||||
}
|
||||
|
||||
// X509CertificateRequest is the certificate request sent to webhook servers for
|
||||
// enriching webhooks when signing x509 certificates
|
||||
type X509CertificateRequest struct {
|
||||
*x509util.CertificateRequest
|
||||
PublicKey []byte `json:"publicKey"`
|
||||
PublicKeyAlgorithm string `json:"publicKeyAlgorithm"`
|
||||
Raw []byte `json:"raw"`
|
||||
}
|
||||
|
||||
// X509Certificate is the certificate sent to webhook servers for authorizing
|
||||
// webhooks when signing x509 certificates
|
||||
type X509Certificate struct {
|
||||
*x509util.Certificate
|
||||
PublicKey []byte `json:"publicKey"`
|
||||
PublicKeyAlgorithm string `json:"publicKeyAlgorithm"`
|
||||
NotBefore time.Time `json:"notBefore"`
|
||||
NotAfter time.Time `json:"notAfter"`
|
||||
}
|
||||
|
||||
// SSHCertificateRequest is the certificate request sent to webhook servers for
|
||||
// enriching webhooks when signing SSH certificates
|
||||
type SSHCertificateRequest struct {
|
||||
PublicKey []byte `json:"publicKey"`
|
||||
Type string `json:"type"`
|
||||
KeyID string `json:"keyID"`
|
||||
Principals []string `json:"principals"`
|
||||
}
|
||||
|
||||
// SSHCertificate is the certificate sent to webhook servers for authorizing
|
||||
// webhooks when signing SSH certificates
|
||||
type SSHCertificate struct {
|
||||
*sshutil.Certificate
|
||||
PublicKey []byte `json:"publicKey"`
|
||||
SignatureKey []byte `json:"signatureKey"`
|
||||
ValidBefore uint64 `json:"validBefore"`
|
||||
ValidAfter uint64 `json:"validAfter"`
|
||||
}
|
||||
|
||||
// AttestationData is data validated by acme device-attest-01 challenge
|
||||
type AttestationData struct {
|
||||
PermanentIdentifier string `json:"permanentIdentifier"`
|
||||
}
|
||||
|
||||
// RequestBody is the body sent to webhook servers.
|
||||
type RequestBody struct {
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
// Only set after successfully completing acme device-attest-01 challenge
|
||||
AttestationData *AttestationData `json:"attestationData,omitempty"`
|
||||
// Set for most provisioners, but not acme or scep
|
||||
// Token any `json:"token,omitempty"`
|
||||
// Exactly one of the remaining fields should be set
|
||||
X509CertificateRequest *X509CertificateRequest `json:"x509CertificateRequest,omitempty"`
|
||||
X509Certificate *X509Certificate `json:"x509Certificate,omitempty"`
|
||||
SSHCertificateRequest *SSHCertificateRequest `json:"sshCertificateRequest,omitempty"`
|
||||
SSHCertificate *SSHCertificate `json:"sshCertificate,omitempty"`
|
||||
}
|
Loading…
Reference in a new issue