Administration
This page covers instance administration in Iskue: how the first administrator account comes into being, how global, project and organisation roles interact, day-to-day user management (including deactivation and the last-admin guard), permission schemes, the audit log, request rate limiting, and the admin-only areas of the web UI. All enforcement is server-side; every admin API sits behind @PreAuthorize("hasRole('ADMIN')").
The first user becomes the administrator
Registration is open at POST /api/v1/auth/register (no authentication required). The first account ever registered is created with the global ADMIN role; every subsequent account is created as MEMBER. The same rule applies when the first sign-in arrives through OIDC single sign-on (Google or Microsoft) — whichever path creates the first user row produces the administrator. To bootstrap a fresh install, simply register before sharing the URL:
curl -s -X POST http://localhost:8088/api/v1/auth/register \
-H 'Content-Type: application/json' \
-d '{"email":"admin@example.com","password":"a-strong-password","displayName":"Site Admin"}'
# The response contains {"token":"...","user":{...,"role":"ADMIN"}} — the first user is ADMIN.The passwordless dev login (app.dev-login.enabled, env APP_DEV_LOGIN_ENABLED) seeds and signs in an ADMIN account with the fixed email dev-admin@issuehub.local, and re-promotes it to ADMIN on every use. It is off by default and enabled only in the throwaway distributed-cluster demo (deploy/docker-compose.yml sets APP_DEV_LOGIN_ENABLED: "true"). Never enable it in production.
Roles
Global roles
| Role | Meaning |
|---|---|
ADMIN | Instance administrator. Bypasses every project, organisation and permission-scheme check, and is the only role that can reach the /api/v1/admin/** APIs and the Administration section of the sidebar. |
MEMBER | The default for every account after the first. Carries no instance-wide rights; effective access comes entirely from project membership, organisation membership and permission schemes. |
GUEST | Assignable from the Users admin page. In the current release no endpoint grants MEMBER anything that GUEST lacks — both are governed by the same membership and scheme checks. Use it as an organisational marker for accounts you intend to keep at minimal access. |
Role changes take effect immediately: the JWT filter re-validates the user row on every request and derives the authority from the live database record, not the token claims. A demoted admin loses ROLE_ADMIN on their very next request, even though session tokens live for JWT_EXPIRATION_MINUTES (default 480).
Project roles
| Project role | Grants (under the default permission scheme) |
|---|---|
ADMIN | Everything, including project settings, deleting tickets, managing versions and components, editing or deleting anyone's comments, and assigning a permission scheme to the project. |
MEMBER | Browse, create/edit/transition/assign tickets, comment, link tickets, manage sprints, edit and delete own comments. |
VIEWER | Read-only: browse the project and its tickets. Any write attempt is refused with "Viewers cannot modify project content". |
Project roles are per-project membership rows, managed by a project admin under the project's settings (POST /api/v1/projects/{key}/members with {"userId":"...","role":"MEMBER"}). The user who creates a project automatically becomes its project ADMIN. Access checks run in a fixed order: the organisation gate first (a non-admin may only touch projects belonging to an organisation they are a member of), then the membership/role check — so a stale membership row can never grant cross-tenant access.
Organisation roles
Organisations (tenants) are managed by instance admins only, at /api/v1/admin/organizations and the Organizations page. Each membership carries an OrgRole of OWNER or MEMBER. Two behaviours to know: the last OWNER of an organisation cannot be removed (409: "Cannot remove the last owner of the organization"), and removing a user from an organisation also strips their project memberships in that organisation's projects. Every new account is automatically enrolled in the built-in Default Organization (slug default, created by migration V52) so fresh users are not locked out of default-org projects.
User management
Open Administration → Users (/admin) to search accounts, change global roles and toggle activation. The page also shows the most recent audit-log entries. The API behind it is GET /api/v1/admin/users (paginated, q searches email and display name, sorted by email, page size capped at 200) and PATCH /api/v1/admin/users/{userId} accepting any subset of displayName, role and active:
# Search users
curl -s 'http://localhost:8088/api/v1/admin/users?q=alice' -H "Authorization: Bearer $TOKEN"
# Promote to admin
curl -s -X PATCH http://localhost:8088/api/v1/admin/users/<user-uuid> \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"role":"ADMIN"}'
# Deactivate
curl -s -X PATCH http://localhost:8088/api/v1/admin/users/<user-uuid> \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"active":false}'Deactivation, not deletion. There is no user-deletion endpoint. Accounts are deactivated instead, which preserves ticket history, comments, worklogs and audit attribution. Deactivation is immediate: the JWT filter rejects tokens whose user row is inactive on the next request, regardless of token expiry, and a deactivated user cannot log in. Reactivating a user occupies a licensed seat again — the request is refused if no seat is available under the current licence.
The last-admin guard. You cannot lock yourself out of administration: any PATCH that would demote or deactivate the last remaining *active* ADMIN is refused with HTTP 409: Cannot demote or deactivate the last active admin. The count considers active admins only, so an instance with one active admin and several deactivated ones is still protected.
Permission schemes
A permission scheme is a named set of grants mapping each of the 20 permission keys (BROWSE_PROJECT, CREATE_TICKET, EDIT_TICKET, DELETE_TICKET, TRANSITION_TICKET, ASSIGN_TICKET, ASSIGNABLE, ADD_COMMENT, EDIT_OWN_COMMENT, EDIT_ALL_COMMENTS, DELETE_OWN_COMMENT, DELETE_ALL_COMMENTS, LINK_TICKET, LOG_WORK, EDIT_ALL_WORKLOGS, DELETE_ALL_WORKLOGS, MANAGE_SPRINTS, MANAGE_VERSIONS, MANAGE_COMPONENTS, ADMINISTER_PROJECT) to one or more grantees. Every project references exactly one scheme; a project without an explicit assignment falls back to the default scheme.
| Grantee type | Applies to |
|---|---|
PROJECT_ROLE | Members holding the named role — a built-in role (ADMIN/MEMBER/VIEWER) or a custom role defined at /api/v1/admin/custom-roles. |
GROUP | Every member of the named user group. |
USER | One specific user. |
REPORTER | The reporter of the ticket in context (no effect on project-level checks). |
ASSIGNEE | The current assignee of the ticket in context. |
ANYONE_LOGGED_IN | Any authenticated user (still subject to tenant isolation). |
A built-in Default Permission Scheme is seeded by migration V55__permission_schemes.sql with the fixed id 11111111-1111-1111-1111-111111111111; its grants reproduce the project-role rules in the table above exactly, so projects on the default scheme behave identically to the role model. Manage schemes at Administration → Permission schemes (/admin/permission-schemes) or via /api/v1/admin/permission-schemes (instance admin only). A newly created scheme is seeded with a copy of the default scheme's grants, so you start from the safe baseline and adjust. The default scheme cannot be deleted, and a scheme still referenced by any project cannot be deleted until those projects are reassigned.
Assigning a scheme to a project is a project-admin operation, not an instance-admin one: GET /api/v1/projects/{projectKey}/permission-scheme shows the effective scheme, GET .../permission-scheme/options lists assignable schemes, and PUT .../permission-scheme applies one. Scheme creation, edits, grant changes and assignments are all recorded in the configuration audit trail under the PERMISSION_SCHEME category.
Two rules override every grant: a global ADMIN always passes, and a restricted ticket blocks every scheme grant for anyone who is not its reporter, its assignee or a project ADMIN — even ANYONE_LOGGED_IN. No grant of any type can admit a user from another organisation.
Audit log
All auditing lands in a single Postgres table, audit_log (a Citus reference table on the distributed cluster), with columns actor_id, action, entity_type, entity_id, detail (capped at 500 characters), project_id and created_at, indexed by time, action, entity type and project. It records who, when and what — not before/after diffs. Two streams share the table, distinguished by project_id: account-security events and instance-level config changes (schemes, groups, organisations, licences, email templates) keep it NULL; project-scoped configuration changes carry the affected project's id, and only those non-NULL rows appear in the Config audit view.
| Account-security action | Recorded when |
|---|---|
USER_REGISTERED | An account is created (detail notes the assigned role). |
USER_LOGIN | A successful password login, or a completed 2FA login (detail notes "logged in with 2FA"). |
DEV_LOGIN | The passwordless dev admin signs in (dev environments only). |
MFA_ENABLED / MFA_DISABLED | A user enables or disables TOTP two-factor authentication. |
USER_SSO_LINKED | An existing local account is linked to an SSO provider. |
ADMIN_USER_UPDATED | An admin changes a user's role, display name or active flag (detail records the resulting role and active state). |
The configuration audit trail covers project and instance configuration: ticket types, workflow statuses and transitions, custom fields, automation rules, SLA policies, integrations, screens, boards, permission and notification schemes, groups, organisations, licences and more — 37 categories in total, each change recorded with one of the actions CREATE, UPDATE, DELETE, REORDER, SET_INITIAL, ARCHIVE, UNARCHIVE, CONNECT, DISCONNECT, MEMBER_ADDED, MEMBER_REMOVED. Browse project-scoped changes at Administration → Config audit (/admin/config-audit), filterable by project, category, action, actor and date range; instance-level changes (schemes, groups, organisations, licences) are recorded without a project id and appear only in the audit stream at the bottom of the Users page (/api/v1/admin/audit), not in the Config audit view. The account-security stream appears at the bottom of the Users page.
# Account-security stream, newest first; optional ?action= filter
curl -s 'http://localhost:8088/api/v1/admin/audit?action=USER_LOGIN' -H "Authorization: Bearer $TOKEN"
# Config trail, filtered; all parameters optional
curl -s 'http://localhost:8088/api/v1/admin/config-audit?projectKey=ENG&category=PERMISSION_SCHEME&action=UPDATE' \
-H "Authorization: Bearer $TOKEN"
# The filterable category/action values, for scripting
curl -s http://localhost:8088/api/v1/admin/config-audit/filters -H "Authorization: Bearer $TOKEN"Iskue never deletes audit rows — there is no built-in retention job. If your compliance policy requires pruning or archival, do it directly in Postgres, e.g. docker exec issuehub-postgres psql -U issuehub -d issuehub -c "DELETE FROM audit_log WHERE created_at < now() - interval '2 years'" (on the distributed cluster, run it against issuehub-cl-coordinator instead).
User onboarding
Onboarding is self-service sign-up only: there is no invite or password-reset flow yet, and SSO provisions accounts only when an OIDC provider is configured. APP_REGISTRATION_ENABLED=false closes sign-up entirely — recommended for any internet-reachable instance once your team is in — and GET /api/v1/auth/registration tells the SPA whether to show the sign-up link. Flip it back on briefly when someone new joins.
Rate limiting
A bucket4j filter applies fixed one-minute windows as defence in depth, keyed by request path. Pre-authentication surfaces are limited per client IP; the general API is limited per user (falling back to per-IP for anonymous requests). Limits are configured through app.ratelimit.* properties, overridable by environment variables in the usual Spring form — the deploy cluster does exactly that with APP_RATELIMIT_AUTH_PER_MIN and APP_RATELIMIT_API_PER_MIN.
| Scope | Property | Default | Keyed by |
|---|---|---|---|
/api/v1/auth/** (login, register, MFA) | app.ratelimit.auth-per-min | 10 | Client IP |
/api/v1/portal/** (public customer portal) | app.ratelimit.portal-per-min | 60 | Client IP |
/api/v1/email/webhook/**, /api/v1/git/webhook/** | app.ratelimit.webhook-per-min | 120 | Client IP |
| Everything else | app.ratelimit.api-per-min | 240 | Authenticated user id (IP when anonymous) |
Requests over the limit receive HTTP 429 with the body {"status":429,"message":"Too many requests — slow down"}. Long-lived SSE streams (paths ending /live) are exempt, so live updates do not consume budget. By default the filter keys on the real socket address and ignores X-Forwarded-For, because the header is client-controlled; when Iskue sits behind a trusted reverse proxy that sets it, enable app.ratelimit.trust-forwarded-for=true (default false) so distinct clients are limited individually rather than collapsing into the proxy's address. The client entry is taken app.ratelimit.trusted-proxy-hops positions (default 1) from the right of the header, so addresses a client fabricates at the front can never displace it.
# Dev/e2e-friendly rate limits. The production defaults (10 auth / 240 api per minute) are
# tuned per real user, but the Playwright suite funnels ~50 specs through one dev-admin user
# and one client IP, which trips 429s mid-suite as the suite grows. Keep abuse protection on,
# just with headroom for test traffic.
APP_RATELIMIT_AUTH_PER_MIN: "200"
APP_RATELIMIT_API_PER_MIN: "2000"Buckets are held in memory per backend instance. Behind the load-balanced cluster, the effective ceiling is roughly the configured limit multiplied by the number of backend replicas, and limits reset when a backend restarts. Move to a shared (e.g. Redis-backed) bucket store if you need exact global limits across nodes.
Admin-only areas in the UI
The sidebar shows an Administration group only when the signed-in user's global role is ADMIN. It is collapsed by default (the state persists per browser) and forced open while you are on any /admin route. Entries whose feature is above the licensed edition, or whose backing flag is off, are hidden. Hiding the menu is a courtesy, not the control: every entry below is backed by an admin-gated controller, so a non-admin calling those APIs directly is refused regardless of what the UI shows.
| Sidebar entry | Route | Notes |
|---|---|---|
| Users | /admin | User search, role changes, activation; recent audit entries below. |
| Groups | /admin/groups | User-group CRUD and membership; groups feed GROUP grants in permission schemes. Deleting a group removes its memberships; grants naming it simply stop matching anyone. |
| Teams & ARTs | /admin/teams | Shown only with the SAFe licence feature. |
| ART dashboard | /art-dashboard | Shown only with the SAFe licence feature. |
| Global automation | /admin/automation | Instance-wide automation rules. |
| Field schemes | /admin/field-schemes | Custom-field scheme administration. |
| Licensing | /admin/licensing | Licence key and seat status. |
| Organizations | /admin/organizations | Tenant management: organisations, members, owner roles. |
| Cluster status | /admin/cluster | Backend replica identity/health (/api/v1/admin/cluster). |
| Config audit | /admin/config-audit | Filterable configuration audit trail. |
| Plugins | /admin/plugins | Shown only with the plugins licence feature. |
| Permission schemes | /admin/permission-schemes | Scheme and grant editor; custom roles. |
| Notification schemes | /admin/notification-schemes | Per-event notification recipient rules (/api/v1/admin/notification-schemes). |
| Email templates | /admin/email-templates | Shown only with the email-templates licence feature. |
| Import from Jira | /admin/jira-import | Shown only when APP_JIRA_IMPORT_ENABLED is true (default false). |