Integrate the SCEP webhook with the existing webhook logic
parent
05f7ab979f
commit
27cdcaf5ee
@ -1,24 +0,0 @@
|
||||
package webhook
|
||||
|
||||
type ControllerOption func(*Controller) error
|
||||
|
||||
func WithURL(url string) ControllerOption {
|
||||
return func(c *Controller) error {
|
||||
c.webhook.URL = url
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithBearerToken(token string) ControllerOption {
|
||||
return func(c *Controller) error {
|
||||
c.webhook.BearerToken = token
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithDisableTLSClientAuth(enabled bool) ControllerOption {
|
||||
return func(c *Controller) error {
|
||||
c.webhook.DisableTLSClientAuth = enabled
|
||||
return nil
|
||||
}
|
||||
}
|
@ -1,161 +1,51 @@
|
||||
package webhook
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/ryboe/q"
|
||||
"go.step.sm/linkedca"
|
||||
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
"github.com/smallstep/certificates/webhook"
|
||||
)
|
||||
|
||||
type Controller struct {
|
||||
client *http.Client
|
||||
webhook *Webhook
|
||||
}
|
||||
|
||||
func New(options ...ControllerOption) (*Controller, error) {
|
||||
c := &Controller{
|
||||
client: http.DefaultClient,
|
||||
webhook: &Webhook{},
|
||||
}
|
||||
for _, apply := range options {
|
||||
if err := apply(c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *Controller) Validate(challenge string) (bool, error) {
|
||||
req := &Request{
|
||||
Challenge: challenge,
|
||||
}
|
||||
client := c.client
|
||||
if client == nil {
|
||||
client = http.DefaultClient
|
||||
}
|
||||
resp, err := c.webhook.Do(client, req)
|
||||
if err != nil {
|
||||
q.Q(err)
|
||||
return false, fmt.Errorf("failed performing webhook request: %w", err)
|
||||
}
|
||||
|
||||
if resp == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
client *http.Client
|
||||
webhooks []*provisioner.Webhook
|
||||
}
|
||||
|
||||
type Webhook struct {
|
||||
URL string
|
||||
DisableTLSClientAuth bool
|
||||
Secret string
|
||||
BearerToken string
|
||||
BasicAuth struct {
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
func New(webhooks []*provisioner.Webhook) (*Controller, error) {
|
||||
return &Controller{
|
||||
client: http.DefaultClient,
|
||||
webhooks: webhooks,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (w *Webhook) Do(client *http.Client, req *Request) (*Response, error) {
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||
defer cancel()
|
||||
|
||||
reqBytes, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
func (c *Controller) Validate(challenge string) error {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
retries := 1
|
||||
retry:
|
||||
|
||||
r, err := http.NewRequestWithContext(ctx, "POST", w.URL, bytes.NewReader(reqBytes))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if w.Secret != "" {
|
||||
secret, err := base64.StdEncoding.DecodeString(w.Secret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
for _, wh := range c.webhooks {
|
||||
if wh.Kind != linkedca.Webhook_SCEPCHALLENGE.String() {
|
||||
continue
|
||||
}
|
||||
sig := hmac.New(sha256.New, secret).Sum(reqBytes)
|
||||
r.Header.Set("X-Smallstep-Signature", hex.EncodeToString(sig))
|
||||
//req.Header.Set("X-Smallstep-Webhook-ID", w.ID)
|
||||
}
|
||||
|
||||
if w.BearerToken != "" {
|
||||
r.Header.Set("Authorization", fmt.Sprintf("Bearer %s", w.BearerToken))
|
||||
} else if w.BasicAuth.Username != "" || w.BasicAuth.Password != "" {
|
||||
r.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")
|
||||
if !c.isCertTypeOK(wh) {
|
||||
continue
|
||||
}
|
||||
transport = transport.Clone()
|
||||
tlsConfig := transport.TLSClientConfig.Clone()
|
||||
tlsConfig.GetClientCertificate = nil
|
||||
tlsConfig.Certificates = nil
|
||||
transport.TLSClientConfig = tlsConfig
|
||||
client = &http.Client{
|
||||
Transport: transport,
|
||||
req := &webhook.RequestBody{
|
||||
SCEPChallenge: challenge,
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := client.Do(r)
|
||||
if err != nil {
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
return nil, err
|
||||
} else if retries > 0 {
|
||||
retries--
|
||||
time.Sleep(time.Second)
|
||||
goto retry
|
||||
resp, err := wh.Do(c.client, req, nil) // TODO(hs): support templated URL?
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
if err := resp.Body.Close(); err != nil {
|
||||
// TODO: return this error instead of (just) logging?
|
||||
log.Printf("failed to close body of response from %s", w.URL)
|
||||
if !resp.Allow {
|
||||
return provisioner.ErrWebhookDenied
|
||||
}
|
||||
}()
|
||||
|
||||
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 := &Response{}
|
||||
// TODO: decide on the JSON structure for the response (if any); HTTP status code may be enough.
|
||||
// if err := json.NewDecoder(resp.Body).Decode(respBody); err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
return respBody, nil
|
||||
}
|
||||
|
||||
type Request struct {
|
||||
Challenge string `json:"challenge"`
|
||||
return nil
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
// TODO: define expected response format? Or do we consider 200 OK enough?
|
||||
Allow bool `json:"allow"`
|
||||
func (c *Controller) isCertTypeOK(wh *provisioner.Webhook) bool {
|
||||
return linkedca.Webhook_X509.String() == wh.CertType
|
||||
}
|
||||
|
Loading…
Reference in New Issue