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:

Bootstrap the first admin on the single-node stack (http://localhost:8088)
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

RoleMeaning
ADMINInstance 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.
MEMBERThe default for every account after the first. Carries no instance-wide rights; effective access comes entirely from project membership, organisation membership and permission schemes.
GUESTAssignable 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 roleGrants (under the default permission scheme)
ADMINEverything, including project settings, deleting tickets, managing versions and components, editing or deleting anyone's comments, and assigning a permission scheme to the project.
MEMBERBrowse, create/edit/transition/assign tickets, comment, link tickets, manage sprints, edit and delete own comments.
VIEWERRead-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:

List and update users (replace $TOKEN with an admin session token)
# 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 typeApplies to
PROJECT_ROLEMembers holding the named role — a built-in role (ADMIN/MEMBER/VIEWER) or a custom role defined at /api/v1/admin/custom-roles.
GROUPEvery member of the named user group.
USEROne specific user.
REPORTERThe reporter of the ticket in context (no effect on project-level checks).
ASSIGNEEThe current assignee of the ticket in context.
ANYONE_LOGGED_INAny 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 actionRecorded when
USER_REGISTEREDAn account is created (detail notes the assigned role).
USER_LOGINA successful password login, or a completed 2FA login (detail notes "logged in with 2FA").
DEV_LOGINThe passwordless dev admin signs in (dev environments only).
MFA_ENABLED / MFA_DISABLEDA user enables or disables TOTP two-factor authentication.
USER_SSO_LINKEDAn existing local account is linked to an SSO provider.
ADMIN_USER_UPDATEDAn 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.

Query the audit APIs (admin token required)
# 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.

ScopePropertyDefaultKeyed by
/api/v1/auth/** (login, register, MFA)app.ratelimit.auth-per-min10Client IP
/api/v1/portal/** (public customer portal)app.ratelimit.portal-per-min60Client IP
/api/v1/email/webhook/**, /api/v1/git/webhook/**app.ratelimit.webhook-per-min120Client IP
Everything elseapp.ratelimit.api-per-min240Authenticated 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.

deploy/docker-compose.yml — raised limits for the cluster's e2e traffic (verbatim)
  # 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 entryRouteNotes
Users/adminUser search, role changes, activation; recent audit entries below.
Groups/admin/groupsUser-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/teamsShown only with the SAFe licence feature.
ART dashboard/art-dashboardShown only with the SAFe licence feature.
Global automation/admin/automationInstance-wide automation rules.
Field schemes/admin/field-schemesCustom-field scheme administration.
Licensing/admin/licensingLicence key and seat status.
Organizations/admin/organizationsTenant management: organisations, members, owner roles.
Cluster status/admin/clusterBackend replica identity/health (/api/v1/admin/cluster).
Config audit/admin/config-auditFilterable configuration audit trail.
Plugins/admin/pluginsShown only with the plugins licence feature.
Permission schemes/admin/permission-schemesScheme and grant editor; custom roles.
Notification schemes/admin/notification-schemesPer-event notification recipient rules (/api/v1/admin/notification-schemes).
Email templates/admin/email-templatesShown only with the email-templates licence feature.
Import from Jira/admin/jira-importShown only when APP_JIRA_IMPORT_ENABLED is true (default false).