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 }