HOCO B2B
SRSRequirements

Roles, permissions & data scope

The eight roles, the module × action permission matrix, delegate and reseller data scopes, least privilege and server-side enforcement on every operation.

This area defines who may do what, and on which records (cahier §3, §3.1, §4.7, §10, §12). Permissions are module × action keys attached to roles; data scopes limit delegates to their zones and assigned resellers and resellers to their own data. The server's access module enforces both on every operation of every client; the back-office edits the matrix (BO-62); every app hides what the current user cannot do. The authoritative cell-by-cell default matrix is the permissions matrix; this page states the rules that matrix must respect.

IDRequirementReleasePrioritySourceApps
ACL-01Eight roles: super admin, commercial admin, stock, finance, delivery, direction, delegate, resellerV1MUST§3API · BO
ACL-02Permissions per module and action (view, create, update, delete, validate, export, administer), editable matrixV1MUST§3.1API · BO
ACL-03Data scope: delegates see only their zones, tours, assigned resellers and own performance; resellers only their own dataV1MUST§3.1API
ACL-04Least privilege by default; financial data (purchase prices, margins, credit) strictly limitedV1MUST§3.1, §4.7API · BO
ACL-05Every API operation enforces permission and scope server-side; UI hides what the user cannot doV1MUST§10, §12API · BO · RA · RW · DA

Requirements

ACL-01 — Eight system roles

ReleasePrioritySourceAppsOwner tasks
V1MUSTcahier §3API · BOSRV-06

Statement — The system shall provide eight system roles — super administrator, commercial administrator, stock manager, finance manager, delivery manager, direction, delegate and reseller — each with a default permission set.

Rules

  1. Role keys (RoleKey) are seeded by the first migration (SRV-02) and are permanent: they cannot be deleted or re-keyed (409 ROLE_PROTECTED). Their French labels come from the enum.

  2. A staff user holds one or more staff roles; the effective permissions are the union of their roles. DELEGATE and RESELLER are exclusive and follow the user type: a delegate user has exactly DELEGATE, a reseller user exactly RESELLER, and neither can receive a staff role (422 ROLE_INCOMPATIBLE).

  3. Default intent of each role (cell-level defaults in the permissions matrix):

    RoleDefault permissions
    SUPER_ADMINEvery permission of every module, including roles.*, users.administer, settings.administer and audit.*. Cannot be reduced.
    COMMERCIAL_ADMINdashboard.view; crm.*; field.*; orders.view/create/update/validate; promotions.*; communication.*; support.*; reports.view/export; catalog.view. No pricing.* (purchase prices, margins) and no finance.* by default.
    STOCK_MANAGERdashboard.view; catalog.*; stock.*; orders.view; reports.view/export. No purchase prices (pricing.view).
    FINANCE_MANAGERdashboard.view; finance.*; pricing.* (the three selling prices, purchase prices, margins); orders.view/validate (credit decisions); crm.view; catalog.view; reports.view/export.
    DELIVERY_MANAGERdashboard.view; delivery.*; orders.view; stock.view; reports.view.
    DIRECTIONview and export on dashboard, reports, crm, orders, catalog, pricing, stock, finance, field and promotions; no create, update, delete or validate.
    DELEGATENo back-office access. The keys of this row (listed in the matrix) are always applied inside the delegate scope (ACL-03).
    RESELLERNo back-office permission. Reseller operations are governed by the self scope.
  4. Roles are linked to users through UserRole. A change applies to the user's next request (permission cache at most 60 seconds) and is logged as PERMISSION_CHANGE.

  5. Custom roles beyond these eight are not part of V1; HOCO adjusts the permissions of the existing roles instead (ACL-02).

ValueUI label (fr)Meaning
SUPER_ADMINSuper administrateurFull access, global settings, roles, permissions and audit.
COMMERCIAL_ADMINAdministrateur commercialCRM, resellers, delegates, tours, promotions, orders and reporting.
STOCK_MANAGERResponsable stockProducts, categories, variants, quantities, inventories and alerts.
FINANCE_MANAGERResponsable financePayments, credit, invoices, due dates and financial reports.
DELIVERY_MANAGERResponsable livraisonPreparation, shipping, carrier assignment and proof of delivery.
DIRECTIONDirectionRead-only dashboards, KPIs and consolidated reports.
DELEGATEDélégué commercialTours, visits, sign-ups, invitations, assisted orders and reports — own scope only.
RESELLERRevendeurCatalog, cart, orders, invoices, promotions, loyalty and support — own data only.

Acceptance criteria

  • On a fresh database the 8 roles exist with the keys above; deleting STOCK_MANAGER returns 409 ROLE_PROTECTED.
  • A staff user holding STOCK_MANAGER and FINANCE_MANAGER can both adjust stock and record a payment.
  • Giving COMMERCIAL_ADMIN to a delegate user returns 422 ROLE_INCOMPATIBLE.
  • A DIRECTION user gets 403 PERMISSION_DENIED on admin.orders.validate and can request an export of the orders report.
  • me.get returns the user's role keys and the effective permission keys.

Data — Role, Permission, RolePermission, UserRole, User, AuditLog.

Interfaces

  • Screens: none dedicated
  • API: no dedicated operation

Notes / open points — The default role contents are a proposal derived from cahier §3; HOCO validates them during cadrage (PRJ-03).

ACL-02 — Permission matrix by module and action

ReleasePrioritySourceAppsOwner tasks
V1MUSTcahier §3.1API · BOSRV-06

Statement — The system shall express permissions as module × action keys and let a super administrator edit the permission set of each staff role in a matrix, within guard rails that prevent lock-out and privilege escalation.

Rules

  1. Keys are module.action with modules dashboard, crm, catalog, pricing, stock, orders, delivery, finance, promotions, loyalty, field, support, communication, reports, users, roles, audit, settings and actions view, create, update, delete, validate, export, administer. The catalogue (admin.permissions.list) lists only the pairs that some operation checks, each with a French description — e.g. stock.validate: « Valider un inventaire ou autoriser une commande au-delà du stock ».
  2. Every non-view action implies view on the same module (a user who validates orders can see them); otherwise no key implies another — orders.administer does not grant orders.validate.
  3. The matrix (BO-62) shows modules as rows and actions as columns for one role at a time; saving calls admin.roles.updatePermissions, which replaces the role's set atomically.
  4. Guard rails: the SUPER_ADMIN set cannot be changed, and the DELEGATE and RESELLER rows are not editable (409 ROLE_PROTECTED); roles.administer and users.administer can be granted to SUPER_ADMIN only (422 PERMISSION_NOT_GRANTABLE); at least one ACTIVE user must hold SUPER_ADMIN (409 LAST_SUPER_ADMIN when removing the role from, suspending or disabling the last one).
  5. Editing needs roles.administer and a fresh session (AUTH-01 R8). Each save writes AuditLog PERMISSION_CHANGE with the added and removed keys.
  6. A change applies to the next request of every holder of the role; open SSE streams re-evaluate permissions on every event they emit.
  7. « Rétablir les valeurs par défaut » restores the seeded set of a role (logged like any change).

Acceptance criteria

  • Removing orders.validate from COMMERCIAL_ADMIN makes admin.orders.validate return 403 PERMISSION_DENIED for a commercial admin on the next request, and the activity log shows the removed key.
  • Saving a set that contains an unknown key such as stock.adjust returns 400 VALIDATION_FAILED.
  • Granting orders.validate automatically includes orders.view in the saved set.
  • Editing the SUPER_ADMIN or DELEGATE row returns 409 ROLE_PROTECTED; granting roles.administer to DIRECTION returns 422 PERMISSION_NOT_GRANTABLE.
  • Suspending the only active super administrator returns 409 LAST_SUPER_ADMIN.

Data — Permission, RolePermission, Role, AuditLog.

Interfaces

ACL-03 — Data scope for delegates and resellers

ReleasePrioritySourceAppsOwner tasks
V1MUSTcahier §3.1APISRV-06 · MDA-04

Statement — The system shall limit delegates to their zones, their tours, the resellers assigned to them and their own performance, and resellers to their own data, on every read and every write.

Rules

  1. Reseller scope (self). The reseller id is taken from the session (User → its Reseller), never from the request. Every self operation filters on it; an id belonging to another reseller answers 404 NOT_FOUND.
  2. Delegate scope (delegate). A reseller is in delegate D's scope when: (a) its current assignment is D; or (b) its commune belongs to one of D's zones (cahier §3.1 « leur zone »: D sees the points of sale of their zone and can prospect without creating duplicates); or (c) it is a stop of a tour of D that is in progress or dated today or later (D covers a colleague).
  3. Records attached to a reseller (orders, visit history, photos, notes) follow the reseller's scope, with three exceptions: D sees only their own follow-up tasks; positions and trails of other delegates are never visible; financial amounts follow ACL-04.
  4. Delegate-owned records — tours, visits, location pings, objectives, performance, invitations, assisted orders — are visible to their delegate only.
  5. Reads of a record outside the scope answer 404 NOT_FOUND, so existence is not leaked; writes that reference an out-of-scope record (an assisted order, a note, an unplanned visit for a reseller outside the scope) answer 403 OUT_OF_SCOPE.
  6. Scope is evaluated at request time, so reassignment applies at once. Data already on a delegate's phone is cleaned by the next sync.delta, which sends removals for records that left the scope; queued offline mutations on such records are rejected item by item with OUT_OF_SCOPE and shown in the sync centre (DA-27).
  7. Staff have no row-level restriction in V1: they see every record of the modules they hold view on.
  8. Scopes are implemented as reusable query filters in the data layer (SRV-06), never by filtering results after the query, so totals, counters and pagination are correct.

Acceptance criteria

  • Reseller A calling orders.get with an order id of reseller B gets 404 NOT_FOUND.
  • Delegate D's delegate.resellers.list returns the resellers assigned to D, every point of sale of D's communes and the stops of D's upcoming tours — and no reseller of a commune outside D's zones unless D has it on a tour.
  • D creating an assisted order for a reseller of another zone, not on D's tours, gets 403 OUT_OF_SCOPE.
  • After a reseller of another zone that was assigned to D is reassigned to E, D's next sync.delta removes it from the phone, and a note D queued offline on it comes back REJECTED with OUT_OF_SCOPE.
  • orders.summary for a reseller counts only that reseller's orders.
  • D requesting another delegate's tour gets 404 NOT_FOUND; admin.tours.track is not reachable with a delegate session.

Data — Reseller, ResellerAssignment, DelegateZone, ZoneArea, Tour, Visit, Order, CrmTask, LocationPing.

Interfaces

Notes / open points — Restricting a commercial administrator to a region is not in the cahier and not in V1; it can be added later as a staff scope (see the decision register).

ACL-04 — Least privilege and restricted financial data

ReleasePrioritySourceAppsOwner tasks
V1MUSTcahier §3.1, §4.7API · BOSRV-06 · SRV-18 · DSH-07

Statement — The system shall grant nothing by default and shall expose purchase prices, margins, credit and other financial data only to users holding the corresponding finance permission.

Rules

  1. Deny by default: a staff user without a role can only sign in and call me.get; a permission added to the catalogue is granted to SUPER_ADMIN only; a back-office operation without a declared permission is unreachable — a contract test fails the build if one exists.

  2. Financial data and the keys that open it:

    DataVisible withChanged with
    Purchase price (prix d'achat) of a variant, unit cost of a stock receipt, price historypricing.viewpricing.update
    Margins and margin rates of products and order lines (computed)pricing.view—
    The three selling prices of a variant (super-gros, demi-gros, détail)catalog.viewpricing.update
    Invoices, payments and declarations of all resellersfinance.viewfinance.create, finance.update, finance.validate
    Credit limit, used and available credit, blockingfinance.view (and orders.validate inside an order's credit control)finance.administer
  3. Fields the caller may not see are omitted from the response — absent, not null — in lists, details, exports, global search and SSE payloads; the contract declares them optional.

  4. Delegates never receive purchase prices, margins, credit amounts or invoices; in assisted orders they see only the price of the reseller's customer type. For a reseller in their scope they receive one boolean, financialAlert (an invoice is overdue or the credit is blocked), so they can raise it during a visit.

  5. Resellers receive only the price of their own customer type — never the two other prices — and their own invoices, payments and credit (PRC-05, FIN-10). Guests receive no price at all (AUTH-10).

  6. Exports re-check permissions when the file is generated: if the requester lost finance.view in the meantime, the financial columns are left out.

  7. Error details follow the same rule: credit amounts in an order's controls go to holders of orders.validate or finance.view, others see only the control's status; the reseller receives the amounts of their own credit in CREDIT_LIMIT_EXCEEDED.

Acceptance criteria

  • admin.products.get returns purchasePrice to a finance manager and omits the field for a stock manager; both see the three selling prices.
  • An XLSX export of products requested by a commercial administrator has no purchase-price or margin column.
  • delegate.resellers.get for a reseller with an overdue invoice returns financialAlert: true and no amount.
  • admin.orders.get returns purchasePrice and marginHt on order lines only to holders of pricing.view; on a credit order over the limit, a delivery manager (no orders.validate, no finance.view) sees the credit control status without figures.
  • A new staff user without roles gets 403 PERMISSION_DENIED on every admin.* operation.

Data — ProductVariant (three selling prices, purchase price), CreditAccount, Invoice, Payment, RolePermission.

Interfaces

ACL-05 — Server-side enforcement, hidden actions in the UI

ReleasePrioritySourceAppsOwner tasks
V1MUSTcahier §10, §12API · BO · RA · RW · DASRV-06 · DSH-01 · SRV-28

Statement — The system shall check version, authentication, account status, permission and scope on the server for every API operation, and every client shall hide the actions the current user is not allowed to perform.

Rules

  1. Every request goes through the same pipeline, in this order: (1) mobile version policy — 426 UPGRADE_REQUIRED; (2) authentication — 401 UNAUTHENTICATED; (3) account status — 403 ACCOUNT_SUSPENDED or 403 ACCOUNT_PENDING_VALIDATION; (4) the operation's permission or scope type — 403 PERMISSION_DENIED; (5) input validation — 400 VALIDATION_FAILED; (6) record lookup within scope — 404 NOT_FOUND or 403 OUT_OF_SCOPE; (7) business rules — 409 or 422. Permission comes before validation so that a caller without access learns nothing about the input.
  2. Each contract procedure declares its access — public, self, delegate, a permission key, or any (any signed-in staff user, used by global search and the event stream) — and a single middleware enforces the declaration. State-dependent rules (cancellation by status, visit lock) live in the domain services, not in the clients.
  3. Detail responses for orders, resellers, tours, visits, invoices and promotions include allowedActions — the actions the caller may perform now (e.g. ["VALIDATE", "REFUSE", "MODIFY", "CANCEL"]) — computed by the same code as the operations. Clients render their buttons from it rather than re-implementing the rules.
  4. Clients hide navigation entries and buttons for which the user lacks the permission. When the user has the permission but the record's state forbids the action, the button stays visible but disabled with the reason (« Commande déjà validée : contactez l'assistance pour l'annuler »).
  5. Opening a back-office URL the user cannot access shows « Accès refusé » (403 page) and never partial data.
  6. Global search (admin.search) returns only the entity types and records the caller can view; the SSE stream filters each event by the recipient's permissions and scope.
  7. Denied calls are written to the application log with user, operation and request id (NFR-08).
  8. An automated permission sweep (SRV-28) calls every operation with a user of each role and compares allow or deny with the matrix; it runs in CI and blocks the merge on a mismatch.

Acceptance criteria

  • A suspended user with an outdated app version gets 426 first, then 403 ACCOUNT_SUSPENDED after updating.
  • A delivery manager sending an invalid body to admin.promotions.crud gets 403 PERMISSION_DENIED, not 400.
  • admin.orders.get on a SUBMITTED order returns allowedActions containing VALIDATE for a commercial administrator and not for a delivery manager.
  • A stock manager does not see the Finance menu, and typing /finance/invoices shows the 403 page.
  • admin.search?q=REV-000123 for a stock manager returns no CRM result.
  • The permission sweep reports zero mismatches on the default matrix.

Data — Permission, RolePermission, UserRole, Session; application logs.

Interfaces

  • Screens: BO-03 App shell: navigation by role, global search, notifications
  • API: admin.search GET /admin/search

Error codes

CodeHTTPRaised when
PERMISSION_DENIED403The user's roles lack the operation's permission
OUT_OF_SCOPE403A write references a record outside the delegate's or reseller's scope
NOT_FOUND404The record does not exist or is outside the reader's scope
ROLE_PROTECTED409Deleting a system role, or editing SUPER_ADMIN, DELEGATE or RESELLER permissions
LAST_SUPER_ADMIN409The change would leave no active super administrator
ROLE_INCOMPATIBLE422A staff role given to a delegate or reseller user (or the reverse)
PERMISSION_NOT_GRANTABLE422roles.administer or users.administer granted to a role other than SUPER_ADMIN

On this page