fe0abd62c8
## Summary - Add `WithAPIKey(key string)` option to `PrivilegeHandler` - When set, `Fetch()` sends `Authorization: Bearer <key>` 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: #294
195 lines
5.1 KiB
Go
195 lines
5.1 KiB
Go
package client
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"reflect"
|
|
"sync"
|
|
|
|
"github.com/sparetimecoders/goamqp"
|
|
)
|
|
|
|
// 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", h.Process, UserAdded{}),
|
|
goamqp.TransientEventStreamConsumer("User.Removed", h.Process, UserRemoved{}),
|
|
goamqp.TransientEventStreamConsumer("Privilege.Added", h.Process, PrivilegeAdded{}),
|
|
goamqp.TransientEventStreamConsumer("Privilege.Removed", h.Process, PrivilegeRemoved{}),
|
|
}
|
|
}
|
|
|
|
// 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.privileges[ev.Email] = map[string]*CompanyPrivileges{
|
|
ev.CompanyID: {},
|
|
}
|
|
}
|
|
return nil, nil
|
|
case *UserRemoved:
|
|
if priv, exists := h.privileges[ev.Email]; exists {
|
|
delete(priv, ev.CompanyID)
|
|
}
|
|
return nil, nil
|
|
case *PrivilegeAdded:
|
|
h.setPrivileges(ev.Email, ev.CompanyID, ev.Privilege, true)
|
|
return nil, nil
|
|
case *PrivilegeRemoved:
|
|
h.setPrivileges(ev.Email, ev.CompanyID, ev.Privilege, false)
|
|
return nil, nil
|
|
default:
|
|
fmt.Printf("Got unexpected message type (%s): '%+v'\n", reflect.TypeOf(msg).String(), msg)
|
|
return nil, 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
|
|
}
|