Permission & notification schemes
Iskue decides who may do what through three stacked layers: global roles, per-project roles, and a per-project permission scheme of granular grants. A parallel notification scheme decides who is told when something happens on a ticket. Both kinds of scheme are defined once, org-wide, by a global administrator, and assigned to projects individually.
How the layers stack
Global roles (ADMIN, MEMBER, GUEST) live on the user account; a global ADMIN bypasses every project gate and every scheme check. Project roles (ADMIN, MEMBER, VIEWER) live on the project membership row and drive the coarse gates: any member may read, members other than VIEWER may write peripheral project content, and only a project ADMIN may administer the project. The permission scheme then decides granular, per-action rights on top of those gates.
| Step | Check | Outcome |
|---|---|---|
| 1 | Global role | A global ADMIN passes unconditionally. |
| 2 | Organisation isolation | A principal outside the project's organisation is denied. No grant type can override this — not even ANYONE_LOGGED_IN. |
| 3 | Scheme resolution | The project's assigned scheme is used, else the scheme flagged is_default. If neither exists, the check denies. |
| 4 | Restricted-ticket gate | When the ticket in scope is restricted, every grant is blocked unless the principal is its reporter, its assignee or a project ADMIN. |
| 5 | Grant matching | Allow if any grant for the requested permission key matches the principal; otherwise deny. |
Many write paths run a membership gate *before* the scheme check — adding a comment, for instance, first requires a non-VIEWER project membership. A generous grant such as ANYONE_LOGGED_IN therefore widens access only within what the surrounding gates allow. On paths that check only the scheme (ticket creation is one), it extends to any member of the project's organisation — and never beyond it.
Permission schemes
A scheme is a named set of grants; each grant maps one permission key to one grantee. Schemes live in the permission_scheme and permission_grant tables (Citus reference tables on a cluster). Scheme names are unique case-insensitively, at most 120 characters, with an optional 500-character description. A project points at exactly one scheme via project.permission_scheme_id.
| Grantee type | Parameter | Matches |
|---|---|---|
PROJECT_ROLE | role name | A built-in name (ADMIN/MEMBER/VIEWER) matches the user's membership role only; a custom role name matches the per-project custom-role assignment. |
USER | user id | That specific user. |
GROUP | group id | Every member of the user group — group membership alone confers the grant. |
REPORTER | — | The reporter of the ticket in context. |
ASSIGNEE | — | The current assignee of the ticket in context. |
ANYONE_LOGGED_IN | — | Any authenticated user, still subject to organisation isolation. |
REPORTER and ASSIGNEE grants take effect only when a ticket is in scope. Project-level checks — creating a ticket, managing sprints, versions or components — carry no ticket context, so those two grants never match there.
Permission keys
| Key | Grants the right to |
|---|---|
BROWSE_PROJECT | View the project and its tickets. |
CREATE_TICKET | Create new tickets in the project. |
EDIT_TICKET | Edit ticket fields, rank, and release links (also required to resolve someone else's comment). |
DELETE_TICKET | Permanently delete tickets. |
TRANSITION_TICKET | Move tickets through the workflow. |
ASSIGN_TICKET | Change a ticket's assignee or co-assignees. |
ASSIGNABLE | Be set as a ticket's assignee (evaluated for the target user, not the actor). |
ADD_COMMENT | Comment on tickets. |
EDIT_OWN_COMMENT | Edit comments you authored. |
EDIT_ALL_COMMENTS | Edit any comment. |
DELETE_OWN_COMMENT | Delete comments you authored. |
DELETE_ALL_COMMENTS | Delete any comment, including contact-authored portal comments. |
LINK_TICKET | Create and remove issue links. |
LOG_WORK | Log work and edit or delete your own worklogs. |
EDIT_ALL_WORKLOGS | Edit anyone's worklog. |
DELETE_ALL_WORKLOGS | Delete anyone's worklog. |
MANAGE_SPRINTS | Create, edit, start and complete sprints. |
MANAGE_VERSIONS | Create, edit and release versions. |
MANAGE_COMPONENTS | Create and edit components. |
ADMINISTER_PROJECT | Manage project configuration and settings. |
Three catalogue keys are not yet enforced in the current code: comment editing is author-only regardless of EDIT_OWN_COMMENT/EDIT_ALL_COMMENTS grants, and project administration is gated by the built-in project ADMIN membership role, not by ADMINISTER_PROJECT. Do not rely on granting or revoking these three to change behaviour. Note also that most read endpoints require project membership; BROWSE_PROJECT is additionally checked on content reads such as worklogs, votes, estimation, checklists, presence and favourites.
Defaults when no scheme is assigned
Resolution is: project.permission_scheme_id if set, otherwise the scheme flagged is_default — the seeded Default Permission Scheme (fixed id 11111111-1111-1111-1111-111111111111). If neither exists, every check denies for everyone except global admins. Migration V55__permission_schemes.sql backfills every existing project to the default scheme, and its grants reproduce the legacy project-role rules exactly, so an upgrade changes nothing; the worklog keys are seeded by V57__reconcile_worklog_time_tracking.sql, mirroring the corresponding comment grants.
| Permission key(s) | Default grantees |
|---|---|
BROWSE_PROJECT | Roles ADMIN, MEMBER, VIEWER |
CREATE_TICKET, EDIT_TICKET, TRANSITION_TICKET, ASSIGN_TICKET | Roles ADMIN, MEMBER |
ADD_COMMENT, EDIT_OWN_COMMENT, DELETE_OWN_COMMENT, LINK_TICKET, MANAGE_SPRINTS, LOG_WORK | Roles ADMIN, MEMBER |
DELETE_TICKET, EDIT_ALL_COMMENTS, DELETE_ALL_COMMENTS, EDIT_ALL_WORKLOGS, DELETE_ALL_WORKLOGS | Role ADMIN |
MANAGE_VERSIONS, MANAGE_COMPONENTS, ADMINISTER_PROJECT | Role ADMIN |
ASSIGNABLE | ANYONE_LOGGED_IN |
ASSIGNABLE is evaluated for the user *being assigned*, not for the actor. Under the default grant, any authenticated user in the project's organisation may be set as assignee; global admins are always assignable; users from another organisation never are.
Custom roles
A custom role is an org-admin-defined role *name* usable in PROJECT_ROLE grants alongside the built-ins. Definitions are global (unique name, at most 60 characters, optional 255-character description); assignment is per project and per user, and leaves the built-in membership role untouched — coarse access still comes from ADMIN/MEMBER/VIEWER, while custom roles add scheme-driven rights on top. Creating a custom role with a built-in name is rejected, and a built-in name in a grant only ever matches the membership role, so a custom role cannot shadow MEMBER.
# Define a role (global admin; authentication omitted)
curl -X POST "$ISKUE_URL/api/v1/admin/custom-roles" \
-H 'Content-Type: application/json' \
-d '{"name":"Release Manager","description":"May manage releases"}'
# Assign it to a user in one project (project admin)
curl -X PUT "$ISKUE_URL/api/v1/projects/PROJ/custom-roles/<roleId>/members/<userId>"
# Remove the assignment
curl -X DELETE "$ISKUE_URL/api/v1/projects/PROJ/custom-roles/<roleId>/members/<userId>"Deleting a custom role cascades its per-project assignments; grants that still name it simply stop matching anyone — the same as a role with no members. The /admin/permission-schemes page hosts the custom-role manager alongside the scheme editor, and the grant editor's role dropdown lists built-in roles first, then custom roles.
User groups
Groups feed GROUP grants and GROUP notification rules. user_group holds the definition (unique name, at most 120 characters) and group_membership the user–group pairs. Management is global-admin only under /api/v1/admin/groups: create, rename (PATCH /{id}), add a member (POST /{id}/members with {"userId":"..."}), remove a member (DELETE /{id}/members/{userId}), delete. On the permission side, a GROUP grant applies to every member of the group; membership alone confers the right, with organisation isolation still applied.
The permission-scheme editor
/admin/permission-schemes (global ADMIN only) lists schemes, creates new ones, edits grants and deletes unused schemes. A newly created scheme starts as a copy of the default scheme's grants, so editing begins from the safe baseline. Grant editing is replace-all: the editor submits the complete grant list. GET /api/v1/admin/permission-schemes/catalog supplies the permission keys, grantee types and role names the editor offers.
# Create a scheme — it starts as a copy of the default scheme's grants
curl -X POST "$ISKUE_URL/api/v1/admin/permission-schemes" \
-H 'Content-Type: application/json' \
-d '{"name":"Locked down","description":"Members read, admins write"}'
# Replace the scheme's grants (replace-all semantics)
curl -X PUT "$ISKUE_URL/api/v1/admin/permission-schemes/<schemeId>/grants" \
-H 'Content-Type: application/json' \
-d '{
"grants": [
{"permissionKey":"BROWSE_PROJECT","granteeType":"PROJECT_ROLE","granteeParam":"VIEWER"},
{"permissionKey":"BROWSE_PROJECT","granteeType":"PROJECT_ROLE","granteeParam":"MEMBER"},
{"permissionKey":"BROWSE_PROJECT","granteeType":"PROJECT_ROLE","granteeParam":"ADMIN"},
{"permissionKey":"EDIT_TICKET","granteeType":"PROJECT_ROLE","granteeParam":"Release Manager"},
{"permissionKey":"ADD_COMMENT","granteeType":"REPORTER"},
{"permissionKey":"ASSIGNABLE","granteeType":"ANYONE_LOGGED_IN"}
]
}'Validation on save: a PROJECT_ROLE grant needs a built-in or existing custom role name, USER an existing user id, GROUP an existing group id; REPORTER, ASSIGNEE and ANYONE_LOGGED_IN carry no parameter. Duplicate grants are silently de-duplicated. The default scheme cannot be deleted, nor can any scheme still assigned to a project — reassign those projects first. Every create, rename (PATCH /{id}), grant change and delete is recorded in the configuration audit log.
Assigning a scheme to a project
GET /api/v1/projects/{projectKey}/permission-scheme # active scheme (any project member)
GET /api/v1/projects/{projectKey}/permission-scheme/options # assignable schemes (project admin)
PUT /api/v1/projects/{projectKey}/permission-scheme # body {"schemeId":"..."} (project admin)Notification schemes
A notification scheme maps ticket events to recipients. On each event, the project's scheme is consulted; every matching rule contributes recipients, and the union — de-duplicated, with the acting user removed — is notified. Schemes live in notification_scheme and notification_rule, are managed at /admin/notification-schemes (global ADMIN), and are assigned per project exactly like permission schemes via /api/v1/projects/{projectKey}/notification-scheme (plus /options, PUT to assign). Resolution defaults are identical: the project's notification_scheme_id, else the seeded Default Notification Scheme (fixed id 22222222-2222-2222-2222-222222222222); with neither, scheme-resolved recipients are simply none.
| Event | Meaning | Default recipients |
|---|---|---|
CREATED | Ticket created | None — emitted with no default rule; opt in per scheme. |
UPDATED | Ticket updated | None — emitted with no default rule; opt in per scheme. |
COMMENTED | Comment added | Reporter, current assignee, watchers. |
STATUS_CHANGED | Status changed | Reporter, current assignee, watchers. |
ASSIGNED | Assignee changed | Current assignee. |
MENTIONED | Mentioned | The @mentioned users — intrinsic to the event and always notified regardless of scheme rules. |
SLA_BREACHED | SLA breached | Current assignee. |
| Recipient type | Parameter | Resolves to |
|---|---|---|
CURRENT_ASSIGNEE | — | The ticket's current assignee. |
REPORTER | — | The ticket's reporter. |
WATCHERS | — | Everyone watching the ticket. |
PROJECT_ROLE | projectRole | Project members holding that built-in role. |
SPECIFIC_USER | userId | That user — only if they are a project member. |
GROUP | groupId | Group members who are also project members. |
ALL_PROJECT_MEMBERS | — | Every member of the project. |
Notification PROJECT_ROLE rules accept only the built-in ADMIN/MEMBER/VIEWER — custom roles work in permission grants but not in notification rules. SPECIFIC_USER and GROUP rules are filtered to project members, so a rule cannot leak ticket activity to someone without project visibility.
How the channels are routed
Every resolved recipient always gets an in-app notification row. Email depends on the recipient's own email mode: IMMEDIATE sends one email per event (active accounts only), DAILY folds events into a single daily digest, and NONE stays in-app only. Two per-user switches silence both channels outright: a mute (notification_pref) may target one project, one event type, or both — a null side means "all" — and suppresses matching events entirely, mentions included; an active snooze suppresses everything until its timestamp expires.
# Replace a scheme's rules (replace-all semantics)
curl -X PUT "$ISKUE_URL/api/v1/admin/notification-schemes/<schemeId>/rules" \
-H 'Content-Type: application/json' \
-d '{
"rules": [
{"eventType":"COMMENTED","recipientType":"REPORTER"},
{"eventType":"COMMENTED","recipientType":"WATCHERS"},
{"eventType":"CREATED","recipientType":"PROJECT_ROLE","projectRole":"ADMIN"},
{"eventType":"STATUS_CHANGED","recipientType":"GROUP","groupId":"<groupId>"},
{"eventType":"SLA_BREACHED","recipientType":"SPECIFIC_USER","userId":"<userId>"}
]
}'The same guard rails apply as for permission schemes: GET /api/v1/admin/notification-schemes/catalog lists events, recipient types and roles; a new scheme starts as a copy of the default scheme's rules; SPECIFIC_USER and GROUP parameters must reference existing users and groups; duplicates are de-duplicated; the default scheme and any scheme still assigned to a project cannot be deleted; and all changes are audited.
Restricted tickets and schemes
A ticket flagged restricted (issue security v1) is content-visible only to its reporter, its assignee, project ADMINs and global admins. Everyone else receives a 404 — existence is not confirmed — and the ticket is filtered out of lists, search and content-bearing reports. Inside the permission evaluator, a restricted ticket in scope blocks *every* scheme grant for principals who cannot see it, so no grant — ANYONE_LOGGED_IN, GROUP or otherwise — reaches a restricted ticket. Set the flag at creation (restricted: true; the creator is the reporter and keeps visibility) or toggle it later via a ticket edit, which requires EDIT_TICKET.
Documented v1 boundary: link lists (bare keys), watch state and aggregate-only counts (stats, CFD, throughput, workload) may still reflect a restricted ticket's *existence*; none expose its content. Restriction is orthogonal to permission schemes — a scheme grant can neither widen nor narrow restricted visibility.