Files
authz_client/client.go
T
argoyleandClaude Opus 5 fc0a4a0e31
authz_client / test (push) Skipped
authz_client / vulnerabilities (push) Skipped
pre-commit / pre-commit (push) Skipped
authz_client / vulnerabilities (pull_request) Successful in 46s
authz_client / test (pull_request) Successful in 59s
pre-commit / pre-commit (pull_request) Successful in 2m39s
test: assert which event type each privilege routing key decodes to
Swapping the handler for Privilege.Removed to PrivilegeAdded passed the suite, and would turn every revocation into a grant. The Setup test now asserts each key's message type, the adapter is exercised for all four events (grant then revoke), and process only accepts the four privilege event types. CLAUDE.md warns against combining Setup() with WithReconnect (per-replica queues are re-created on reconnect, losing revocations sent meanwhile).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SV1epy2pKvBx3yDfhxATk2
2026-09-11 22:57:25 +02:00

209 lines
5.5 KiB
Go

package client
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"reflect"
"sync"
goamqp "codeberg.org/messaging/go-messaging-amqp"
spec "codeberg.org/messaging/messaging"
)
// CompanyPrivileges contains the privileges for a combination of email address and company id
type CompanyPrivileges struct {
Admin bool `json:"admin"`
Company bool `json:"company"`
Consumer bool `json:"consumer"`
Time bool `json:"time"`
Invoicing bool `json:"invoicing"`
Accounting bool `json:"accounting"`
Supplier bool `json:"supplier"`
Salary bool `json:"salary"`
}
// PrivilegeHandler processes PrivilegeAdded-events and fetches the initial set of privileges from an authz-service
type PrivilegeHandler struct {
*sync.RWMutex
client *http.Client
baseURL string
apiKey string
privileges map[string]map[string]*CompanyPrivileges
}
// OptsFunc is used to configure the PrivilegeHandler
type OptsFunc func(handler *PrivilegeHandler)
// WithBaseURL sets the base URL to the authz-service
func WithBaseURL(url string) OptsFunc {
return func(handler *PrivilegeHandler) {
handler.baseURL = url
}
}
// 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{
RWMutex: &sync.RWMutex{},
client: &http.Client{},
baseURL: "http://authz-service",
privileges: map[string]map[string]*CompanyPrivileges{},
}
for _, opt := range opts {
opt(handler)
}
return handler
}
// Fetch the initial set of privileges from an authz-service
func (h *PrivilegeHandler) Fetch() error {
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
}
buff, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
h.Lock()
defer h.Unlock()
err = json.Unmarshal(buff, &h.privileges)
if err != nil {
return err
}
return nil
}
func (h *PrivilegeHandler) Setup() []goamqp.Setup {
return []goamqp.Setup{
goamqp.TransientEventStreamConsumer("User.Added", process[UserAdded](h)),
goamqp.TransientEventStreamConsumer("User.Removed", process[UserRemoved](h)),
goamqp.TransientEventStreamConsumer("Privilege.Added", process[PrivilegeAdded](h)),
goamqp.TransientEventStreamConsumer("Privilege.Removed", process[PrivilegeRemoved](h)),
}
}
// privilegeEvent is the set of events Process handles.
type privilegeEvent interface {
UserAdded | UserRemoved | PrivilegeAdded | PrivilegeRemoved
}
// process adapts Process to a typed go-messaging-amqp handler.
func process[T privilegeEvent](h *PrivilegeHandler) spec.EventHandler[T] {
return func(_ context.Context, event spec.ConsumableEvent[T]) error {
return h.Process(&event.Payload)
}
}
// Process privilege-related events and update the internal state
func (h *PrivilegeHandler) Process(msg any) 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.privileges[ev.Email] = map[string]*CompanyPrivileges{
ev.CompanyID: {},
}
}
return nil
case *UserRemoved:
if priv, exists := h.privileges[ev.Email]; exists {
delete(priv, ev.CompanyID)
}
return nil
case *PrivilegeAdded:
h.setPrivileges(ev.Email, ev.CompanyID, ev.Privilege, true)
return nil
case *PrivilegeRemoved:
h.setPrivileges(ev.Email, ev.CompanyID, ev.Privilege, false)
return nil
default:
fmt.Printf("Got unexpected message type (%s): '%+v'\n", reflect.TypeOf(msg).String(), msg)
return fmt.Errorf("unexpected event type: '%s'", reflect.TypeOf(msg))
}
}
func (h *PrivilegeHandler) setPrivileges(email, companyId string, privilege Privilege, set bool) {
if priv, exists := h.privileges[email]; exists {
if c, exists := priv[companyId]; exists {
switch privilege {
case PrivilegeAdmin:
c.Admin = set
case PrivilegeCompany:
c.Company = set
case PrivilegeConsumer:
c.Consumer = set
case PrivilegeTime:
c.Time = set
case PrivilegeInvoicing:
c.Invoicing = set
case PrivilegeAccounting:
c.Accounting = set
case PrivilegeSupplier:
c.Supplier = set
case PrivilegeSalary:
c.Salary = set
}
} else {
priv[companyId] = &CompanyPrivileges{}
h.setPrivileges(email, companyId, privilege, set)
}
} else {
h.privileges[email] = map[string]*CompanyPrivileges{}
h.setPrivileges(email, companyId, privilege, set)
}
}
// CompaniesByUser return a slice of company ids matching the provided email and predicate func
func (h *PrivilegeHandler) CompaniesByUser(email string, predicate func(privileges CompanyPrivileges) bool) []string {
h.RLock()
defer h.RUnlock()
var result []string
if p, exists := h.privileges[email]; exists {
for k, v := range p {
if predicate(*v) {
result = append(result, k)
}
}
}
return result
}
// IsAllowed return true if the provided predicate return true for the privileges matching the provided email and companyID, return false otherwise
func (h *PrivilegeHandler) IsAllowed(email, companyID string, predicate func(privileges CompanyPrivileges) bool) bool {
h.RLock()
defer h.RUnlock()
if p, exists := h.privileges[email]; exists {
if v, exists := p[companyID]; exists {
return predicate(*v)
}
}
return false
}