# 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`](../../auto-catalog/tables/gdsjurisdiction.md) | per **module** | the **catalog** of actions/buttons that exist on each module (`BtnAdd`, `BtnUpd`, `BtnDel`, …) | every `getModelBysId` call (skipped for ADMIN — see [Slice 1](../../slices/01-hello-world.md)) | | [`sysjurisdiction`](../../auto-catalog/tables/sysjurisdiction.md) | 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: ```java 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`): ```java List> 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](define-form.md)). 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](../../concepts/multi-tenancy.md) — 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.