# 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`, `BtnPrint`, …) | 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`, `BtnPrint` |
| `sChinese` / `sEnglish` / `sBig5` | display label (e.g., `新增`) |
| `iOrder` | sort order in the permission UI |
| `sBrandsId` / `sSubsidiaryId` | tenant scope |

`gdsjurisdiction` does **not** contain a role column or a user column —
it is purely a catalog of which buttons/actions exist for each module.
Every module that wants any permission control has rows here.

## 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 |

`sysjurisdiction` is the volume table of the permission system: one
grant per (role-or-user × module × action). Roles live in
`sisjurisdictionclassify`. A deployment may use only role-level grants
(every `sUserId` empty), only user-level grants, or a mix.

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.

## How permissions are loaded on a request

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

```java
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](define-form.md)).
2. Insert `gdsjurisdiction` rows for each button/action the module
   exposes (`BtnAdd`, `BtnUpd`, `BtnPrint`, …). 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`)
exist in the schema and look like they ought to be lookups for
button-type and data-permission-type identifiers. 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.
