2022-09-30 00:16:26 +00:00
|
|
|
package provisioner
|
|
|
|
|
|
|
|
import (
|
2023-09-19 13:39:54 +00:00
|
|
|
"context"
|
2022-09-30 00:16:26 +00:00
|
|
|
"crypto/hmac"
|
|
|
|
"crypto/sha256"
|
|
|
|
"crypto/tls"
|
2023-07-20 20:03:45 +00:00
|
|
|
"crypto/x509"
|
2022-09-30 00:16:26 +00:00
|
|
|
"encoding/base64"
|
|
|
|
"encoding/hex"
|
|
|
|
"encoding/json"
|
2024-02-27 12:44:44 +00:00
|
|
|
"errors"
|
2022-09-30 00:16:26 +00:00
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"net/http"
|
|
|
|
"net/http/httptest"
|
|
|
|
"testing"
|
2023-09-19 13:39:54 +00:00
|
|
|
"time"
|
2022-09-30 00:16:26 +00:00
|
|
|
|
2024-02-27 12:39:21 +00:00
|
|
|
"github.com/stretchr/testify/assert"
|
|
|
|
"github.com/stretchr/testify/require"
|
2024-03-04 11:00:08 +00:00
|
|
|
|
2023-07-20 20:03:45 +00:00
|
|
|
"go.step.sm/crypto/pemutil"
|
2022-09-30 00:16:26 +00:00
|
|
|
"go.step.sm/crypto/x509util"
|
|
|
|
"go.step.sm/linkedca"
|
2024-03-04 11:00:08 +00:00
|
|
|
|
2024-03-07 09:41:19 +00:00
|
|
|
"github.com/smallstep/certificates/middleware/requestid"
|
2024-03-04 11:00:08 +00:00
|
|
|
"github.com/smallstep/certificates/webhook"
|
2022-09-30 00:16:26 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
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) {
|
2024-02-27 12:44:44 +00:00
|
|
|
assert.Equal(t, test.want, test.wc.isCertTypeOK(test.wh))
|
2022-09-30 00:16:26 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-28 12:18:10 +00:00
|
|
|
// withRequestID is a helper that calls into [requestid.NewContext] and returns
|
|
|
|
// a new context with the requestID added.
|
2024-03-04 11:00:08 +00:00
|
|
|
func withRequestID(t *testing.T, ctx context.Context, requestID string) context.Context {
|
|
|
|
t.Helper()
|
2024-02-28 12:18:10 +00:00
|
|
|
return requestid.NewContext(ctx, requestID)
|
2024-02-27 12:39:21 +00:00
|
|
|
}
|
|
|
|
|
2022-09-30 00:16:26 +00:00
|
|
|
func TestWebhookController_Enrich(t *testing.T) {
|
2023-07-20 20:03:45 +00:00
|
|
|
cert, err := pemutil.ReadCertificate("testdata/certs/x5c-leaf.crt", pemutil.WithFirstBlock())
|
2024-02-27 12:39:21 +00:00
|
|
|
require.NoError(t, err)
|
2023-07-20 20:03:45 +00:00
|
|
|
|
2022-09-30 00:16:26 +00:00
|
|
|
type test struct {
|
|
|
|
ctl *WebhookController
|
2024-02-27 12:39:21 +00:00
|
|
|
ctx context.Context
|
2022-09-30 00:16:26 +00:00
|
|
|
req *webhook.RequestBody
|
|
|
|
responses []*webhook.ResponseBody
|
|
|
|
expectErr bool
|
|
|
|
expectTemplateData any
|
2023-07-20 20:03:45 +00:00
|
|
|
assertRequest func(t *testing.T, req *webhook.RequestBody)
|
2022-09-30 00:16:26 +00:00
|
|
|
}
|
|
|
|
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{},
|
|
|
|
},
|
2024-03-04 11:00:08 +00:00
|
|
|
ctx: withRequestID(t, context.Background(), "reqID"),
|
2022-09-30 00:16:26 +00:00
|
|
|
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{},
|
|
|
|
},
|
2024-03-04 11:00:08 +00:00
|
|
|
ctx: withRequestID(t, context.Background(), "reqID"),
|
2022-09-30 00:16:26 +00:00
|
|
|
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,
|
|
|
|
},
|
2024-03-04 11:00:08 +00:00
|
|
|
ctx: withRequestID(t, context.Background(), "reqID"),
|
2022-09-30 00:16:26 +00:00
|
|
|
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"},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
2023-07-20 20:03:45 +00:00
|
|
|
"ok/with options": {
|
|
|
|
ctl: &WebhookController{
|
|
|
|
client: http.DefaultClient,
|
|
|
|
webhooks: []*Webhook{{Name: "people", Kind: "ENRICHING"}},
|
|
|
|
TemplateData: x509util.TemplateData{},
|
|
|
|
options: []webhook.RequestBodyOption{webhook.WithX5CCertificate(cert)},
|
|
|
|
},
|
2024-03-04 11:00:08 +00:00
|
|
|
ctx: withRequestID(t, context.Background(), "reqID"),
|
2023-07-20 20:03:45 +00:00
|
|
|
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"}}},
|
|
|
|
assertRequest: func(t *testing.T, req *webhook.RequestBody) {
|
|
|
|
key, err := x509.MarshalPKIXPublicKey(cert.PublicKey)
|
2024-02-27 12:44:44 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, &webhook.X5CCertificate{
|
2023-07-20 20:03:45 +00:00
|
|
|
Raw: cert.Raw,
|
|
|
|
PublicKey: key,
|
|
|
|
PublicKeyAlgorithm: cert.PublicKeyAlgorithm.String(),
|
|
|
|
NotBefore: cert.NotBefore,
|
|
|
|
NotAfter: cert.NotAfter,
|
|
|
|
}, req.X5CCertificate)
|
|
|
|
},
|
|
|
|
},
|
2022-09-30 00:16:26 +00:00
|
|
|
"deny": {
|
|
|
|
ctl: &WebhookController{
|
|
|
|
client: http.DefaultClient,
|
|
|
|
webhooks: []*Webhook{{Name: "people", Kind: "ENRICHING"}},
|
|
|
|
TemplateData: x509util.TemplateData{},
|
|
|
|
},
|
2024-03-04 11:00:08 +00:00
|
|
|
ctx: withRequestID(t, context.Background(), "reqID"),
|
2022-09-30 00:16:26 +00:00
|
|
|
req: &webhook.RequestBody{},
|
|
|
|
responses: []*webhook.ResponseBody{{Allow: false}},
|
|
|
|
expectErr: true,
|
|
|
|
expectTemplateData: x509util.TemplateData{},
|
|
|
|
},
|
2023-07-20 20:03:45 +00:00
|
|
|
"fail/with options": {
|
|
|
|
ctl: &WebhookController{
|
|
|
|
client: http.DefaultClient,
|
|
|
|
webhooks: []*Webhook{{Name: "people", Kind: "ENRICHING"}},
|
|
|
|
TemplateData: x509util.TemplateData{},
|
|
|
|
options: []webhook.RequestBodyOption{webhook.WithX5CCertificate(&x509.Certificate{
|
|
|
|
PublicKey: []byte("bad"),
|
|
|
|
})},
|
|
|
|
},
|
2024-03-04 11:00:08 +00:00
|
|
|
ctx: withRequestID(t, context.Background(), "reqID"),
|
2023-07-20 20:03:45 +00:00
|
|
|
req: &webhook.RequestBody{},
|
|
|
|
responses: []*webhook.ResponseBody{{Allow: false}},
|
|
|
|
expectErr: true,
|
|
|
|
expectTemplateData: x509util.TemplateData{},
|
|
|
|
},
|
2022-09-30 00:16:26 +00:00
|
|
|
}
|
|
|
|
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) {
|
2024-02-27 12:39:21 +00:00
|
|
|
assert.Equal(t, "reqID", r.Header.Get("X-Request-ID"))
|
|
|
|
|
2022-09-30 00:16:26 +00:00
|
|
|
err := json.NewEncoder(w).Encode(test.responses[j])
|
2024-02-27 12:39:21 +00:00
|
|
|
require.NoError(t, err)
|
2022-09-30 00:16:26 +00:00
|
|
|
}))
|
|
|
|
// nolint: gocritic // defer in loop isn't a memory leak
|
|
|
|
defer ts.Close()
|
|
|
|
wh.URL = ts.URL
|
|
|
|
}
|
|
|
|
|
2024-02-27 12:39:21 +00:00
|
|
|
err := test.ctl.Enrich(test.ctx, test.req)
|
2022-09-30 00:16:26 +00:00
|
|
|
if (err != nil) != test.expectErr {
|
|
|
|
t.Fatalf("Got err %v, want %v", err, test.expectErr)
|
|
|
|
}
|
2024-02-27 12:44:44 +00:00
|
|
|
assert.Equal(t, test.expectTemplateData, test.ctl.TemplateData)
|
2023-07-20 20:03:45 +00:00
|
|
|
if test.assertRequest != nil {
|
|
|
|
test.assertRequest(t, test.req)
|
|
|
|
}
|
2022-09-30 00:16:26 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestWebhookController_Authorize(t *testing.T) {
|
2023-07-20 20:03:45 +00:00
|
|
|
cert, err := pemutil.ReadCertificate("testdata/certs/x5c-leaf.crt", pemutil.WithFirstBlock())
|
2024-02-27 12:39:21 +00:00
|
|
|
require.NoError(t, err)
|
2023-07-20 20:03:45 +00:00
|
|
|
|
2022-09-30 00:16:26 +00:00
|
|
|
type test struct {
|
2023-07-20 20:03:45 +00:00
|
|
|
ctl *WebhookController
|
2024-02-27 12:39:21 +00:00
|
|
|
ctx context.Context
|
2023-07-20 20:03:45 +00:00
|
|
|
req *webhook.RequestBody
|
|
|
|
responses []*webhook.ResponseBody
|
|
|
|
expectErr bool
|
|
|
|
assertRequest func(t *testing.T, req *webhook.RequestBody)
|
2022-09-30 00:16:26 +00:00
|
|
|
}
|
|
|
|
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"}},
|
|
|
|
},
|
2024-03-04 11:00:08 +00:00
|
|
|
ctx: withRequestID(t, context.Background(), "reqID"),
|
2022-09-30 00:16:26 +00:00
|
|
|
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,
|
|
|
|
},
|
2024-03-04 11:00:08 +00:00
|
|
|
ctx: withRequestID(t, context.Background(), "reqID"),
|
2022-09-30 00:16:26 +00:00
|
|
|
req: &webhook.RequestBody{},
|
|
|
|
responses: []*webhook.ResponseBody{{Allow: false}},
|
|
|
|
expectErr: false,
|
|
|
|
},
|
2023-07-20 20:03:45 +00:00
|
|
|
"ok/with options": {
|
|
|
|
ctl: &WebhookController{
|
|
|
|
client: http.DefaultClient,
|
|
|
|
webhooks: []*Webhook{{Name: "people", Kind: "AUTHORIZING"}},
|
|
|
|
options: []webhook.RequestBodyOption{webhook.WithX5CCertificate(cert)},
|
|
|
|
},
|
2024-03-04 11:00:08 +00:00
|
|
|
ctx: withRequestID(t, context.Background(), "reqID"),
|
2023-07-20 20:03:45 +00:00
|
|
|
req: &webhook.RequestBody{},
|
|
|
|
responses: []*webhook.ResponseBody{{Allow: true}},
|
|
|
|
expectErr: false,
|
|
|
|
assertRequest: func(t *testing.T, req *webhook.RequestBody) {
|
|
|
|
key, err := x509.MarshalPKIXPublicKey(cert.PublicKey)
|
2024-02-27 12:39:21 +00:00
|
|
|
require.NoError(t, err)
|
2024-02-27 12:44:44 +00:00
|
|
|
assert.Equal(t, &webhook.X5CCertificate{
|
2023-07-20 20:03:45 +00:00
|
|
|
Raw: cert.Raw,
|
|
|
|
PublicKey: key,
|
|
|
|
PublicKeyAlgorithm: cert.PublicKeyAlgorithm.String(),
|
|
|
|
NotBefore: cert.NotBefore,
|
|
|
|
NotAfter: cert.NotAfter,
|
|
|
|
}, req.X5CCertificate)
|
|
|
|
},
|
|
|
|
},
|
2022-09-30 00:16:26 +00:00
|
|
|
"deny": {
|
|
|
|
ctl: &WebhookController{
|
|
|
|
client: http.DefaultClient,
|
|
|
|
webhooks: []*Webhook{{Name: "people", Kind: "AUTHORIZING"}},
|
|
|
|
},
|
2024-03-04 11:00:08 +00:00
|
|
|
ctx: withRequestID(t, context.Background(), "reqID"),
|
2022-09-30 00:16:26 +00:00
|
|
|
req: &webhook.RequestBody{},
|
|
|
|
responses: []*webhook.ResponseBody{{Allow: false}},
|
|
|
|
expectErr: true,
|
|
|
|
},
|
2023-07-20 20:03:45 +00:00
|
|
|
"fail/with options": {
|
|
|
|
ctl: &WebhookController{
|
|
|
|
client: http.DefaultClient,
|
|
|
|
webhooks: []*Webhook{{Name: "people", Kind: "AUTHORIZING"}},
|
|
|
|
options: []webhook.RequestBodyOption{webhook.WithX5CCertificate(&x509.Certificate{
|
|
|
|
PublicKey: []byte("bad"),
|
|
|
|
})},
|
|
|
|
},
|
2024-03-04 11:00:08 +00:00
|
|
|
ctx: withRequestID(t, context.Background(), "reqID"),
|
2023-07-20 20:03:45 +00:00
|
|
|
req: &webhook.RequestBody{},
|
|
|
|
responses: []*webhook.ResponseBody{{Allow: false}},
|
|
|
|
expectErr: true,
|
|
|
|
},
|
2022-09-30 00:16:26 +00:00
|
|
|
}
|
|
|
|
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) {
|
2024-02-27 12:39:21 +00:00
|
|
|
assert.Equal(t, "reqID", r.Header.Get("X-Request-ID"))
|
|
|
|
|
2022-09-30 00:16:26 +00:00
|
|
|
err := json.NewEncoder(w).Encode(test.responses[j])
|
2024-02-27 12:39:21 +00:00
|
|
|
require.NoError(t, err)
|
2022-09-30 00:16:26 +00:00
|
|
|
}))
|
|
|
|
// nolint: gocritic // defer in loop isn't a memory leak
|
|
|
|
defer ts.Close()
|
|
|
|
wh.URL = ts.URL
|
|
|
|
}
|
|
|
|
|
2024-02-27 12:39:21 +00:00
|
|
|
err := test.ctl.Authorize(test.ctx, test.req)
|
2022-09-30 00:16:26 +00:00
|
|
|
if (err != nil) != test.expectErr {
|
|
|
|
t.Fatalf("Got err %v, want %v", err, test.expectErr)
|
|
|
|
}
|
2023-07-20 20:03:45 +00:00
|
|
|
if test.assertRequest != nil {
|
|
|
|
test.assertRequest(t, test.req)
|
|
|
|
}
|
2022-09-30 00:16:26 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestWebhook_Do(t *testing.T) {
|
|
|
|
csr := parseCertificateRequest(t, "testdata/certs/ecdsa.csr")
|
|
|
|
type test struct {
|
|
|
|
webhook Webhook
|
|
|
|
dataArg any
|
2024-02-27 12:39:21 +00:00
|
|
|
requestID string
|
2022-09-30 00:16:26 +00:00
|
|
|
webhookResponse webhook.ResponseBody
|
|
|
|
expectPath string
|
|
|
|
errStatusCode int
|
|
|
|
serverErrMsg string
|
|
|
|
expectErr error
|
|
|
|
// expectToken any
|
|
|
|
}
|
|
|
|
tests := map[string]test{
|
|
|
|
"ok": {
|
2024-02-27 12:39:21 +00:00
|
|
|
webhook: Webhook{
|
|
|
|
ID: "abc123",
|
|
|
|
Secret: "c2VjcmV0Cg==",
|
|
|
|
},
|
|
|
|
requestID: "reqID",
|
|
|
|
webhookResponse: webhook.ResponseBody{
|
|
|
|
Data: map[string]interface{}{"role": "dba"},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
"ok/no-request-id": {
|
2022-09-30 00:16:26 +00:00
|
|
|
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",
|
|
|
|
},
|
2024-02-27 12:39:21 +00:00
|
|
|
requestID: "reqID",
|
2022-09-30 00:16:26 +00:00
|
|
|
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",
|
|
|
|
},
|
|
|
|
},
|
2024-02-27 12:39:21 +00:00
|
|
|
requestID: "reqID",
|
2022-09-30 00:16:26 +00:00
|
|
|
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==",
|
|
|
|
},
|
2024-02-27 12:39:21 +00:00
|
|
|
requestID: "reqID",
|
|
|
|
dataArg: map[string]interface{}{"username": "areed", "region": "central"},
|
2022-09-30 00:16:26 +00:00
|
|
|
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==",
|
|
|
|
},
|
2024-02-27 12:39:21 +00:00
|
|
|
requestID: "reqID",
|
2022-09-30 00:16:26 +00:00
|
|
|
webhookResponse: webhook.ResponseBody{
|
|
|
|
Allow: true,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
"fail/404": {
|
|
|
|
webhook: Webhook{
|
|
|
|
ID: "abc123",
|
|
|
|
Secret: "c2VjcmV0Cg==",
|
|
|
|
},
|
|
|
|
webhookResponse: webhook.ResponseBody{
|
|
|
|
Data: map[string]interface{}{"role": "dba"},
|
|
|
|
},
|
2024-02-27 12:39:21 +00:00
|
|
|
requestID: "reqID",
|
2022-09-30 00:16:26 +00:00
|
|
|
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) {
|
2024-02-27 12:39:21 +00:00
|
|
|
if tc.requestID != "" {
|
|
|
|
assert.Equal(t, tc.requestID, r.Header.Get("X-Request-ID"))
|
|
|
|
}
|
|
|
|
|
2024-02-27 12:44:44 +00:00
|
|
|
assert.Equal(t, tc.webhook.ID, r.Header.Get("X-Smallstep-Webhook-ID"))
|
2022-09-30 00:16:26 +00:00
|
|
|
|
|
|
|
sig, err := hex.DecodeString(r.Header.Get("X-Smallstep-Signature"))
|
2024-02-27 12:39:21 +00:00
|
|
|
assert.NoError(t, err)
|
2022-09-30 00:16:26 +00:00
|
|
|
|
|
|
|
body, err := io.ReadAll(r.Body)
|
2024-02-27 12:39:21 +00:00
|
|
|
assert.NoError(t, err)
|
2022-09-30 00:16:26 +00:00
|
|
|
|
|
|
|
secret, err := base64.StdEncoding.DecodeString(tc.webhook.Secret)
|
2024-02-27 12:39:21 +00:00
|
|
|
assert.NoError(t, err)
|
2023-09-22 20:21:00 +00:00
|
|
|
h := hmac.New(sha256.New, secret)
|
|
|
|
h.Write(body)
|
|
|
|
mac := h.Sum(nil)
|
2024-02-27 12:44:44 +00:00
|
|
|
assert.True(t, hmac.Equal(sig, mac))
|
2022-09-30 00:16:26 +00:00
|
|
|
|
|
|
|
switch {
|
|
|
|
case tc.webhook.BearerToken != "":
|
|
|
|
ah := fmt.Sprintf("Bearer %s", tc.webhook.BearerToken)
|
2024-02-27 12:44:44 +00:00
|
|
|
assert.Equal(t, ah, r.Header.Get("Authorization"))
|
2022-09-30 00:16:26 +00:00
|
|
|
case tc.webhook.BasicAuth.Username != "" || tc.webhook.BasicAuth.Password != "":
|
|
|
|
whReq, err := http.NewRequest("", "", http.NoBody)
|
2024-02-27 12:44:44 +00:00
|
|
|
require.NoError(t, err)
|
2022-09-30 00:16:26 +00:00
|
|
|
whReq.SetBasicAuth(tc.webhook.BasicAuth.Username, tc.webhook.BasicAuth.Password)
|
|
|
|
ah := whReq.Header.Get("Authorization")
|
2024-02-27 12:44:44 +00:00
|
|
|
assert.Equal(t, ah, whReq.Header.Get("Authorization"))
|
2022-09-30 00:16:26 +00:00
|
|
|
default:
|
2024-02-27 12:44:44 +00:00
|
|
|
assert.Equal(t, "", r.Header.Get("Authorization"))
|
2022-09-30 00:16:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if tc.expectPath != "" {
|
2024-02-27 12:44:44 +00:00
|
|
|
assert.Equal(t, tc.expectPath, r.URL.Path+"?"+r.URL.RawQuery)
|
2022-09-30 00:16:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if tc.errStatusCode != 0 {
|
|
|
|
http.Error(w, tc.serverErrMsg, tc.errStatusCode)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
reqBody := new(webhook.RequestBody)
|
|
|
|
err = json.Unmarshal(body, reqBody)
|
2024-02-27 12:39:21 +00:00
|
|
|
require.NoError(t, err)
|
2022-09-30 00:16:26 +00:00
|
|
|
|
|
|
|
err = json.NewEncoder(w).Encode(tc.webhookResponse)
|
2024-02-27 12:39:21 +00:00
|
|
|
require.NoError(t, err)
|
2022-09-30 00:16:26 +00:00
|
|
|
}))
|
|
|
|
defer ts.Close()
|
|
|
|
|
|
|
|
tc.webhook.URL = ts.URL + tc.webhook.URL
|
|
|
|
|
|
|
|
reqBody, err := webhook.NewRequestBody(webhook.WithX509CertificateRequest(csr))
|
2024-02-27 12:39:21 +00:00
|
|
|
require.NoError(t, err)
|
2023-09-19 13:39:54 +00:00
|
|
|
|
2024-02-27 12:39:21 +00:00
|
|
|
ctx := context.Background()
|
|
|
|
if tc.requestID != "" {
|
2024-03-04 11:00:08 +00:00
|
|
|
ctx = withRequestID(t, ctx, tc.requestID)
|
2024-02-27 12:39:21 +00:00
|
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(ctx, time.Second*10)
|
2023-09-19 13:39:54 +00:00
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
got, err := tc.webhook.DoWithContext(ctx, http.DefaultClient, reqBody, tc.dataArg)
|
2022-09-30 00:16:26 +00:00
|
|
|
if tc.expectErr != nil {
|
2024-02-27 12:44:44 +00:00
|
|
|
assert.Equal(t, tc.expectErr.Error(), err.Error())
|
2022-09-30 00:16:26 +00:00
|
|
|
return
|
|
|
|
}
|
2024-02-27 12:39:21 +00:00
|
|
|
assert.NoError(t, err)
|
2022-09-30 00:16:26 +00:00
|
|
|
|
2024-02-27 12:44:44 +00:00
|
|
|
assert.Equal(t, &tc.webhookResponse, got)
|
2022-09-30 00:16:26 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
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")
|
2024-02-27 12:39:21 +00:00
|
|
|
require.NoError(t, err)
|
2022-09-30 00:16:26 +00:00
|
|
|
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))
|
2024-02-27 12:39:21 +00:00
|
|
|
require.NoError(t, err)
|
2023-09-19 13:39:54 +00:00
|
|
|
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
_, err = wh.DoWithContext(ctx, client, reqBody, nil)
|
2024-02-27 12:39:21 +00:00
|
|
|
require.NoError(t, err)
|
2022-09-30 00:16:26 +00:00
|
|
|
|
2023-09-19 13:39:54 +00:00
|
|
|
ctx, cancel = context.WithTimeout(context.Background(), time.Second*10)
|
|
|
|
defer cancel()
|
|
|
|
|
2022-09-30 00:16:26 +00:00
|
|
|
wh.DisableTLSClientAuth = true
|
2023-09-19 13:39:54 +00:00
|
|
|
_, err = wh.DoWithContext(ctx, client, reqBody, nil)
|
2024-02-27 12:39:21 +00:00
|
|
|
require.Error(t, err)
|
2022-09-30 00:16:26 +00:00
|
|
|
})
|
|
|
|
}
|