Security & Permissions
This document covers the Forge sandboxing model, the lens-level permission system, Jira permission integration, and security boundaries in the Foundation/Lens app.
Forge Sandboxing Model
Section titled “Forge Sandboxing Model”Foundation runs entirely on Atlassian Forge, which provides a sandboxed execution environment:
- Isolated runtime: Backend code runs in Forge’s managed Node.js 22.x container. There is no direct access to servers, file systems, or network resources outside of Forge APIs.
- No self-managed infrastructure: No servers, databases, or storage to secure. Forge SQL and KVS are managed by Atlassian.
- Scoped API access: The app can only access Jira APIs within its declared permission scopes.
- Automatic authentication: User identity (
context.accountId) is provided by the Forge platform, not managed by the app. No session tokens, cookies, or auth flows to implement.
Runtime Constraints
Section titled “Runtime Constraints”| Constraint | Limit |
|---|---|
| Standard function timeout | 25 seconds |
| Async event timeout | 900 seconds (15 minutes) |
| Frontend response payload | 5 MB |
| Forge SQL storage | 1 GiB per installation |
| Forge SQL DML | 150 operations/second |
| Invocations | 1,200/user/min, 5,000/install/min |
Permission Model
Section titled “Permission Model”Foundation implements a two-layer permission model: lens-level permissions (app-managed) and issue-level permissions (Jira-managed).
Lens-Level Permissions (ACL)
Section titled “Lens-Level Permissions (ACL)”Each lens has an owner and optional permission grants. Permissions are stored in the lens_permissions Forge SQL table.
Permission Hierarchy
Section titled “Permission Hierarchy”The edit_generators level was collapsed into edit in schema v30, leaving three explicit levels:
| Level | Capabilities | Notes |
|---|---|---|
owner | Full control (implicit) | Creator of the lens; cannot be revoked |
control | Grant/revoke permissions, delete lens, all below | Highest explicit grant |
edit | Edit hierarchy, inline edit issues, move nodes, create/edit/delete generators, all below | Standard editor |
view | Read-only access to the lens | Lowest explicit grant |
Permissions cascade upward: a user with edit implicitly has view. A user with control has all lower permissions.
Grant Types
Section titled “Grant Types”grantee_type | Description |
|---|---|
user | Specific Atlassian account ID |
group | Jira group name |
role | Jira project role |
everyone | All users on the Jira site (grantee_id is NULL) |
The lens_permissions table enforces a UNIQUE constraint on (lens_id, grantee_type, grantee_id) to prevent duplicate grants.
Permission Check Flow
Section titled “Permission Check Flow”- Resolver receives a request with
context.accountId permission-service.tscallscheckPermission(lensId, accountId, requiredLevel)- Checks if user is the lens owner (implicit
control) - Queries
lens_permissionsfor explicit grants - Resolves group/role membership if applicable
- Returns the user’s effective permission level
Issue-Level Permissions (Jira BROWSE)
Section titled “Issue-Level Permissions (Jira BROWSE)”Users can only see issues they have Jira BROWSE permission for, regardless of their lens-level access.
How It Works
Section titled “How It Works”- When loading a lens view, the backend fetches the hierarchy tree from Forge SQL.
- Before returning rows to the frontend, it filters by Jira BROWSE permission.
- BROWSE permission is checked via batch JQL query: issues the user can’t see are excluded from results.
- Results are cached in Forge KVS for 30 minutes per project per user to avoid redundant API calls.
Cascading Visibility
Section titled “Cascading Visibility”If a parent node is hidden (user lacks BROWSE on the parent issue), all children are also hidden. This prevents information leakage through the tree hierarchy structure.
Fail-Open Strategy
Section titled “Fail-Open Strategy”If the Jira API is unreachable during permission checks, the app assumes all issues are visible. This is a deliberate design choice:
- Rationale: A Jira API outage should not block all users from accessing the app. The risk of briefly showing issues a user shouldn’t see is lower than the cost of a complete app outage.
- Mitigation: BROWSE permission checks are retried before falling back. The 30-minute KVS cache means recent permission decisions are honored even during brief outages.
Manifest Permissions
Section titled “Manifest Permissions”OAuth Scopes
Section titled “OAuth Scopes”The manifest declares these Jira API scopes:
| Scope | Purpose |
|---|---|
read:jira-work | Read issues, projects, boards, sprints |
write:jira-work | Update issues (inline editing, status transitions) |
read:jira-user | Read user profiles (assignee picker, user search) |
read:sprint:jira-software | Read sprint data for sprint dropdown |
read:board-scope:jira-software | Read board scope for project analysis |
read:project:jira | Read project metadata |
storage:app | Access Forge SQL and KVS |
Content Security Policy
Section titled “Content Security Policy”The manifest declares:
permissions: content: styles: - 'unsafe-inline'This is required for:
- Atlassian Design System CSS custom properties (
var(--ds-*)) - AG Grid dynamic inline styles (cell formatting, density, conditional formatting)
Forge enforces script-src 'self' by default for Custom UI apps, preventing script injection. The unsafe-inline for styles only applies to CSS, not JavaScript.
External Fetch Permissions
Section titled “External Fetch Permissions”Backend functions can only make HTTP requests to explicitly declared domains:
permissions: external: fetch: backend: - address: "*.ingest.sentry.io" inScopeEUD: false| Domain | Purpose | EUD Data |
|---|---|---|
*.ingest.sentry.io | Error tracking (Sentry) | No (inScopeEUD: false) |
The inScopeEUD: false declaration indicates that no End User Data is transmitted to this endpoint, which is relevant for Marketplace data handling policies.
Jira API Security
Section titled “Jira API Security”asUser() vs asApp()
Section titled “asUser() vs asApp()”Foundation uses two modes for Jira API calls:
| Mode | When Used | Permission Model |
|---|---|---|
asUser() | Most operations (inline edit, transitions, user search) | Runs with the current user’s Jira permissions |
asApp() | Admin-level operations, background tasks, event handlers | Runs with the app’s system permissions |
asUser() Security Rules
Section titled “asUser() Security Rules”- All
asUser()calls are wrapped in try/catch blocks. - Raw API errors are never propagated to the frontend (prevents ECO-1297 false re-consent).
- Controlled error objects are returned instead:
{ error: 'message' }.
asApp() Security Rules
Section titled “asApp() Security Rules”- Used only when
asUser()cannot fulfill the operation (e.g., ECO-1222 ADMINISTER permission issue). - When using
asApp(), the resolver performs explicit permission validation in application code before executing.
Rate Limit Security
Section titled “Rate Limit Security”Jira API rate limits are a shared resource:
| Tier | Budget | Scope |
|---|---|---|
| Tier 1 | 65,000 points/hour | Global pool across ALL app installations |
| Tier 2 (granted) | 100,000-500,000 points/hour | Per-tenant |
Mitigation strategies:
- Product event triggers are free (no rate limit cost). The event-driven issue cache avoids most direct API calls.
- KVS-backed rate limit counters (
utils/rate-limiter.ts) track and throttle outbound API usage. - Generator execution (bulk JQL) runs as async events with 15-minute timeout, naturally spreading load.
Data Security
Section titled “Data Security”Forge SQL
Section titled “Forge SQL”- Encryption at rest: Managed by Atlassian’s infrastructure.
- Tenant isolation: Each Jira site installation has its own isolated Forge SQL database (1 GiB limit).
- No cross-tenant access: One installation’s data is never accessible from another installation.
Forge KVS
Section titled “Forge KVS”- Per-installation isolation: Same as SQL — each installation has its own KVS namespace.
- Sensitive variables: Environment variables (like
SENTRY_DSN) are stored encrypted usingforge variables set --encrypt. - User-scoped data: Announcement dismissals are stored per user (
announcements_dismissed:${accountId}).
Frontend Security
Section titled “Frontend Security”- Cross-origin iframe: The Custom UI runs in a Forge-managed iframe with strict CSP. The frontend cannot access the parent page’s DOM or cookies.
- No direct API calls: The frontend communicates with the backend exclusively through
@forge/bridgeinvoke()calls. It cannot make direct HTTP requests to Jira APIs. - No secrets in frontend code: API keys, tokens, and credentials are never included in the frontend bundle. All external service communication goes through the backend.
Resolver Security Patterns
Section titled “Resolver Security Patterns”Payload Validation
Section titled “Payload Validation”- Resolvers validate required payload fields before processing.
- The
"schema"key is never used in resolver payloads (ECO-1307 — Forge bridge rejects it). - Resolvers never return bare
undefined(ECO-277 — would become{}on frontend).
Error Handling
Section titled “Error Handling”// Standard pattern for resolver error handlingresolver.define('methodName', async ({ payload, context }) => { try { // Permission check first await checkPermission(payload.lensId, context.accountId, 'edit');
// Business logic const result = await someOperation(payload); return { data: result }; } catch (error) { // Never propagate raw errors -- return controlled response return { error: error.message || 'Operation failed' }; }});Permission-First Pattern
Section titled “Permission-First Pattern”Every resolver that modifies data follows this pattern:
- Extract
accountIdfromcontext(Forge-provided, tamper-proof) - Check lens-level permission via
permission-service.ts - If the operation touches Jira issues, verify BROWSE permission
- Execute the operation
- Return controlled response
Known Security-Adjacent Platform Bugs
Section titled “Known Security-Adjacent Platform Bugs”| Bug | Risk | Mitigation |
|---|---|---|
| ECO-1297 | 401 from asUser() triggers confusing consent dialog | All asUser() calls wrapped in try/catch |
| ECO-1222 | ADMINISTER perm not recognized with asUser() | Use asApp() with explicit permission validation |
| ECO-1356/1198 | Stale context could show wrong issue’s data | Context-key guards verify issue before rendering |
| ECO-277 | undefined -> {} could bypass “no data” checks | Always return explicit null or { data: null } |
| ECO-1175 | Missing permissions on install if Jira unavailable | Re-installing fixes permissions |