APPSEC/i — Multi-Tenant Application Security & Entitlements

SteelFrame X application operation manual  ·  ← back to Operation Manuals  ·  Sign On

APPSEC/i is a multi-tenant SaaS application-security and entitlements platform: it registers tenants and provisions a private SQL schema for each, manages roles and per-user role assignments, gates feature access through per-tenant entitlements, vetoes cross-tenant writes, and audits every privilege change. It is a pure SQL PL application — every rule lives in DB2 for i functions, stored procedures, statement/row triggers, a global variable, distinct types, sequences and security views; there is no 5250 screen and no RPG driver. The application is reached through CALL from STRSQL (or any SQL client) and is host-orchestrated by a driver that runs the provisioning script. This manual is the reference for the operator who provisions tenants and grants access, and for the developer maintaining the security layer. It is grounded entirely in the committed source (sqlpl-app-sec2/src/schema.sql, routines.sql, seed.mjs, and the test/appsec_daily.mjs driver).

Honest scope note carried throughout. APPSEC/i models application-level security in data: tenants, roles, user–role assignments and feature entitlements are ordinary tables, and access decisions are made by the SP_CHECK_ACCESS procedure and the cross-tenant veto triggers. The application also exercises the DB2 GRANT/REVOKE privilege surface (table, column, EXECUTE, schema-level, WITH GRANT OPTION) — but those SQL privileges are catalog-visible (they land in SYSTABAUTH/SYSCOLAUTH) and are used here to prove the grant/revoke bookkeeping; they are not necessarily runtime-enforced as an authorization gate by the emulator. Every place this distinction matters is called out inline.

A. Overview & Architecture ↑ top

A.1 What it does

APPSEC/i is the security and entitlements substrate a multi-tenant SaaS platform sits on. Its responsibilities:

A.2 Multi-tenant security model: data-enforced, catalog-visible privileges

The application is deliberately split into two concerns that are important not to confuse:

The benefit for operations: the tenancy boundary and entitlement rules are one auditable place (the SQL-PL routines and triggers), the same procedures are reachable from any SQL client, and the privilege catalog is kept faithfully so an auditor can inspect who was granted what. Everything runs in library / schema APPSEC, with per-tenant data in its own TEN… schema.

A.3 Component & flow

  PROVISIONING            ACCESS DECISION        GRANT / AUDIT
  ------------            ---------------        -------------
  SP_PROVISION_TENANT     SP_CHECK_ACCESS        SP_GRANT_ROLE  --INSERT-->  USERROLE
    -> TENANT row           user disabled?         SP_REVOKE_ROLE --DELETE-->  |
    -> TENANT_SEQ           tenant suspended?                                  v
    -> SECAUDIT             lacks role?          TR_USERROLE_AUDIT_INS/_DEL -> PRIVAUDIT
  (driver) CREATE SCHEMA    not entitled?          (AFTER STATEMENT, transition table)
    -> TEN....TENDATA         -> SIGNAL 75040
    -> GRANT ... TO PUBLIC / QPGMR  (SYSTABAUTH / SYSCOLAUTH)

  SESSION CONTEXT                        CROSS-TENANT VETO
  --------------                         -----------------
  SET APPSEC.CURRENT_TENANT = 'TENnnn'   TR_RESOURCE_TENANT_VETO   (BEFORE INSERT, 75030)
  UPDATE APPSEC.SESSION_CTX (lockstep)   TR_RESOURCE_TENANT_VETO_UPD (BEFORE UPDATE, 75031)
        |                                        ^
        +--- driver keeps both in sync ---------+ (trigger reads SESSION_CTX, see D)

  SET PATH = PATHA, PATHB  -->  unqualified FN_ENTITLEMENT_SOURCE() resolves PATHA (first wins)

A single access event flows: the caller sets the session's current tenant (SET APPSEC.CURRENT_TENANT plus the lockstep SESSION_CTX update) → issues a write to APPSEC.RESOURCE → the BEFORE trigger compares the row's TENANT_ID against the session context and either allows it or raises 75030/75031. A role change flows: SP_GRANT_ROLE/SP_REVOKE_ROLE mutates USERROLE → the AFTER statement trigger writes a PRIVAUDIT row per affected assignment through the transition table.

A.4 Object inventory

ObjectTypeRole
TENANTIDDistinct typeVARCHAR(10) tenant-id domain (WITH COMPARISONS).
ROLEIDDistinct typeVARCHAR(10) role-id domain (unrelated to TENANTID).
TENANT_SEQ / USER_SEQSequenceTenant-id / user-id generators.
CURRENT_TENANTGlobal variableSession current-tenant context (access predicate).
TENANTTableTenant registry (id, name, per-tenant schema, status).
ROLETableRole catalog.
APPUSERTableUsers, each scoped to a tenant.
USERROLETableUser–role assignments (the grant log).
FEATURETableFeature catalog.
TENANT_ENTITLETableWhich features a tenant is entitled to.
RESOURCETableTenant-scoped business object (veto target).
PRIVAUDITTablePrivilege-change audit (grant/revoke).
SESSION_CTXTableOne-row current-tenant context read by the veto triggers.
SECAUDITTableGeneral security-event audit (provisioning).
V_ACTIVE_ENTITLEMENTSViewActive tenants ⨯ enabled entitlements (CREATE OR REPLACE).
V_USER_ROLESViewActive users ⨯ their roles.
SP_* (5)SQL proceduresProvision / deprovision / grant / revoke / check-access.
TR_* (4)TriggersAudit (2, transition table) + cross-tenant veto (2).
FN_ENTITLEMENT_SOURCESQL function ×2PATHA/PATHB SET PATH resolution probe.

The full catalogue is 2 distinct types + 2 sequences + 1 global variable, over 10 APPSEC tables, 2 views, 5 procedures, 4 triggers and a pair of same-named functions in schemas PATHA/PATHB, plus one TENDATA table per provisioned tenant schema. Sections D and F expand each.

B. Online / Access Surface ↑ top

B.1 The STRSQL CALL surface (no 5250 screen)

Honest statement: APPSEC/i has no 5250 display file, no subfile, and no interactive RPG program. It is a pure SQL PL application. The operator equivalent of "open a screen" is "start an SQL session and CALL a procedure". On IBM i that is STRSQL (or Run SQL Scripts / any SQL client); in the tested environment the driver runs the same statements in-process through executeSqlBatch / callProc. Before invoking anything, the job's library list must include APPSEC — the tested job runs with LIBL = QSYS QGPL APPSEC QTEMP and CURLIB = APPSEC under user QSECOFR.

To do thisType in STRSQL / an SQL session
Provision (or find) a tenant + its schemaCALL APPSEC.SP_PROVISION_TENANT('Acme Corp','TENACME',?,?,?) then the driver's CREATE SCHEMA step
Deprovision a tenant schemaCALL APPSEC.SP_DEPROVISION_TENANT('TENACME')
Grant a role to a userCALL APPSEC.SP_GRANT_ROLE('UACME01','ROLADMIN','SYSTEM',?)
Revoke a roleCALL APPSEC.SP_REVOKE_ROLE('UACME01','ROLADMIN',?)
Make an access decisionCALL APPSEC.SP_CHECK_ACCESS('UACME01','ROLADMIN','RESOURCE',?,?)
Set the session's current tenantSET APPSEC.CURRENT_TENANT = 'TEN0000001' (and the lockstep SESSION_CTX UPDATE)
Read the entitlement gate viewSELECT * FROM APPSEC.V_ACTIVE_ENTITLEMENTS

Because the OUT parameters (tenant id, return code, message) are what the caller inspects, an interactive STRSQL user passes host markers (?) for them and reads the returned values; a scripted/driver caller reads them out of the call result. There is no menu, no F-key, and no screen to refresh — the "result" of every action is the procedure's OUT return code plus the rows it leaves in the catalog and audit tables (section E shows the verification queries).

Several statements in this application are host-orchestrated, not routine-issued, by design. The per-tenant CREATE SCHEMA is issued by the caller immediately after SP_PROVISION_TENANT returns a fresh tenant (confirmed platform gap SQLPL-PLAT-APPSEC-03: CREATE SCHEMA is wired up only in the top-level batch executor, not the routine dispatcher). Likewise the two probe schemas PATHA/PATHB are created once from the driver. Operationally this means "provision a tenant" is a two-step call sequence, documented in E.1.

B.2 The security model — GRANT/REVOKE granularities, SET PATH, audit

APPSEC/i's controls are its subject matter. This section is the operator-facing summary of the security model the application enforces and records; F.5–F.7 give the developer-level detail.

Access decision (live, data-enforced)

SP_CHECK_ACCESS(userid, roleid, featcd) is the single gate. It returns cleanly (P_RC = 0, 'access granted') only when all four conditions hold, and otherwise SIGNALs condition C_DENIED (SQLSTATE 75040) with a specific message. The checks, in the order they run (short-circuiting on the first failure):

CheckDenial message on failure
User existsunknown user
User is active (USTAT='A')user is disabled
Tenant is active (TSTAT='A')tenant is suspended
User holds the required role (USERROLE)user lacks required role
Tenant is entitled to the feature (ENSTAT='Y')tenant not entitled to feature

A denial surfaces to the caller as a failed CALL (non-zero code), never a silent grant — the driver proves each denial path explicitly.

Cross-tenant isolation (live, trigger-enforced)

The session's current tenant is held in the global variable APPSEC.CURRENT_TENANT and, in lockstep, in the one-row APPSEC.SESSION_CTX table. When a current tenant is set, any INSERT or UPDATE on APPSEC.RESOURCE whose TENANT_ID does not match is refused by the BEFORE triggers with SQLSTATE 75030 (insert) or 75031 (update); a same-tenant write is allowed. With no current tenant set (NULL), the veto is inert and all writes pass. See D and F.4 for the documented reason the trigger reads SESSION_CTX rather than the global variable directly.

GRANT/REVOKE granularities (catalog-visible bookkeeping)

The application issues, and reads back, DB2 privileges at every granularity. These are recorded in the authorization catalog; they are the auditable record of who was granted what:

GranularityExample statementCatalog
Table privilegeGRANT SELECT, INSERT ON APPSEC.RESOURCE TO QPGMRSYSTABAUTH
Column UPDATEGRANT UPDATE (SENSITIVE, RESNM) ON APPSEC.RESOURCE TO QPGMRSYSCOLAUTH
Column REFERENCESGRANT REFERENCES (RESID) ON APPSEC.RESOURCE TO QPGMRSYSCOLAUTH
EXECUTE on procedureGRANT EXECUTE ON PROCEDURE APPSEC.SP_CHECK_ACCESS TO QPGMR WITH GRANT OPTIONSYSTABAUTH (OBJECT_TYPE='PROCEDURE')
EXECUTE on functionGRANT EXECUTE ON FUNCTION APPSEC.FN_ENTITLEMENT_SOURCE TO PUBLICSYSTABAUTH
Schema privilegesGRANT CREATEIN, ALTERIN, DROPIN ON SCHEMA TENACME TO QPGMR WITH GRANT OPTIONSYSTABAUTH (OBJECT_TYPE='SCHEMA')
Grant to PUBLICGRANT SELECT ON TENDATA TO PUBLIC (inside the per-tenant CREATE SCHEMA)SYSTABAUTH (GRANTEE='*PUBLIC')

The DB2 revoke rules are exercised and honoured in the catalog: a column-scoped REVOKE UPDATE (SENSITIVE) leaves the other UPDATE column intact; a bare REVOKE UPDATE (no column list) clears all remaining UPDATE column rows for that grantee but leaves a different privilege type (REFERENCES) and the table-level INSERT grant untouched; a re-GRANT of the identical privilege to the identical grantee does not duplicate the row; REVOKE EXECUTE clears the procedure row.

Enforcement caveat (carried from A.2). The GRANT/REVOKE surface above is verified for catalog correctness — the rows land, revoke rules apply, WITH GRANT OPTION is recorded as IS_GRANTABLE='YES'. This is the privilege bookkeeping. It is not a claim that the emulator refuses ordinary DML at runtime for a grantee lacking the privilege. Runtime authorization in APPSEC/i is the data-enforced model above (SP_CHECK_ACCESS + cross-tenant veto), which the driver proves takes effect.

SET PATH (unqualified routine resolution)

Two identically-named functions FN_ENTITLEMENT_SOURCE() live in schemas PATHA and PATHB, each returning its own schema tag. A qualified call always resolves to the named schema; an unqualified call resolves by the current PATHSET PATH = PATHA, PATHB makes the unqualified call return PATHA, and re-ordering to SET PATH = PATHB, PATHA flips it to PATHB. The CURRENT PATH register reflects the reordered list. This is how per-tenant schema resolution order is expressed. See F.6.

C. Batch / Cycle Procedures ↑ top

APPSEC/i has no periodic accounting "close" — it is an operational security platform, so its "cycle" is the provisioning & entitlement lifecycle: deploy the schema and routines, provision tenants, seed roles/features, grant and revoke, check access, and (when a tenant leaves) deprovision. All of it is driven by CALL against the SQL-PL procedures and the security triggers that fire off the resulting table changes. There is no control row and no parameterless SBMJOB idiom — every action is an explicit call with its own parameters.

-- deploy (idempotent for OR-REPLACE objects; TYPE/TABLE/SEQ/VAR fail "already exists" on re-run)
RUNSQLSTM SRCFILE(APPSEC/SRC) SRCMBR(SCHEMA)     -- schema.sql
RUNSQLSTM SRCFILE(APPSEC/SRC) SRCMBR(ROUTINES)   -- routines.sql

-- provision a tenant (two-step: procedure, then host-issued CREATE SCHEMA)
CALL APPSEC.SP_PROVISION_TENANT('Acme Corp','TENACME',?,?,?);
CREATE SCHEMA TENACME
  CREATE TABLE TENDATA (TID VARCHAR(10) NOT NULL, TVAL VARCHAR(60))
  GRANT SELECT ON TENDATA TO PUBLIC;
GRANT CREATEIN, ALTERIN, DROPIN ON SCHEMA TENACME TO QPGMR WITH GRANT OPTION;

C.1 Full procedure / trigger table

ObjectPurposeCalls / firesInputsOutputs / effectsIdempotency
SP_PROVISION_TENANT Register a tenant (sequence-generated id) if the name is new. NEXT VALUE FOR TENANT_SEQ; INSERT TENANT + SECAUDIT. P_TENANTNM, P_SCHEMANM. OUT P_TENANTID, P_RC (0 fresh / 1 existed / 9 error), P_MSG; TENANT + SECAUDIT rows. Idempotent by tenant name (second call RC=1, no dup row).
(driver) CREATE SCHEMA Stand up the per-tenant schema + TENDATA + PUBLIC/QPGMR grants. Top-level CREATE SCHEMA / GRANT (host-orchestrated). Schema name; only on a FRESH provision (RC=0). TEN… schema, TENDATA table, SYSTABAUTH grant rows. Only issued when SP_PROVISION_TENANT reports fresh, so not re-run for an existing tenant.
SP_DEPROVISION_TENANT Tear a tenant schema down (DROP SCHEMA CASCADE). EXECUTE IMMEDIATE DROP SCHEMA … CASCADE, guarded by a SYSSCHEMAS existence probe. P_SCHEMANM. Schema and its contents removed. Idempotent (IF-EXISTS guard; re-run on a gone schema is a clean no-op).
SP_GRANT_ROLE Assign a role to a user. Guarded INSERT into USERROLE → fires TR_USERROLE_AUDIT_INS. P_USERID, P_ROLEID, P_GRANTEDBY. OUT P_RC (0 granted / 1 already granted); USERROLE row; PRIVAUDIT ROLE_GRANT row. Idempotent (existence check; a no-op re-grant does NOT re-fire the trigger).
SP_REVOKE_ROLE Remove a role assignment. Guarded DELETE from USERROLE → fires TR_USERROLE_AUDIT_DEL. P_USERID, P_ROLEID. OUT P_RC (0 revoked / 1 nothing to revoke); PRIVAUDIT ROLE_REVOKE row. Idempotent (a no-op re-revoke adds no audit row).
SP_CHECK_ACCESS The entitlement gate (allow / SIGNAL denial). SELECT INTO over APPUSER/TENANT/USERROLE/TENANT_ENTITLE; SIGNAL 75040 on any failure. P_USERID, P_ROLEID, P_FEATCD. OUT P_RC=0 + 'access granted', or a failed CALL with a denial message. Read-only (no state change); always safe to re-run.
TR_USERROLE_AUDIT_INS Audit every role grant. AFTER INSERT ON USERROLE, FOR EACH STATEMENT, REFERENCING NEW TABLE. Transition table NT. One PRIVAUDIT ROLE_GRANT row per inserted assignment (set-based INSERT..SELECT). Fires once per statement; multi-row batches audited in one INSERT.
TR_USERROLE_AUDIT_DEL Audit every role revoke. AFTER DELETE ON USERROLE, FOR EACH STATEMENT, REFERENCING OLD TABLE. Transition table OT. One PRIVAUDIT ROLE_REVOKE row per deleted assignment. Fires once per statement.
TR_RESOURCE_TENANT_VETO Refuse a cross-tenant INSERT. BEFORE INSERT ON RESOURCE, FOR EACH ROW, WHEN SESSION_CTX set & mismatched. NEW row TENANT_ID vs SESSION_CTX.CUR_TENANT. SIGNAL SQLSTATE 75030; the row never lands. Deterministic per row.
TR_RESOURCE_TENANT_VETO_UPD Refuse a cross-tenant UPDATE. BEFORE UPDATE ON RESOURCE, FOR EACH ROW, WHEN OLD row not owned by current tenant. OLD row TENANT_ID vs SESSION_CTX.CUR_TENANT. SIGNAL SQLSTATE 75031; the row is unchanged. Deterministic per row.

C.2 Provisioning & grant/revoke detail

Provisioning — SP_PROVISION_TENANT

The procedure counts existing tenants of the given name. If one exists it returns that tenant's id with P_RC=1 ('tenant already provisioned'); otherwise it draws the next TENANT_SEQ value, formats a TEN… id ('TEN' || LPAD(CHAR(seq),7,'0'), e.g. TEN0000001), inserts the TENANT row and a PROVISION_OK SECAUDIT row, and returns P_RC=0. An EXIT HANDLER traps any SQLEXCEPTION, sets P_RC=9, captures the message via GET DIAGNOSTICS, and writes a PROVISION_FAIL SECAUDIT row. The per-tenant CREATE SCHEMA is then issued by the caller only when RC=0 (fresh), so a re-provision of the same name touches neither the catalog nor the schema.

Expected result (Acme, fresh then re-run):
  call 1:  P_TENANTID = TEN0000001,  P_RC = 0  ('provisioned')
  call 2:  P_TENANTID = TEN0000001,  P_RC = 1  ('tenant already provisioned')
  invariant: exactly one TENANT row for 'Acme Corp'

Grant / revoke — SP_GRANT_ROLE / SP_REVOKE_ROLE + the audit triggers

Both procedures guard their mutation with an existence check, so a re-grant (P_RC=1) and a re-revoke (P_RC=1) are clean no-ops. The audit is trigger-driven, not coded in the procedure: the AFTER-statement triggers use transition tables (NEW TABLE / OLD TABLE) to write one PRIVAUDIT row per affected assignment in a single set-based INSERT…SELECT. Because they are statement-level and only fire when a row actually changes, an idempotent no-op re-grant does not add a spurious audit row.

Expected (grant ROLADMIN to UACME01, then re-grant):
  grant 1:  P_RC = 0        1 USERROLE row, 1 PRIVAUDIT ROLE_GRANT row
  grant 2:  P_RC = 1        no new USERROLE row, no new audit row
  PRIVAUDIT ROLE_GRANT for UACME01/ROLADMIN carries EVTBY = 'SYSTEM' (from GRANTEDBY, via NT)

Access check — SP_CHECK_ACCESS

The gate short-circuits on the first failing condition (see B.2). A denial is a SIGNAL of condition C_DENIED (75040), which surfaces to the caller as a non-zero call code plus the denial message — never a silent grant. Only when unknown-user, disabled-user, suspended-tenant, missing-role and un-entitled-feature all pass does it set P_RC=0 / 'access granted'.

Deprovisioning — SP_DEPROVISION_TENANT

Probes QSYS2.SYSSCHEMAS for the schema and, if present, issues DROP SCHEMA … CASCADE via EXECUTE IMMEDIATE (which, unlike CREATE SCHEMA, does run from inside a routine body). A DROP SCHEMA … RESTRICT against a non-empty per-tenant schema is refused ("not empty"); CASCADE removes the schema and its contents. Note this drops only the tenant's schema — it deliberately does not delete the APPSEC.TENANT registry row (a real platform would archive that separately).

C.3 Ordering & dependencies

  • schema.sql before routines.sql. The routines reference the tables, distinct types, sequences, the global variable and SESSION_CTX declared in schema.sql; deploy the schema first.
  • PATHA/PATHB schemas before routines.sql. The two FN_ENTITLEMENT_SOURCE functions target schemas PATHA/PATHB, which the driver creates once before loading the routines (they cannot be created idempotently inside the reloadable routines file).
  • Seed roles/features before grants and access checks. SP_GRANT_ROLE references a ROLE; SP_CHECK_ACCESS references a FEATURE entitlement. Load seed.mjs (3 roles, 3 features, the empty SESSION_CTX row) first.
  • Provision before grant. A user is scoped to a tenant (APPUSER.TENANT_ID), so the tenant must exist before its users and their role grants.
  • Set the session context before a tenant-scoped write. The veto only fires when SESSION_CTX.CUR_TENANT is set; keep CURRENT_TENANT and SESSION_CTX in lockstep (the driver always updates both together).

D. Data Files (data dictionary) ↑ top

All core objects are in schema/library APPSEC, grounded in schema.sql. Dates are stored as INT in YYYYMMDD form; the tenant/role id columns use the distinct types APPSEC.TENANTID / APPSEC.ROLEID (both over VARCHAR(10)). Per-tenant data lives in each tenant's own TEN… schema, not in APPSEC.

TENANT — Tenant registry (PK TENANT_ID)

FieldTypeMeaning
TENANT_IDTENANTID (VARCHAR(10))Tenant id (PK), e.g. TEN0000001 (sequence-generated).
TENANTNMVARCHAR(60)Tenant name; provisioning is idempotent by this name.
SCHEMANMVARCHAR(18)The per-tenant SQL schema/library name (e.g. TENACME).
TSTATCHAR(1)A active, S suspended (a suspended tenant fails SP_CHECK_ACCESS).
CRTDATEINTCreated date (YYYYMMDD).

ROLE — Role catalog (PK ROLE_ID)

FieldTypeMeaning
ROLE_IDROLEID (VARCHAR(10))Role id (PK), e.g. ROLADMIN.
ROLENMVARCHAR(40)Role name.
RDESCVARCHAR(100)Description.

Seeded roles: ROLADMIN (Administrator), ROLVIEWER (Viewer), ROLEDITOR (Editor).

APPUSER — Users (PK USERID; scoped to a tenant)

FieldTypeMeaning
USERIDVARCHAR(10)User id (PK).
TENANT_IDTENANTIDOwning tenant.
USERNMVARCHAR(60)User name.
USTATCHAR(1)A active, D disabled (a disabled user fails SP_CHECK_ACCESS).

USERROLE — User–role assignments (PK USERID, ROLE_ID) — the grant log

FieldTypeMeaning
USERID / ROLE_IDVARCHAR(10) / ROLEIDThe assignment (PK).
GRANTEDBYVARCHAR(10)Who granted it (flows to PRIVAUDIT.EVTBY via the trigger).
GRANTDATEINTGrant date (YYYYMMDD).

INSERT here fires TR_USERROLE_AUDIT_INS; DELETE fires TR_USERROLE_AUDIT_DEL.

FEATURE — Feature catalog (PK FEATCD)

FieldTypeMeaning
FEATCDCHAR(8)Feature code (PK), e.g. RESOURCE, REPORTS  , AUDITLOG.
FEATNMVARCHAR(60)Feature name.

Because FEATCD is CHAR(8), a shorter code is space-padded — the seed uses 'REPORTS ' (trailing blank).

TENANT_ENTITLE — Per-tenant entitlements (PK TENANT_ID, FEATCD)

FieldTypeMeaning
TENANT_ID / FEATCDTENANTID / CHAR(8)Tenant + feature (PK).
ENSTATCHAR(1)Y enabled, N disabled. SP_CHECK_ACCESS requires an existing row with ENSTAT='Y'.

RESOURCE — Tenant-scoped business object (PK RESID) — the veto target

FieldTypeMeaning
RESIDVARCHAR(12)Resource id (PK).
TENANT_IDTENANTIDOwning tenant — compared to the session context by the veto triggers.
RESNMVARCHAR(60)Resource name.
SENSITIVEVARCHAR(60)The column that receives column-level UPDATE/REFERENCES grants.
RSTATCHAR(1)Status (default A).

PRIVAUDIT — Privilege-change audit (PK AUDID identity)

FieldTypeMeaning
AUDIDINT identityGenerated-always audit id (PK).
EVTTYPEVARCHAR(12)ROLE_GRANT or ROLE_REVOKE.
USERID / ROLE_IDVARCHAR(10) / ROLEIDThe affected assignment.
EVTBYVARCHAR(10)Who did it (GRANTEDBY on grant; NULL on revoke).
EVTDATEINTEvent date (YYYYMMDD).
EVTTIMETIMESTAMPDefault CURRENT_TIMESTAMP (underscore spelling — see note below).

SESSION_CTX — Session current-tenant context (one row, PK CTXKEY='X')

FieldTypeMeaning
CTXKEYCHAR(1)Always 'X' (PK, single row).
CUR_TENANTVARCHAR(10)The current tenant the veto triggers read.
The cross-tenant veto triggers read this table, not the APPSEC.CURRENT_TENANT global variable directly. This is a documented app-level workaround for a confirmed platform gap (SQLPL-PLAT-APPSEC-05): a trigger's WHEN clause (and the other non-exec() SQL-PL read paths) cannot resolve a CREATE VARIABLE global-variable reference. The driver keeps SESSION_CTX in exact lockstep with CURRENT_TENANT on every set. The global variable is still exercised for real through the working top-level batch path (SET / VALUES APPSEC.CURRENT_TENANT).

SECAUDIT — General security-event audit (PK AUDID identity)

FieldTypeMeaning
AUDIDINT identityGenerated-always id (PK).
EVTTYPEVARCHAR(20)e.g. PROVISION_OK / PROVISION_FAIL.
REFDOCVARCHAR(20)Reference (schema/tenant id).
EVTTEXTVARCHAR(200)Human-readable detail.
EVTDATE / EVTTIMEINT / TIMESTAMPWhen (date + default CURRENT_TIMESTAMP).

V_ACTIVE_ENTITLEMENTS / V_USER_ROLES — security views

V_ACTIVE_ENTITLEMENTS joins TENANT ⨯ TENANT_ENTITLE ⨯ FEATURE, filtered to TSTAT='A' and ENSTAT='Y' — the effective entitlement gate list. V_USER_ROLES joins APPUSER ⨯ USERROLE ⨯ ROLE for active users. Both are deployed via CREATE OR REPLACE VIEW, so the daily deploy can redeploy them idempotently without dropping dependents.

TENANTID / ROLEID — distinct types; TENANT_SEQ / USER_SEQ — sequences; CURRENT_TENANT — global variable

CREATE TYPE APPSEC.TENANTID AS VARCHAR(10) WITH COMPARISONS and APPSEC.ROLEID similarly — two unrelated distinct types over the identical source, used to probe whether cross-type assignment is rejected (see F.1). TENANT_SEQ / USER_SEQ are AS INT START WITH 1 INCREMENT BY 1 NO CYCLE generators. CURRENT_TENANT is a CREATE VARIABLE APPSEC.CURRENT_TENANT TENANTID DEFAULT NULL global (the type is spelled unqualified as an app-level workaround for SQLPL-PLAT-APPSEC-01).

Per-tenant schema — TEN….TENDATA

Each provisioned tenant gets its own SQL schema named by SCHEMANM, created (by the driver) with a contained TENDATA (TID VARCHAR(10), TVAL VARCHAR(60)) table and a GRANT SELECT ON TENDATA TO PUBLIC, plus a schema-level GRANT CREATEIN, ALTERIN, DROPIN … TO QPGMR WITH GRANT OPTION. This is where a tenant's own data would live, isolated from every other tenant's schema.

Relationships

  • APPUSER.TENANT_ID → TENANT.TENANT_ID (users are tenant-scoped).
  • USERROLE.(USERID, ROLE_ID) → APPUSER.USERID / ROLE.ROLE_ID (assignments).
  • TENANT_ENTITLE.(TENANT_ID, FEATCD) → TENANT / FEATURE (entitlements).
  • RESOURCE.TENANT_ID → TENANT.TENANT_ID (tenant-scoped object, veto-guarded).
  • PRIVAUDIT / SECAUDIT are append-only audit logs (identity PK, no per-row FK).
  • SESSION_CTX is a standalone one-row context; CURRENT_TENANT is the parallel global variable.
  • TENANT.SCHEMANM names the per-tenant schema holding TENDATA.
Platform-workaround defaults documented in the source. Two column defaults use the underscore spelling DEFAULT CURRENT_TIMESTAMP (not the two-word DEFAULT CURRENT TIMESTAMP) as an app-level workaround for a confirmed platform gap (SQLPL-PLAT-APPSEC-02). These are honest, source-documented accommodations; operationally the columns default to the insert time as described.

E. Operations Runbook ↑ top

E.1 Provision a tenant

  1. Ensure the platform is deployed: schema.sql then routines.sql loaded, the PATHA/PATHB probe schemas created, and the seed run (3 roles, 3 features, the empty SESSION_CTX row).
  2. Call the procedure (two-step provisioning — the CREATE SCHEMA is host-issued only on a fresh provision):
CALL APPSEC.SP_PROVISION_TENANT('Acme Corp','TENACME',?,?,?);
-- if the OUT P_RC came back 0 (fresh), issue the per-tenant schema:
CREATE SCHEMA TENACME
  CREATE TABLE TENDATA (TID VARCHAR(10) NOT NULL, TVAL VARCHAR(60))
  GRANT SELECT ON TENDATA TO PUBLIC;
GRANT CREATEIN, ALTERIN, DROPIN ON SCHEMA TENACME TO QPGMR WITH GRANT OPTION;

Verify:

  • A tenant id came back, e.g. TEN0000001; P_RC=0 ('provisioned') on a fresh name, P_RC=1 ('tenant already provisioned') on a repeat.
  • Exactly one registry row: SELECT COUNT(*) FROM APPSEC.TENANT WHERE TENANTNM='Acme Corp' → 1.
  • The schema exists: SELECT COUNT(*) FROM QSYS2.SYSSCHEMAS WHERE SCHEMA_NAME='TENACME' → 1, and SELECT COUNT(*) FROM TENACME.TENDATA answers (0 rows).
  • The grants are catalog-visible: the PUBLIC SELECT on TENDATA in SYSTABAUTH, and the schema-level CREATEIN/ALTERIN/DROPIN for QPGMR with IS_GRANTABLE='YES' (for a schema grant, the schema name lands in SYSTABAUTH.TABLE_NAME with OBJECT_TYPE='SCHEMA').
Provisioning is idempotent by tenant name and the sequence never collides: provisioning ten tenants yields ten distinct TEN… ids and ten schemas. A whole-platform redeploy (reload schema + routines + re-seed + re-provision) is safe end to end; the only errors on a naive schema re-run are the expected "already exists" (SQL0601) on the non-OR-REPLACE creates (the two types, two sequences, one variable, and ten tables), while the CREATE OR REPLACE VIEWs reload clean.

E.2 Grant / verify access

  1. Add the user (scoped to the tenant): INSERT INTO APPSEC.APPUSER (USERID, TENANT_ID, USERNM, USTAT) VALUES ('UACME01','TEN0000001','Alice','A').
  2. Grant a role: CALL APPSEC.SP_GRANT_ROLE('UACME01','ROLADMIN','SYSTEM',?) → expect P_RC=0.
  3. Entitle the tenant to a feature: INSERT INTO APPSEC.TENANT_ENTITLE (TENANT_ID, FEATCD, ENSTAT) VALUES ('TEN0000001','RESOURCE','Y').
  4. Make the access decision: CALL APPSEC.SP_CHECK_ACCESS('UACME01','ROLADMIN','RESOURCE',?,?) → expect P_RC=0, 'access granted'.

Negative paths to confirm the gate really gates (each returns a failed CALL with the named message, never a silent grant):

ScenarioExpected denial message
User lacks the required roleuser lacks required role
Feature entitlement disabled (ENSTAT='N')tenant not entitled to feature
Disabled user (USTAT='D')user is disabled
Unknown userunknown user
Suspended tenant (TSTAT='S')tenant is suspended

Cross-tenant write test — set the session context and prove isolation:

SET APPSEC.CURRENT_TENANT = 'TEN0000001';
UPDATE APPSEC.SESSION_CTX SET CUR_TENANT = 'TEN0000001' WHERE CTXKEY='X';
-- same-tenant INSERT is allowed:
INSERT INTO APPSEC.RESOURCE (RESID,TENANT_ID,RESNM,SENSITIVE) VALUES ('RES00001','TEN0000001','Acme Widget','secret-a');
-- cross-tenant INSERT is VETOED (SQLSTATE 75030); the row never lands:
INSERT INTO APPSEC.RESOURCE (RESID,TENANT_ID,RESNM,SENSITIVE) VALUES ('RES00002','TEN0000002','Globex Widget','secret-b');

Audit verification: after N grants and M revokes, SELECT COUNT(*) FROM APPSEC.PRIVAUDIT WHERE EVTTYPE='ROLE_GRANT' = N and ...='ROLE_REVOKE' = M — an idempotent no-op grant/revoke adds nothing. The grant's EVTBY equals the GRANTEDBY supplied to SP_GRANT_ROLE.

E.3 Reconciling & re-run rules

Every procedure returns an OUT return code; a healthy call ends P_RC=0 (acted) or P_RC=1 (idempotent no-op). A denial or error surfaces as a non-zero call code with a message (section F.7).

SituationBehaviourAction
Re-provision same tenant nameReturns the existing id, P_RC=1; no dup row, no dup schema.Safe no-op. Idempotent by name.
Provision failureEXIT HANDLER sets P_RC=9, writes a PROVISION_FAIL SECAUDIT row with the captured message.Read the SECAUDIT row, fix the cause, re-run.
Re-grant same user/roleP_RC=1; no USERROLE row, no new PRIVAUDIT row (trigger does not re-fire).Safe no-op.
Re-revoke (nothing to revoke)P_RC=1; no new audit row.Safe no-op.
SP_CHECK_ACCESS denialSIGNAL 75040; call code non-zero, message names the reason; no state changed.Correct the user/role/entitlement/tenant status and retry.
Cross-tenant writeTrigger SIGNAL 75030/75031; the row never lands / is unchanged.Set the correct current tenant (both CURRENT_TENANT and SESSION_CTX) and retry.
DROP SCHEMA RESTRICT on a non-empty tenant schemaRefused ("not empty"); the schema stays.Use SP_DEPROVISION_TENANT (CASCADE) to tear it down.
Deprovision an already-gone schemaIF-EXISTS guard makes it a clean no-op.Safe. Note the TENANT registry row persists (schema-only deprovision).
Naive schema.sql re-runExactly the non-OR-REPLACE creates fail "already exists" (SQL0601); views reload clean.Expected. Use the redeploy drill; no other error type should appear.
Because every role change is journaled to PRIVAUDIT (grant/revoke, by whom) and every provisioning event to SECAUDIT, the security posture is fully reconstructable after the fact for reconciliation and audit.

F. Developer Reference ↑ top

The complete SQL-PL and privilege surface, from schema.sql and routines.sql. Core objects are in schema APPSEC; the resolution-probe functions are in PATHA / PATHB.

F.1 Distinct types, sequences, global variable

CREATE TYPE APPSEC.TENANTID AS VARCHAR(10) WITH COMPARISONS
Distinct type for a tenant id, so cross-tenant assignment is at least nominally type-checked. Used as the column type of TENANT.TENANT_ID, APPUSER.TENANT_ID, RESOURCE.TENANT_ID, etc.
CREATE TYPE APPSEC.ROLEID AS VARCHAR(10) WITH COMPARISONS
A second, unrelated distinct type over the identical source. It exists so a probe can attempt to flow a ROLEID value into a TENANTID position and observe whether the engine rejects the cross-type assignment. Documented deviation: strong typing / CAST identity is not modeled by the platform (SQLPL-COVERAGE.md section E), so the assignment is accepted rather than rejected — the app proves that boundary rather than assuming it.
CREATE SEQUENCE APPSEC.TENANT_SEQ / USER_SEQ AS INT START WITH 1 INCREMENT BY 1 NO CYCLE
Id generators. SP_PROVISION_TENANT draws NEXT VALUE FOR APPSEC.TENANT_SEQ and formats 'TEN' || LPAD(CHAR(seq),7,'0'). Sequence-generation is what makes repeated provisioning collision-free.
CREATE VARIABLE APPSEC.CURRENT_TENANT TENANTID DEFAULT NULL
The session current-tenant context. Source-documented workaround (SQLPL-PLAT-APPSEC-01): the CREATE VARIABLE parser does not accept a schema-qualified distinct-type name in the type position, so the type is spelled unqualified (TENANTID, not APPSEC.TENANTID). The variable reads/writes correctly through the top-level batch path (SET / VALUES APPSEC.CURRENT_TENANT); the veto triggers read the parallel SESSION_CTX table instead (see F.4).
Catalog visibility (proved by the driver): the sequence appears in QSYS2.SYSSEQUENCES, the variable in QSYS2.SYSVARIABLES, and both distinct types in QSYS2.SYSTYPES with SOURCE_TYPE='VARCHAR(10)'.

F.2 Functions (2 — the SET PATH probe)

PATHA.FN_ENTITLEMENT_SOURCE () RETURNS CHAR(5) LANGUAGE SQL RETURN 'PATHA'
Returns the literal tag 'PATHA'.
PATHB.FN_ENTITLEMENT_SOURCE () RETURNS CHAR(5) LANGUAGE SQL RETURN 'PATHB'
Returns the literal tag 'PATHB'. Two identically-named functions in two schemas, used to prove unqualified-call resolution follows SET PATH (F.6). Both are CREATE OR REPLACE FUNCTION, so they reload idempotently; only their containing schemas are non-idempotent (driver-created once).

F.3 Procedures (5)

SP_PROVISION_TENANT (IN P_TENANTNM VARCHAR(60), IN P_SCHEMANM VARCHAR(18); OUT P_TENANTID TENANTID, OUT P_RC INT, OUT P_MSG VARCHAR(100))
Idempotent tenant registration. If a tenant of that name exists → returns its id, P_RC=1. Otherwise draws TENANT_SEQ, inserts the TENANT + a PROVISION_OK SECAUDIT row, P_RC=0. An EXIT HANDLER FOR SQLEXCEPTION sets P_RC=9, captures the message via GET DIAGNOSTICS CONDITION 1 ... = MESSAGE_TEXT, and writes a PROVISION_FAIL SECAUDIT row. Does not itself issue CREATE SCHEMA (SQLPL-PLAT-APPSEC-03: CREATE SCHEMA is not recognized by the routine dispatcher; the caller issues it top-level on a fresh provision).
SP_DEPROVISION_TENANT (IN P_SCHEMANM VARCHAR(18))
Probes QSYS2.SYSSCHEMAS; if the schema exists, EXECUTE IMMEDIATE 'DROP SCHEMA ' || P_SCHEMANM || ' CASCADE'. DROP SCHEMA does run from inside a routine (unlike CREATE SCHEMA). Idempotent via the existence probe.
SP_GRANT_ROLE (IN P_USERID VARCHAR(10), IN P_ROLEID VARCHAR(10), IN P_GRANTEDBY VARCHAR(10); OUT P_RC INT)
Existence-guarded INSERT into USERROLE; P_RC=0 granted, 1 already granted. The INSERT fires TR_USERROLE_AUDIT_INS. Workaround (SQLPL-PLAT-APPSEC-04): P_ROLEID is declared VARCHAR(10), not the ROLEID distinct type, because a CALL argument bound to a character-sourced distinct-type IN parameter is spuriously rejected with SQL0302 (callProc0's isCharType() inspects the raw declared-type text rather than the resolved source type).
SP_REVOKE_ROLE (IN P_USERID VARCHAR(10), IN P_ROLEID VARCHAR(10); OUT P_RC INT)
Existence-guarded DELETE from USERROLE; P_RC=0 revoked, 1 nothing to revoke. Fires TR_USERROLE_AUDIT_DEL. Same VARCHAR(10) workaround on P_ROLEID.
SP_CHECK_ACCESS (IN P_USERID VARCHAR(10), IN P_ROLEID VARCHAR(10), IN P_FEATCD CHAR(8); OUT P_RC INT, OUT P_MSG VARCHAR(100))
The entitlement gate. Declares condition C_DENIED FOR SQLSTATE '75040' and SELECT INTOs the user (tenant + status), tenant status, role count and entitlement status, SIGNAL C_DENIEDing with a specific MESSAGE_TEXT at the first failure: unknown user → disabled user → suspended tenant → missing role → un-entitled feature. Clean pass → P_RC=0, 'access granted'. Same VARCHAR(10) workaround on P_ROLEID.

F.4 Triggers (4) & views

TR_USERROLE_AUDIT_INS — AFTER INSERT ON USERROLE, FOR EACH STATEMENT, REFERENCING NEW TABLE AS NT
BEGIN ATOMIC INSERT INTO PRIVAUDIT (...) SELECT 'ROLE_GRANT', NT.USERID, NT.ROLE_ID, NT.GRANTEDBY, NT.GRANTDATE FROM NT; — one audit row per inserted assignment in a single set-based statement (proves multi-row transition-table batching, not FOR EACH ROW).
TR_USERROLE_AUDIT_DEL — AFTER DELETE ON USERROLE, FOR EACH STATEMENT, REFERENCING OLD TABLE AS OT
INSERT ... SELECT 'ROLE_REVOKE', OT.USERID, OT.ROLE_ID, NULL, 20260807 FROM OT; — one ROLE_REVOKE row per deleted assignment (EVTBY NULL on revoke).
TR_RESOURCE_TENANT_VETO — BEFORE INSERT ON RESOURCE, FOR EACH ROW
WHEN a current tenant is set in SESSION_CTX AND the new row's TENANT_ID does not match → SIGNAL SQLSTATE '75030' ("cross-tenant write refused"). Reads SESSION_CTX, not the global variable (below).
TR_RESOURCE_TENANT_VETO_UPD — BEFORE UPDATE ON RESOURCE, FOR EACH ROW
WHEN the existing row (OLD.TENANT_ID) is not owned by the current tenant → SIGNAL SQLSTATE '75031'.
V_ACTIVE_ENTITLEMENTS / V_USER_ROLES — CREATE OR REPLACE VIEW
The active-entitlement gate list and the active-user role list (see D). Redeployable idempotently.
Why the veto triggers read SESSION_CTX (SQLPL-PLAT-APPSEC-05). A trigger's WHEN clause — and every SQL-PL read-context except plExecStmt's exec() path (SELECT INTO, cursor queries, scalar IF-conditions, WHEN clauses) — cannot resolve a CREATE VARIABLE global-variable reference; they route through host.query()/host.eval()/whenPasses(), none of which apply gvarRewrite. A WHEN clause referencing the global variable is silently never true (whenPasses fails closed on the throw — which for a VETO trigger means it would fail open, a genuine security-relevant finding). The app therefore points the WHEN clause at the real one-row SESSION_CTX table (ordinary table reads work correctly), and the driver keeps SESSION_CTX in exact lockstep with CURRENT_TENANT. This is honestly documented in the source; operationally the veto behaves as described (a cross-tenant row is refused, a same-tenant row is allowed).

F.5 GRANT/REVOKE granularity surface

Every DB2 privilege granularity the application exercises, with the catalog it lands in and the behaviour the driver verifies. All of this is catalog bookkeeping (A.2 enforcement caveat); the runtime authorization gate is the data-enforced model in F.3–F.4.

StatementCatalog row observedVerified behaviour
GRANT SELECT, INSERT ON APPSEC.RESOURCE TO QPGMRSYSTABAUTH (TABLE_SCHEMA='APPSEC', GRANTEE='QPGMR')Table-level privilege lands.
GRANT UPDATE (SENSITIVE, RESNM) ON APPSEC.RESOURCE TO QPGMRSYSCOLAUTH, one row per column, PRIVILEGE_TYPE='UPDATE'Both columns appear.
GRANT REFERENCES (RESID) ON APPSEC.RESOURCE TO QPGMRSYSCOLAUTH, COLUMN_NAME='RESID', PRIVILEGE_TYPE='REFERENCES'Column REFERENCES lands.
REVOKE UPDATE (SENSITIVE) ON APPSEC.RESOURCE FROM QPGMRSYSCOLAUTH now shows only RESNM for UPDATEColumn-scoped revoke removes just that column; the other UPDATE column and REFERENCES survive.
REVOKE UPDATE ON APPSEC.RESOURCE FROM QPGMRSYSCOLAUTH UPDATE rows → 0 for QPGMRBare revoke (no column list) clears ALL remaining UPDATE columns (the DB2 rule); REFERENCES and table-level INSERT untouched.
GRANT EXECUTE ON PROCEDURE APPSEC.SP_CHECK_ACCESS TO QPGMR WITH GRANT OPTIONSYSTABAUTH OBJECT_TYPE='PROCEDURE', IS_GRANTABLE='YES'EXECUTE-on-procedure + grant option recorded; a re-grant does NOT duplicate the row.
GRANT EXECUTE ON FUNCTION APPSEC.FN_ENTITLEMENT_SOURCE TO PUBLICSYSTABAUTH GRANTEE='*PUBLIC'EXECUTE-on-function to PUBLIC recorded.
REVOKE EXECUTE ON PROCEDURE APPSEC.SP_CHECK_ACCESS FROM QPGMRSYSTABAUTH EXECUTE row → 0Revoke clears the procedure privilege.
GRANT CREATEIN, ALTERIN, DROPIN ON SCHEMA TENACME TO QPGMR WITH GRANT OPTIONSYSTABAUTH OBJECT_TYPE='SCHEMA', TABLE_NAME='TENACME'All three schema privileges, IS_GRANTABLE='YES'. For a schema grant the schema NAME lands in TABLE_NAME (not TABLE_SCHEMA).
GRANT SELECT ON TENDATA TO PUBLIC (inside CREATE SCHEMA)SYSTABAUTH TABLE_SCHEMA='TENACME', GRANTEE='*PUBLIC'Contained-element PUBLIC grant lands with the schema.

The revoke rules proven, in words: a column-scoped revoke removes only the named columns; a bare REVOKE UPDATE removes all remaining UPDATE columns for the grantee; a different privilege type (REFERENCES) and the table-level grant survive an UPDATE revoke; and re-granting the identical privilege to the identical grantee is a no-op in the catalog (no duplicate row).

F.6 SET PATH resolution

StatementUnqualified FN_ENTITLEMENT_SOURCE() resolves to
VALUES PATHA.FN_ENTITLEMENT_SOURCE()PATHA (qualified — always, regardless of PATH)
VALUES PATHB.FN_ENTITLEMENT_SOURCE()PATHB (qualified)
SET PATH = PATHA, PATHBPATHA (first schema in the path wins)
SET PATH = PATHB, PATHAPATHB (re-ordering flips resolution)
VALUES CURRENT PATH"PATHB","PATHA" (the register reflects the reordered list)

This is the mechanism per-tenant schema resolution order would ride on: an unqualified routine name binds to the first schema on the PATH that defines it, and SET PATH re-orders which wins.

F.7 Application SQLSTATE table

SQLSTATERaised byMeaning
75030TR_RESOURCE_TENANT_VETOCross-tenant INSERT refused: resource TENANT_ID does not match CURRENT_TENANT.
75031TR_RESOURCE_TENANT_VETO_UPDCross-tenant UPDATE refused: existing resource does not belong to CURRENT_TENANT.
75040SP_CHECK_ACCESS (condition C_DENIED)Access denied. The MESSAGE_TEXT names the specific reason: unknown user / user is disabled / tenant is suspended / user lacks required role / tenant not entitled to feature.
Return-code conventions (not SQLSTATEs). The procedures also signal outcome through their OUT P_RC: 0 = acted (provisioned / granted / revoked / access granted), 1 = idempotent no-op (already existed / already granted / nothing to revoke), 9 = SP_PROVISION_TENANT caught an SQLEXCEPTION (details in the PROVISION_FAIL SECAUDIT row).

G. Glossary ↑ top

Distinct type
A user-defined type over a built-in source type (here TENANTID / ROLEID over VARCHAR(10)). Nominally scopes a domain; note the platform does not model strong CAST identity, so cross-type assignment is accepted, not rejected.
Entitlement
A per-tenant feature grant (TENANT_ENTITLE, ENSTAT='Y'). A tenant can act on a feature only when entitled, checked by SP_CHECK_ACCESS.
Global variable (CREATE VARIABLE)
A session-scoped SQL variable (APPSEC.CURRENT_TENANT) holding the current-tenant context. Kept in lockstep with the SESSION_CTX table the veto triggers read.
GRANT / REVOKE
The DB2 statements that record privileges in the authorization catalog. In APPSEC/i they are proved for catalog correctness at every granularity (table, column, EXECUTE, schema, PUBLIC, WITH GRANT OPTION); they are the auditable record of who was granted what, not the runtime authorization gate.
Idempotent
Safe to run again with the same result. Provisioning (by name), grant, revoke and deprovision are all idempotent; the whole platform re-deploy is safe end to end.
Multi-tenant / tenant
One customer of the SaaS platform. Each tenant has a registry row (TENANT) and a private SQL schema (SCHEMANM); cross-tenant writes are vetoed.
Role & user–role assignment
A named privilege bundle (ROLE) and its assignment to a user (USERROLE), granted/revoked through procedures and audited by triggers.
SET PATH
The SQL statement that orders which schemas resolve an unqualified routine name; the first schema on the path that defines the name wins.
SIGNAL / RESIGNAL
SQL-PL statements that raise an SQLSTATE condition. SP_CHECK_ACCESS SIGNALs 75040 on denial; the veto triggers SIGNAL 75030/75031.
SQLSTATE / SQLCODE
SQL return-status fields. A clean call returns normally with P_RC 0 or 1; a denial or veto surfaces as a failed call carrying an application SQLSTATE (F.7).
STRSQL
Start SQL Interactive Session — the IBM i interactive SQL entry point. APPSEC/i has no 5250 screen; it is driven by CALL from STRSQL or any SQL client.
SYSTABAUTH / SYSCOLAUTH
The DB2 for i catalog views recording table/routine/schema and column privileges. APPSEC/i reads them back to prove GRANT/REVOKE bookkeeping.
Transition table (NEW TABLE / OLD TABLE)
The set of rows an INSERT/DELETE affected, referenced by a FOR-EACH-STATEMENT trigger. The audit triggers use them to write one audit row per affected assignment in a single set-based statement.
Cross-tenant veto
The BEFORE-trigger rule that refuses a write to a resource whose tenant does not match the session's current tenant (SQLSTATE 75030/75031).