forked from TrueCloudLab/certificates
669 lines
21 KiB
Go
669 lines
21 KiB
Go
|
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)
|
||
|
})
|
||
|
}
|
||
|
}
|