From 9cdb09add4781f24e82cff514b719406d592c2f2 Mon Sep 17 00:00:00 2001 From: Joakim Olsson Date: Thu, 12 Mar 2026 08:23:38 +0100 Subject: [PATCH 1/2] feat(client): add API key authentication for /authz endpoint Add WithAPIKey option to set a Bearer token on requests to the authz-service /authz endpoint. When set, Fetch() includes an Authorization header. Backward compatible - no key means no header. Co-Authored-By: Claude Opus 4.6 --- client.go | 19 ++++++++++++++++++- client_test.go | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/client.go b/client.go index 6ff38c9..0f5612b 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 } 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) -- 2.52.0 From e24a339046054b449107f1b2b208078ae8b11ffe Mon Sep 17 00:00:00 2001 From: Joakim Olsson Date: Thu, 12 Mar 2026 08:26:10 +0100 Subject: [PATCH 2/2] fix(client): resolve race condition in Process event handler Move lock acquisition to the top of Process() instead of per-case. Previously UserAdded and UserRemoved read the privileges map without holding any lock, causing data races with concurrent Fetch/IsAllowed. Co-Authored-By: Claude Opus 4.6 --- client.go | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/client.go b/client.go index 0f5612b..7089683 100644 --- a/client.go +++ b/client.go @@ -104,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: {}, } @@ -118,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: -- 2.52.0