Permissions matrix¶
This page is the mechanical truth about authorization in the eBiz platform
API: what is checked, where, and what each token grants. Verified against
API module v3_7_3 source code.
The platform does not use dotted permission keys (there is no
deal.create / rebate.edit style catalogue anywhere in the code).
Authorization is enforced in three layers:
- Authentication firewall — a valid JWT is required for all API events except an explicit public whitelist.
- Role tokens — programmatic checks against the roles carried on the
authenticated request (
request.sIsUserInRole(...)). - Object-level ACL — per-object
view/edit/delete/adminprivileges stored in thesecuritydatabase table and evaluated bysecurityService.hasPermission(...).
Layer 1: Authentication firewall¶
Configured in the app security config (firewall rules, lines 858–947). Two
rules cover event patterns v3_* and api.*; any matching event requires
a valid JWT (HS512, 600-minute expiration, Authorization: Bearer or
x-auth-token header). Failing requests are redirected to /login.
Public (no authentication) events are those on the two whitelists
(app config :871 and :879), including: login.*, api.auth.*,
signup.* / api.signup.*, healthcheck.*, admin.*, avatar.* /
api.avatar.*, campaign.view / campaign.rd* / campaign.webhook /
api.campaign.rd / api.campaign.track, blog.viewDigest,
documents.thumbnail / api.documents.thumbnail, pim.image /
api.pim.image, quickbooks.*, indexer.* / api.indexer.*,
site.getSite, whatif.linearReport, audit.userReport, cbswagger.*,
api.test.*, api.email.*, api.import.*, api.msgraph.*,
api.tickets.checkMail, socialite.*, error.*, api.index.*.
Both rules also declare roles: "view" and permissions: "view".
Handler annotation security is enabled (handlerAnnotationSecurity: true,
app config :914) but no handler in any API version uses a
secured= annotation, so there is no per-action declarative permission
gating.
Layer 2: Role tokens¶
On every authenticated request, request.roles is built by
interceptors/eunify (the JWT-authentication interceptor, lines
250–320) as the union of:
- the user's company known-as name and company name
memberif the user's company is COMPANYTYPE 3,supplierif COMPANYTYPE 2, both if COMPANYTYPE 4 (v3_7_3/models/userService:288–297)- the name of each database role assigned to the user
- the user's email address (lowercased)
- each of the user's contact tags
Handlers and models test membership with request.sIsUserInRole("token")
(exact match) or request.sIsUserInAnyRole("a,b,c") (any of a list).
Static tokens checked in v3_7_3 handlers and models:
| Role token | Guards | Representative check sites |
|---|---|---|
member |
Member-vs-supplier branching: search scope, document visibility, figures visibility, blog, sign-off, rebate views | handlers/search:32, handlers/documents:695, handlers/signoff:141, handlers/rebate:363, handlers/audit:55 |
admin |
Admin-only create paths, category-security bypass, extended search | handlers/documents:48, handlers/search:179, handlers/psa:550, handlers/spend:552 |
superusers |
Superuser dashboards, aggregate figures, security admin, and the ACL fallback for edit/delete/admin (see layer 3) | handlers/dashboard:48, handlers/rebate:783, handlers/security:15, models/securityService:386 |
superuser (singular) |
Campaign edit/admin/delete capability flags | handlers/campaign:349–351 |
supplier |
Supplier-specific rebate logic | handlers/rebate:363,422,461 |
figures |
Exposure of PAYABLE vs MEMBERPAYABLE rebate figures | handlers/rebate:1016,1031,1074 |
figuresEntry |
Turnover/figures data entry | handlers/spend:309,424 |
edit |
Spend edit rights (checked as edit or any-of edit,admin) |
handlers/spend:424 |
turnover, ownfigures |
Spend/turnover access (any-of check with figuresEntry) |
handlers/spend:309 |
rebates |
Rebate access | handlers/rebate:486 |
categoryRestrictions |
Category restriction behavior | checked in v3_7_3 handlers/models (2 sites) |
staff, ebiz |
Internal/staff branches (ebiz gates a debug dump only) |
handlers/documents:1056 |
Some report/form definitions also check dynamic tokens read from XML
element attributes (sIsUserInRole("#elem.permissions[...]#")), so the
static list above is not exhaustive for XML-driven surfaces.
Because role tokens include company names, emails, and tags, the full set of possible tokens is per-site data, not a static enum. The table above lists only the tokens hard-coded in application logic.
Layer 3: Object-level ACL (view / edit / delete / admin)¶
securityService.hasPermission(priviledge, objectID, objectType)
(v3_7_3/models/securityService:375–421) evaluates rules from the
security DB table (columns: priviledge, role, pType of
any|must|not, relatedID, securityAgainst, siteID), falling back to
request.siteConfig.defaultSecurity[objectType] when an object has no
rows for a privilege.
Evaluation semantics (as implemented):
- If no rules exist for the requested privilege:
viewis granted to any authenticated user (default-open), andedit/delete/adminare granted only to thesuperusersrole (securityService:382–388). - Otherwise the privilege is granted when the user matches at least one
anyrole, allmustroles, and none of thenotroles. A grant can only originate from ananymatch — rules with onlymust/notentries evaluate to denied. - Role entries beginning with
$are skipped during evaluation.
The same rules are enforced inside list queries via a SQL
hasPermission(...) function in HAVING clauses, so users only see rows
they can view.
Object types covered, and where each is checked:
| Object type | Blocking server-side check | Capability flags in response | List filtering (SQL) |
|---|---|---|---|
agreement (deals/PSAs) |
handlers/psa:55 aborts if no view |
handlers/psa:93–99 |
psaService, export, figuresService |
document |
— | handlers/documents:368–371 |
documentService |
category (DMS) |
— | handlers/documents:57–60,238–241 |
documentService |
calendar |
— | handlers/calendar:240–243 |
calendarService |
blog (news) |
handlers/blog:229 guards delete |
handlers/blog:212–217 |
— |
tender |
— | handlers/tender:21–24 |
— |
survey |
— | handlers/survey:469–472 |
surveyService |
Important: most handler-level hasPermission calls populate a
permissions struct in the JSON response — capability hints the front
end uses to show or hide actions. They do not block the request. The
only confirmed hard server-side gates are the agreement view abort
(psa:55) and the blog delete guard (blog:229); everything else
relies on list filtering plus front-end enforcement.
ACL rules are administered through handlers/security:
getBasicPermissions (groups available to the current user, gated by
superusers membership), getPermissions, and savePermissions.
sitePermissions (proof of concept)¶
A per-site deal-security field (sitePermissions) is being added to
Elasticsearch documents and query filters
(securityService:164–294, projected in psaService:1246,
companyService:884, contactService:947). During the POC it is
additive and fail-open, and is not yet a load-bearing authorization
layer.
What this page is not¶
- It is not a catalogue of user-manageable permission toggles — role
tokens come from database group/role names per site; group-specific role
conventions belong in
tenant-notes/pages. - It does not document authentication setup (login flows, JWT issuance) beyond the firewall summary above.
userService.hasPermission()(v3_7_3/models/userService:53–63) exists but has no callers — it is interface-satisfying code, not part of the authorization model described here.