Skip to content

Commit

Permalink
github-events: Add GITHUB_ORGANIZATIONS_FILTER
Browse files Browse the repository at this point in the history
Adds filter for only listening to events from specific orgs. In order to
install an App on multiple orgs (e.g. chainguard-dev, wolfi-os), we need to make it public.
This restricts the trampoline to only listen and forward to specific
orgs. If omitted, all events are forwarded.
  • Loading branch information
wlynch committed Jan 13, 2025
1 parent 33b4f50 commit 1f314e4
Show file tree
Hide file tree
Showing 5 changed files with 113 additions and 21 deletions.
8 changes: 7 additions & 1 deletion modules/github-events/cmd/trampoline/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ var env = envconfig.MustProcess(context.Background(), &struct {
RequestedOnly []string `env:"REQUESTED_ONLY_WEBHOOK_ID"`
// If set, only events from the specified webhook IDs will be processed.
WebhookID []string `env:"WEBHOOK_ID"`
OrgFilter []string `env:"GITHUB_ORGANIZATIONS_FILTER"`
}{})

func main() {
Expand All @@ -58,7 +59,12 @@ func main() {
srv := &http.Server{
Addr: fmt.Sprintf(":%d", env.Port),
ReadHeaderTimeout: 10 * time.Second,
Handler: httpmetrics.Handler("trampoline", trampoline.NewServer(ceclient, secrets, env.WebhookID, env.RequestedOnly)),
Handler: httpmetrics.Handler("trampoline", trampoline.NewServer(ceclient, trampoline.ServerOptions{
Secrets: secrets,
WebhookID: env.WebhookID,
RequestedOnlyWebhook: env.RequestedOnly,
OrgFilter: env.OrgFilter,
})),
}
clog.FatalContextf(ctx, "ListenAndServe: %v", srv.ListenAndServe())
}
34 changes: 30 additions & 4 deletions modules/github-events/internal/trampoline/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"io"
"mime"
"net/http"
"strings"
"time"

"github.com/chainguard-dev/clog"
Expand All @@ -24,14 +25,23 @@ type Server struct {
// If webhookID is empty, the trampoline will listen to all events.
webhookID []string
requestedOnlyWebhook []string
orgFilter []string
}

func NewServer(client cloudevents.Client, secrets [][]byte, webhookID []string, requestedOnlyWebhook []string) *Server {
type ServerOptions struct {
Secrets [][]byte
WebhookID []string
RequestedOnlyWebhook []string
OrgFilter []string
}

func NewServer(client cloudevents.Client, opts ServerOptions) *Server {
return &Server{
client: client,
secrets: secrets,
requestedOnlyWebhook: requestedOnlyWebhook,
webhookID: webhookID,
secrets: opts.Secrets,
requestedOnlyWebhook: opts.RequestedOnlyWebhook,
webhookID: opts.WebhookID,
orgFilter: opts.OrgFilter,
clock: clockwork.NewRealClock(),
}
}
Expand Down Expand Up @@ -102,6 +112,22 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
log = log.With("action", msg.Action, "repo", msg.Repository.FullName)
}

// Filter webhook at org level.
if len(s.orgFilter) > 0 {
found := false
for _, org := range s.orgFilter {
if strings.HasPrefix(msg.Repository.FullName, org+"/") {
found = true
break
}
}
if !found {
log.Warnf("ignoring event from repository %q due to non-matching org", msg.Repository.FullName)
w.WriteHeader(http.StatusAccepted)
return
}
}

log.Debugf("forwarding event: %s", t)

event := cloudevents.NewEvent()
Expand Down
62 changes: 55 additions & 7 deletions modules/github-events/internal/trampoline/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,13 @@ func TestTrampoline(t *testing.T) {

secret := []byte("hunter2")
clock := clockwork.NewFakeClock()
impl := NewServer(client, [][]byte{
[]byte("badsecret"), // This secret should be ignored
secret,
}, nil, nil)
opts := ServerOptions{
Secrets: [][]byte{
[]byte("badsecret"), // This secret should be ignored
secret,
},
}
impl := NewServer(client, opts)
impl.clock = clock

srv := httptest.NewServer(impl)
Expand Down Expand Up @@ -127,7 +130,7 @@ func sendevent(t *testing.T, client *http.Client, url string, eventType string,
}

func TestForbidden(t *testing.T) {
srv := httptest.NewServer(NewServer(&fakeClient{}, nil, nil, nil))
srv := httptest.NewServer(NewServer(&fakeClient{}, ServerOptions{}))
defer srv.Close()

// Doesn't really matter what we send, we just want to ensure we get a forbidden response
Expand All @@ -142,7 +145,11 @@ func TestForbidden(t *testing.T) {

func TestWebhookIDFilter(t *testing.T) {
secret := []byte("hunter2")
srv := httptest.NewServer(NewServer(&fakeClient{}, [][]byte{secret}, []string{"doesnotmatch"}, nil))
opts := ServerOptions{
Secrets: [][]byte{secret},
WebhookID: []string{"doesnotmatch"},
}
srv := httptest.NewServer(NewServer(&fakeClient{}, opts))
defer srv.Close()

// Send an event with the requested action
Expand All @@ -157,12 +164,19 @@ func TestWebhookIDFilter(t *testing.T) {

func TestRequestedOnlyWebhook(t *testing.T) {
secret := []byte("hunter2")
srv := httptest.NewServer(NewServer(&fakeClient{}, [][]byte{secret}, nil, []string{"1234"}))
opts := ServerOptions{
Secrets: [][]byte{secret},
RequestedOnlyWebhook: []string{"1234"},
}
srv := httptest.NewServer(NewServer(&fakeClient{}, opts))
defer srv.Close()

// Send an event with the requested action
resp, err := sendevent(t, srv.Client(), srv.URL, "check_run", map[string]interface{}{
"action": "requested",
"repository": map[string]interface{}{
"full_name": "org/repo",
},
}, secret)
if err != nil {
t.Fatalf("error sending event: %v", err)
Expand All @@ -180,3 +194,37 @@ func TestRequestedOnlyWebhook(t *testing.T) {
t.Fatalf("unexpected status: %v", resp.Status)
}
}

func TestOrgFilter(t *testing.T) {
secret := []byte("hunter2")
opts := ServerOptions{
Secrets: [][]byte{secret},
OrgFilter: []string{"org"},
}
srv := httptest.NewServer(NewServer(&fakeClient{}, opts))
defer srv.Close()

// Send an event with the requested action
resp, err := sendevent(t, srv.Client(), srv.URL, "pull_request", map[string]interface{}{
"action": "opened",
}, secret)
if err != nil {
t.Fatalf("error sending event: %v", err)
}
if resp.StatusCode != http.StatusAccepted {
t.Fatalf("unexpected status: %v", resp.Status)
}

resp, err = sendevent(t, srv.Client(), srv.URL, "pull_request", map[string]interface{}{
"action": "opened",
"repository": map[string]interface{}{
"full_name": "org/repo",
},
}, secret)
if err != nil {
t.Fatalf("error sending event: %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Fatalf("unexpected status: %v", resp.Status)
}
}
22 changes: 14 additions & 8 deletions modules/github-events/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,21 @@ module "this" {
}
ports = [{ container_port = 8080 }]
env = concat(
[{
name = "WEBHOOK_SECRET"
value_source = {
secret_key_ref = {
secret = module.webhook-secret.secret_id
version = "latest"
[
{
name = "WEBHOOK_SECRET"
value_source = {
secret_key_ref = {
secret = module.webhook-secret.secret_id
version = "latest"
}
}
}
}],
},
{
name = "GITHUB_ORGANIZATIONS_FILTER"
value = var.github_organizations
},
],
[for name, secret in var.additional_webhook_secrets : {
name = "WEBHOOK_SECRET_${upper(name)}"
value_source = {
Expand Down
8 changes: 7 additions & 1 deletion modules/github-events/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ Additional secrets to be used by the service.
See https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/cloud_run_v2_service#nested_env for related documentation.
EOD
default = {}
default = {}
}

variable "service-ingress" {
Expand Down Expand Up @@ -93,3 +93,9 @@ variable "squad" {
error_message = "squad needs to specified or disable check by setting require_squad = false"
}
}

variable "github_organizations" {
description = "csv string of GitHub organizations to allow."
type = string
default = ""
}

0 comments on commit 1f314e4

Please sign in to comment.