From fe0abd62c86baa12dbd49a1d90d63f1d370eeef8 Mon Sep 17 00:00:00 2001 From: Joakim Olsson Date: Thu, 12 Mar 2026 07:32:12 +0000 Subject: [PATCH] feat(client): add API key authentication for /authz endpoint (#294) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Add `WithAPIKey(key string)` option to `PrivilegeHandler` - When set, `Fetch()` sends `Authorization: Bearer ` header - Backward compatible: no key = no header (existing behavior) ## Test plan - [x] Unit test verifying Authorization header is sent - [x] Unit test verifying no header without key - [x] Existing tests still pass 🤖 Generated with [Claude Code](https://claude.com/claude-code) Reviewed-on: https://gitea.unbound.se/shiny/authz_client/pulls/294 --- client.go | 30 +++++++++++++++++++++--------- client_test.go | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 9 deletions(-) diff --git a/client.go b/client.go index 6ff38c9..7089683 100644 --- a/client.go +++ b/client.go @@ -28,6 +28,7 @@ type PrivilegeHandler struct { *sync.RWMutex client *http.Client baseURL string + apiKey string privileges map[string]map[string]*CompanyPrivileges } @@ -41,6 +42,13 @@ func WithBaseURL(url string) OptsFunc { } } +// WithAPIKey sets an API key used as a Bearer token when fetching privileges +func WithAPIKey(key string) OptsFunc { + return func(handler *PrivilegeHandler) { + handler.apiKey = key + } +} + // New creates a new PrivilegeHandler. Pass OptsFuncs to configure. func New(opts ...OptsFunc) *PrivilegeHandler { handler := &PrivilegeHandler{ @@ -57,7 +65,16 @@ func New(opts ...OptsFunc) *PrivilegeHandler { // Fetch the initial set of privileges from an authz-service func (h *PrivilegeHandler) Fetch() error { - resp, err := h.client.Get(fmt.Sprintf("%s/authz", h.baseURL)) + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/authz", h.baseURL), nil) + if err != nil { + return err + } + + if h.apiKey != "" { + req.Header.Set("Authorization", "Bearer "+h.apiKey) + } + + resp, err := h.client.Do(req) if err != nil { return err } @@ -87,13 +104,14 @@ func (h *PrivilegeHandler) Setup() []goamqp.Setup { // Process privilege-related events and update the internal state func (h *PrivilegeHandler) Process(msg interface{}, _ goamqp.Headers) (interface{}, error) { + h.Lock() + defer h.Unlock() + switch ev := msg.(type) { case *UserAdded: if priv, exists := h.privileges[ev.Email]; exists { priv[ev.CompanyID] = &CompanyPrivileges{} } else { - h.Lock() - defer h.Unlock() h.privileges[ev.Email] = map[string]*CompanyPrivileges{ ev.CompanyID: {}, } @@ -101,19 +119,13 @@ func (h *PrivilegeHandler) Process(msg interface{}, _ goamqp.Headers) (interface return nil, nil case *UserRemoved: if priv, exists := h.privileges[ev.Email]; exists { - h.Lock() - defer h.Unlock() delete(priv, ev.CompanyID) } return nil, nil case *PrivilegeAdded: - h.Lock() - defer h.Unlock() h.setPrivileges(ev.Email, ev.CompanyID, ev.Privilege, true) return nil, nil case *PrivilegeRemoved: - h.Lock() - defer h.Unlock() h.setPrivileges(ev.Email, ev.CompanyID, ev.Privilege, false) return nil, nil default: diff --git a/client_test.go b/client_test.go index d8cfe4e..f179563 100644 --- a/client_test.go +++ b/client_test.go @@ -251,6 +251,39 @@ func TestPrivilegeHandler_IsAllowed_Return_True_If_Privilege_Exists(t *testing.T assert.True(t, result) } +func TestPrivilegeHandler_Fetch_Sends_Authorization_Header_When_APIKey_Set(t *testing.T) { + var receivedAuth string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + receivedAuth = r.Header.Get("Authorization") + _, _ = w.Write([]byte("{}")) + })) + defer server.Close() + + handler := New( + WithBaseURL(server.URL), + WithAPIKey("my-secret-key"), + ) + + err := handler.Fetch() + assert.NoError(t, err) + assert.Equal(t, "Bearer my-secret-key", receivedAuth) +} + +func TestPrivilegeHandler_Fetch_No_Authorization_Header_Without_APIKey(t *testing.T) { + var receivedAuth string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + receivedAuth = r.Header.Get("Authorization") + _, _ = w.Write([]byte("{}")) + })) + defer server.Close() + + handler := New(WithBaseURL(server.URL)) + + err := handler.Fetch() + assert.NoError(t, err) + assert.Empty(t, receivedAuth) +} + func TestPrivilegeHandler_Fetch_Error_Response(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(500)