All Journal Notes

Inside Jira’s Authorization Service: RBAC Enforcement Across Every Layer

How Atlassian engineered the Jira Authorization Service (JAS) and Jira Issue Service (JIS) to enforce hybrid RBAC, ReBAC, and ABAC policies under sub-15ms p99 read SLOs across presentation, domain, and protocol layers.

Authorization in large-scale multi-tenant systems is notoriously difficult. Unlike authentication—which verifies identity once at the perimeter edge—authorization must be continuously evaluated across thousands of nested entity graphs on every single read, mutation, and search query.

When Atlassian undertook the historic decomposition of the monolithic Jira codebase, it faced a daunting challenge: decompose an intertwined monolith into isolated cloud microservices without degrading query latency or breaching multi-tenant data boundaries.

The core service responsible for issue lifecycles—the Jira Issue Service (JIS)—was mandated to operate under an uncompromising sub-15ms p99 read Service Level Objective (SLO). Because authorization is invoked on nearly every database fetch, the Jira Authorization Service (JAS) was allocated a strict latency envelope of no more than 2 to 3 milliseconds per evaluation.

Here is the architectural teardown of how Jira enforces authorization across every layer of the modern cloud stack.

---

1. The Triad Service Topology

To handle millions of daily active enterprise users, Jira’s backend was decoupled into three foundational services:

1. JIS (Jira Issue Service): The source of truth for issue CRUD operations, status workflows, custom fields, and relational linking graphs. 2. JSIS (Jira Search and Indexing Service): A distributed Lucene/Elasticsearch cluster providing sub-second JQL (Jira Query Language) execution across billions of indexed documents. 3. JAS (Jira Authorization Service): The centralized policy evaluation engine determining whether an actor has permission to view, edit, transition, or administer any entity.

code
   [ Client / Browser ]
           │
           ▼
    [ Edge Gateway ]
           │
     ┌─────┴────────────────────────┐
     ▼                              ▼
 [ Jira Issue Svc (JIS) ]   [ Jira Search Svc (JSIS) ]
     │                              │
     └──────────────┬───────────────┘
                    ▼ (sub-3ms gRPC)
        [ Jira Auth Svc (JAS) ]
                    │
            ┌───────┴───────┐
            ▼               ▼
     [ Policy Cache ]  [ Multi-Raft Graph ]

When a user requests an issue or executes a JQL query, JIS and JSIS interact with JAS over high-performance gRPC channels. If JAS took 20ms to evaluate an issue's permission schemes, JIS would immediately violate its 15ms p99 SLO.

---

2. The Hybrid Access Control Model: RBAC + ReBAC + ABAC

Real-world enterprise governance cannot be modeled with pure Role-Based Access Control (RBAC) alone. Jira combines three authorization paradigms into a single unified evaluation graph:

#### A. Role-Based Access Control (RBAC) Global Permissions: System-wide rights (e.g., `ADMINISTER_JIRA`, `CREATE_SHARED_OBJECTS`). Project Roles: Roles scoped to a specific project (e.g., Developers, Administrators, Service Desk Agents). Permission Schemes:* Mappings linking specific permissions (like EDIT_ISSUES) to project roles or user groups.

#### B. Relationship-Based Access Control (ReBAC) Access is derived from graph topology. For example: User A is a member of Team Beta. Team Beta owns Component Storage. Issue JRA-104 belongs to Component Storage. Therefore, User A inherits triage rights on JRA-104 via transitive graph inheritance.

#### C. Attribute-Based Access Control (ABAC) Fine-grained dynamic conditions evaluated at runtime: Issue Security Levels: Restricting visibility to only the Reporter, Assignee, and specific designated security groups. Status Gates: For instance, only users with the role Release Manager can transition an issue when Status == "Ready for Deployment".

---

3. Multi-Layer Enforcement Architecture

A fundamental principle of resilient security is defense in depth. In Jira's cloud architecture, authorization is not a single gateway check; it is enforced across three distinct structural tiers:

#### Layer 1: Presentation & Cosmetic Layer (UI & Navigation) The user interface must never expose actions the user is unauthorized to execute. Before rendering an issue view, JIS returns an authorization mask containing booleans for permitted operations:

json
{
  "issueKey": "ENG-402",
  "permissions": {
    "canEdit": true,
    "canTransition": false,
    "canAssign": true,
    "canDelete": false
  }
}

This prevents UI frustration by hiding "Transition to Done" buttons and edit pencils for users without write privileges. However, client-side hiding is strictly cosmetic; the true security guarantees reside in deeper layers.

#### Layer 2: Application / Business Logic Layer Within the domain model, state transitions are governed by workflow guards. Even if a malicious actor bypasses the web UI, the domain service enforces lifecycle invariants:

go
func (s *IssueWorkflowService) TransitionIssue(
    ctx context.Context, 
    actor Actor, 
    issueID string, 
    targetState State,
) (*Issue, error) {
    // 1. Fetch issue domain entity
    issue, err := s.repo.GetByID(ctx, issueID)
    if err != nil {
        return nil, err
    }

    // 2. Query JAS policy engine for transition capability
    allowed, err := s.authSvc.CheckPermission(ctx, AuthRequest{
        Subject:  actor.PrincipalID,
        Action:   "transition.execute",
        Resource: issue.ResourceURN(),
        Context:  map[string]any{"target_status": targetState},
    })
    if err != nil || !allowed {
        return nil, ErrUnauthorizedTransition
    }

    // 3. Mutate domain state
    return s.repo.ApplyTransition(ctx, issue, targetState)
}

#### Layer 3: Protocol & Security Layer (gRPC Interceptors) At the lowest network boundary, every incoming gRPC RPC and REST endpoint passes through an unbypassable interceptor. If an incoming RPC payload lacks valid tenant tokens or principal claims, it is terminated before reaching the business logic:

go
func AuthUnaryServerInterceptor(auth JASClient) grpc.UnaryServerInterceptor {
    return func(
        ctx context.Context,
        req any,
        info *grpc.UnaryServerInfo,
        handler grpc.UnaryHandler,
    ) (any, error) {
        claims, err := extractSecurityContext(ctx)
        if err != nil {
            return nil, status.Error(codes.Unauthenticated, "missing principal claims")
        }

        // Fast-path evaluation against local tenant memory cache
        allowed := auth.FastEvaluate(ctx, claims.TenantID, claims.UserID, info.FullMethod)
        if !allowed {
            return nil, status.Error(codes.PermissionDenied, "unauthorized RPC invocation")
        }

        return handler(ctx, req)
    }
}

---

4. Meeting the Sub-15ms Read SLO: Multi-Tier Caching

Evaluating complex graph-based ReBAC relationships on every database read would melt the database and violate the 15ms p99 latency target. To achieve sub-3ms evaluation times, JAS deploys a multi-tier caching hierarchy:

1. Request-Scoped Ephemeral Cache: Within the lifecycle of a single HTTP request (which might fetch 50 linked issues), user group memberships and project permission schemes are memoized in thread-local memory. 2. Local In-Memory Cache (Guava/Ristretto): Each JIS pod maintains a local, bounded LRU cache of recently evaluated tenant permissions with a 60-second TTL. 3. Distributed Redis Cache with Invalidation Streams: When an administrator alters a project permission scheme or reassigns a user role, an invalidation message is broadcast over Kafka/Redis PubSub, purging stale cache entries across all service pods in under 50 milliseconds.

---

Key Architectural Takeaways

1. Never conflate UI masking with security: Cosmetic button hiding creates good UX, but unbypassable protocol interceptors provide real security. 2. Model ReBAC for enterprise graphs: Pure RBAC breaks down when permissions depend on hierarchical project structures and team ownership. 3. Budget latency aggressively: An authorization service cannot afford to take 20ms if your application read SLO is 15ms. Invest in multi-tier caching and fail-closed local verification.

UG

Umesh Gupta

@umesh

Founder & Software Architect

Founder of Abeta. Software architect focusing on high-throughput distributed state, authorization engines, and @abeta.dev/react-libs. Writing on Medium @adroitexplorer.

View all articles by Umesh Gupta