diff --git a/api/v1beta1/receiver_types.go b/api/v1beta1/receiver_types.go index 75566ce47..a19c5b214 100644 --- a/api/v1beta1/receiver_types.go +++ b/api/v1beta1/receiver_types.go @@ -28,6 +28,7 @@ type ReceiverSpec struct { // Type of webhook sender, used to determine // the validation procedure and payload deserialization. // +kubebuilder:validation:Enum=generic;github;gitlab;bitbucket;harbor;dockerhub;quay;gcr + // +kubebuilder:validation:Enum=generic;github;gitlab;bitbucket;harbor;dockerhub;quay;gcr;nexus // +required Type string `json:"type"` @@ -71,6 +72,7 @@ const ( DockerHubReceiver string = "dockerhub" QuayReceiver string = "quay" GCRReceiver string = "gcr" + NexusReceiver string = "nexus" ) func ReceiverReady(receiver Receiver, reason, message, url string) Receiver { diff --git a/config/crd/bases/notification.toolkit.fluxcd.io_receivers.yaml b/config/crd/bases/notification.toolkit.fluxcd.io_receivers.yaml index 97eb09e7a..9d9b17900 100644 --- a/config/crd/bases/notification.toolkit.fluxcd.io_receivers.yaml +++ b/config/crd/bases/notification.toolkit.fluxcd.io_receivers.yaml @@ -111,6 +111,7 @@ spec: - dockerhub - quay - gcr + - nexus type: string required: - resources diff --git a/docs/spec/v1beta1/receiver.md b/docs/spec/v1beta1/receiver.md index 25858213c..8eaaabfa2 100644 --- a/docs/spec/v1beta1/receiver.md +++ b/docs/spec/v1beta1/receiver.md @@ -46,6 +46,7 @@ const ( DockerHubReceiver string = "dockerhub" QuayReceiver string = "quay" GCRReceiver string = "gcr" + NexusReceiver string = "nexus" ) ``` @@ -229,6 +230,27 @@ spec: name: webapp ``` +### Nexus receiver + +```yaml +apiVersion: notification.toolkit.fluxcd.io/v1beta1 +kind: Receiver +metadata: + name: nexus-receiver + namespace: default +spec: + type: nexus + secretRef: + name: webhook-token + resources: + - kind: ImageRepository + name: webapp +``` + +Note that you have to fill in the generated token as the secret key when creating the Nexus Webhook Capability. +See [Nexus Webhook Capability](https://help.sonatype.com/repomanager3/webhooks/enabling-a-repository-webhook-capability) +The controller uses the `X-Nexus-Webhook-Signature` HTTP header to verify that the request is legitimate. + ### GCR receiver ```yaml diff --git a/internal/server/receiver_handlers.go b/internal/server/receiver_handlers.go index 2f5c90f63..39a429c76 100644 --- a/internal/server/receiver_handlers.go +++ b/internal/server/receiver_handlers.go @@ -18,9 +18,13 @@ package server import ( "context" + "crypto/hmac" + "crypto/sha1" "encoding/base64" + "encoding/hex" "encoding/json" "fmt" + "io/ioutil" "net/http" "net/url" "strings" @@ -265,6 +269,34 @@ func (s *ReceiverServer) validate(ctx context.Context, receiver v1beta1.Receiver fmt.Sprintf("handling event from %s for tag %s", d.Digest, d.Tag), "receiver", receiver.Name) return nil + case v1beta1.NexusReceiver: + signature := r.Header.Get("X-Nexus-Webhook-Signature") + if len(signature) == 0 { + return fmt.Errorf("Signature is missing from header") + } + + b, err := ioutil.ReadAll(r.Body) + if err != nil { + return fmt.Errorf("cannot read payload. error: %s", err) + } + + if !verifyHmacSignature([]byte(token), signature, b) { + return fmt.Errorf("invalid nexus signature") + } + type payload struct { + Action string `json:"action"` + RepositoryName string `json:"repositoryName"` + } + var p payload + + if err := json.Unmarshal(b, &p); err != nil { + return fmt.Errorf("cannot decode Nexus webhook payload: %s", err) + } + + s.logger.Info( + fmt.Sprintf("handling event from %s", p.RepositoryName), + "receiver", receiver.Name) + return nil } return fmt.Errorf("recevier type '%s' not supported", receiver.Spec.Type) @@ -382,3 +414,10 @@ func authenticateGCRRequest(c *http.Client, bearer string, tokenIndex int) (err return nil } + +func verifyHmacSignature(key []byte, signature string, payload []byte) bool { + mac := hmac.New(sha1.New, key) + _, _ = mac.Write(payload) + expectedMAC := hex.EncodeToString(mac.Sum(nil)) + return hmac.Equal([]byte(signature), []byte(expectedMAC)) +}