permissions.md 5.81 KB

How to set permissions

xly's permission model has two complementary tables. They answer different questions and live at different layers; reading the wiki correctly here matters because the names look superficially similar.

The two tables

Table Granularity What it stores Loaded when
gdsjurisdiction per module the catalog of actions/buttons that exist on each module (BtnAdd, BtnUpd, BtnDel, …) every getModelBysId call (skipped for ADMIN — see Slice 1)
sysjurisdiction per role (and optionally per user) the grants — which role (or user) can perform which action on which module resolved on getModelBysId for the user's role; BusinessGdsconfigformsServiceImpl.getJurisdictionData()

The mental model: gdsjurisdiction is the menu of permission items the framework knows about per module. sysjurisdiction is the seating chart — who's allowed to use which item.

What gdsjurisdiction contains

A tree of permission items keyed by module:

Column Meaning
sParentId the gdsmodule.sId this permission item belongs to
sName the action key — e.g., BtnAdd, BtnUpd, BtnDel, BtnExport
sChinese / sEnglish / sBig5 display label (e.g., 新增)
iOrder sort order in the permission UI
sBrandsId / sSubsidiaryId tenant scope

In the current live DB there are 4,335 gdsjurisdiction rows covering 892 distinct modules and 186 distinct action names. gdsjurisdiction does not contain a role column or a user column — it is purely a catalog.

What sysjurisdiction contains

The actual grants:

Column Meaning
sParentId the module sId the grant is for
sParent2Id the parent module (group/category) — used by the permission UI for tree rendering
sJurisdictionClassifyId role / permission-class id (FK to sisjurisdictionclassify)
sUserId user id, for per-user overrides — empty means "role-level grant"
sAction the granted action — e.g., BtnBsOperation.BtnDesignFunction
sKey composite path key, encoding the module hierarchy that owns the action

In the current live DB there are 28,045 sysjurisdiction rows, all of them role-level (every sUserId is empty) — 22 roles in sisjurisdictionclassify × the modules and actions each role can use.

The runtime in BusinessGdsconfigformsServiceImpl.getJurisdictionData (line 212) chooses between user-level and role-level grants:

Boolean hasCheckUser = checkUserJurisdiction(map);
if (hasCheckUser) {
    return this.getGroupJurisdictionUserNew(map);  // per-user overrides
} else {
    return this.getGroupJurisdictionNew(map);      // role-only fallback
}

If the calling user has any row in sysjurisdiction with their sUserId set, the user path wins; otherwise the role-only path is used. In the present dev DB, the user path is unreachable.

How permissions are loaded on a request

Pseudocode for the relevant slice of BusinessBaseServiceImpl.getModelBysId (xlyBusinessService/.../BusinessBaseServiceImpl.java:196-198):

List<Map<String, Object>> jList = new ArrayList<>();
if (!UserType.ADMIN.getTypeName().equals(map.get("sUserType"))) {
    jList = businessGdsconfigformsService.getJurisdictionData(qMap);
}
returnMap.put("gdsjurisdiction", jList);

ADMIN bypasses permissions entirely and gets an empty grant list, which the SPA interprets as "show every button". Non-admin users get the filtered list of actions resolvable for their role + module + tenant. The SPA enables/disables buttons and toggles columns based on what came back.

How to grant a permission

For a new module, the typical path is:

  1. Define the module + form (recipe).
  2. Insert gdsjurisdiction rows for each button/action the module exposes (BtnAdd, BtnUpd, BtnExport, …). One row per action; sParentId = your module's sId.
  3. Decide which roles get which actions, and insert sysjurisdiction rows: sParentId = module sId, sJurisdictionClassifyId = role sId, sAction = action key. Leave sUserId empty for role-level grants.
  4. (Or override at the user level by inserting rows with sUserId set; per-user overrides shadow the role-level grants for that user.)
  5. Save through BACK so cache invalidation propagates (the runtime caches getJurisdictionData per (sUserId, sModelsId, sBrandsId, sSubsidiaryId) — see the @Cacheable annotation at line 209).

ADMIN as a special case

The hardcoded ADMIN bypass is a real escape hatch. Every deployment has at least one ADMIN account by convention. From a security audit perspective, ADMIN access should be tightly controlled and audited externally — the framework offers no within-app constraint on what an ADMIN can do.

Cross-tenant boundary is not permissions

Worth saying explicitly: gdsjurisdiction + sysjurisdiction enforce within-tenant permissions. The cross-tenant boundary (one company can't read another company's rows) is enforced by the multi-tenant scoping — automatic sBrandsId/sSubsidiaryId injection — not by jurisdiction rules.

Note on plat_base_authority_*

Three tables under plat_base_authority (plat_base_authority, plat_base_authority_button_type, plat_base_authority_data_type) live in the schema and look like they ought to be lookups for button-type and data-permission-type identifiers. All three are empty in the live DB; they belong to the out-of-scope xlyPlat* B2B platform layer, not to the framework's own permission flow. Don't write referencing them when documenting framework permissions — the jurisdiction surface above is self-contained.