Provisioner webhooks (#1001)
parent
6fe0fc852a
commit
7101fbb0ee
@ -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)
|
||||
}
|
@ -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-----
|
@ -0,0 +1,5 @@
|
||||
-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEIJmnxm3N/ahRA2PWeZhRGJUKPU1lI44WcE4P1bynIim6oAoGCCqGSM49
|
||||
AwEHoUQDQgAEG6bXw6JxWnlD4k6+dsJfa93XM4K3qJcA0ZrIMhYJ69RLih+elkXI
|
||||
B9YI3y9KcmxZTtz3YAbpEeLvrL3qt+NzgA==
|
||||
-----END EC PRIVATE KEY-----
|
@ -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
|
||||
}
|
@ -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)
|
||||
})
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
package authority
|
||||
|
||||
import "github.com/smallstep/certificates/webhook"
|
||||
|
||||
type webhookController interface {
|
||||
Enrich(*webhook.RequestBody) error
|
||||
Authorize(*webhook.RequestBody) error
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
@ -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 New Issue