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