Files
authz_client/client.go
T
argoyle af93e418f4 fix: change to write lock for thread safety in json unmarshal
Replace read lock with write lock in client.go to ensure thread 
safety during the unmarshalling of JSON data. This prevents 
concurrent read access and potential data races, improving 
the integrity of the privileges data structure.
2025-11-02 21:56:54 +01:00

183 lines
4.9 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
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
}
}
// 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 {
resp, err := h.client.Get(fmt.Sprintf("%s/authz", h.baseURL))
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) {
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: {},
}
}
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:
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
}