Skip to content

Temporary Access Requests

Overview

The Access Request system lets users request time-limited access to applications without requiring permanent group membership. An administrator creates templates that define which app-mappings are granted, who can approve, how notifications are dispatched, and for how long access may be granted.

When a user submits a request against a template, designated approvers are notified. On approval, a temporary group is created and the requester is added to it for the chosen duration. When the grant expires—or is revoked—the group is removed and the claims stop being emitted at the next token issuance.


Concepts

Concept Description
Template Admin-defined workflow: which app, which mappings, who approves, notification policy, escalation
Access Request A user's submission against a template (status: pending → approved / denied / revoked / expired). A revoked request with no decided_by was cancelled by the requester before a decision was made.
Temporary group Auto-created system group (__access_req_<id>) with an expires_at timestamp; deleted on expiry or revocation
Discussion thread Per-request chat visible to requester and approvers

Notification Policies

Policy Behaviour
all Every member of every approver group is notified
first_n The first notification_count members of the first approver group are notified
random_n notification_count randomly chosen members across all approver groups
round_robin One member per approval, cycling through approver group members in order

Lifecycle Events

Integration events are fired at every lifecycle transition and carry the following payload keys:

Event Key payload fields
access_request.submitted request_id, template_name, requester_email, notified_approvers
access_request.approved request_id, template_name, requester_email, decided_by
access_request.denied request_id, template_name, requester_email, decided_by
access_request.revoked request_id, template_name, requester_email
access_request.expired request_id, template_name, requester_email
access_request.escalated request_id, template_name, requester_email

Use these events in Scheduled Tasks / Integrations to send Slack / email notifications.


API Reference

All endpoints are under /api/v1 and require a Bearer token.

Templates

Method Path Permission Description
GET /access-request-templates authenticated List all templates
GET /access-request-templates/{id} authenticated Get template
POST /access-request-templates access_requests.manage Create template
PUT /access-request-templates/{id} access_requests.manage Update template
DELETE /access-request-templates/{id} access_requests.manage Delete template (blocked if active requests exist)

Requests

Method Path Permission Description
GET /access-requests authenticated List my requests
POST /access-requests authenticated Submit a request
GET /access-requests/pending authenticated List requests awaiting my approval
GET /access-requests/all access_requests.manage List all requests — supports ?status=, ?template_id=, ?requester= filters
GET /access-requests/{id} requester or approver Get a request
POST /access-requests/{id}/approve approver group member Approve
POST /access-requests/{id}/deny approver group member Deny
POST /access-requests/{id}/revoke requester or admin Revoke
GET /access-requests/{id}/messages requester or approver List discussion thread
POST /access-requests/{id}/messages requester or approver Add a message

Submit a request

POST /api/v1/access-requests
Authorization: Bearer <token>
Content-Type: application/json

{
  "template_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "requested_duration_mins": 480,
  "justification": "Need access to debug the production incident."
}

Guardrail — one pending request per template: If the user already has a pending request for the same template, the API returns 409 Conflict with error code pending_request_exists. The UI disables the submit button and shows a warning in this case. Users must wait for the existing request to be decided or cancel it before submitting another.

Cancel a pending request

A user can cancel their own pending (not yet decided) request by calling the revoke endpoint:

POST /api/v1/access-requests/{id}/revoke
Authorization: Bearer <token>

A cancelled request has status = revoked with no decided_by (the requester acted, not an approver). In the UI the Decision column shows Cancelled (not Approved) to make this distinction clear. An approved-then-revoked request retains the Approved decision.

POST /api/v1/access-requests/{id}/approve
Authorization: Bearer <token>
Content-Type: application/json

{
  "comment": "Approved for the incident window."
}

Terraform

Use the justiam_access_request_template resource to manage templates as code.

Resource: justiam_access_request_template

resource "justiam_access_request_template" "db_readonly" {
  name        = "Database read-only access"
  description = "Grants read-only database credentials for up to 8 hours."
  app_id      = justiam_application.internal_db.id

  mapping_ids        = [justiam_app_mapping.db_readonly_role.id]
  allowed_durations  = [60, 240, 480]   # 1h, 4h, 8h
  approver_group_ids = [justiam_group.dba_team.id]

  require_justification = true
  auto_approve          = false
  notification_policy   = "all"

  escalation_after_mins = 30
  escalation_group_id   = justiam_group.engineering_managers.id
}

Arguments

Argument Type Required Description
name string yes Display name
description string no Shown to requesters
app_id string yes Target application UUID (replace requires new resource)
mapping_ids list(string) no App-mapping UUIDs to grant
allowed_durations list(number) no Allowed durations in minutes
approver_group_ids list(string) no Approver group UUIDs
allowed_group_ids list(string) no Groups allowed to submit requests. Empty = anyone
prefer_requester_groups bool no Prefer approvers who share a group with the requester. Default false
require_justification bool no Default true
auto_approve bool no Default false
notification_policy string no all / first_n / random_n / round_robin. Default all
notification_count number no Used by first_n, random_n, round_robin. Default 1
escalation_after_mins number no Escalation timeout in minutes. Omit to disable
escalation_group_id string no Escalation group UUID
custom_fields string (JSON) no JSON array of custom field definitions (see Custom Fields)
trigger_task_id string no UUID of the script task to fire on every status change
trigger_task_failure_mode string no ignore / retry / block. Default ignore

Computed Attributes

Attribute Description
id Template UUID assigned by the server

Expiry Enforcement

Temporary access is enforced at two levels:

  1. Background task (Access Request Expiry) — runs every 5 minutes, removes expired temporary groups and fires access_request.expired events.
  2. Token issuanceGetGroups and GetEffectiveMappingValues filter out groups where expires_at < NOW(), so a token issued before the background task runs will still not contain expired claims.

This dual enforcement means access is revoked within the token's validity period (typically a few minutes) even if the background task has not yet run.


Admin History View

Admins with the access_requests.manage permission have access to a History tab on the Access Requests page. It shows every request across all users with the following filters:

Filter Values
Status All / Pending / Approved / Denied / Expired / Revoked
Template Dropdown of all configured templates
Requester Partial email match (case-insensitive)

Requests whose template was subsequently deleted continue to appear with a (deleted template) label.


Custom Fields

Templates can define extra fields that are shown to the user at submission time. Each field has:

Property Values Description
label string Shown above the input
type text | textarea | select | multi_select | checkbox Input type
required bool Block submission if empty
placeholder string Input hint text
options list(string) Allowed values for select / multi_select

Answers are stored under the access request's custom_answers object, keyed by the field's UUID, and are visible in the access request detail panel.


Trigger Tasks

Every template can wire a script task that fires automatically on every lifecycle event of requests against that template. This is the recommended way to send notifications or call external APIs for access management events.

Supported events

Event When
created A user submits a new request
approved An approver (or auto-approve) grants the request
denied An approver denies the request
revoked An admin or approver revokes an active grant
expired The background expiry task removes an expired grant

Failure modes

Mode Behaviour
ignore Fire-and-forget, no retries. Failures are logged but do not affect the request.
retry Fire-and-forget, uses the task's configured max retries.
block Runs before the status change and waits up to 30 s. The status change is aborted if the task fails or times out. Only supported for the approved event.

Available config keys in triggered scripts

All keys are accessible via config.Get("key"):

Key Description
trigger_event Event name: created | approved | denied | revoked | expired
request_id UUID of the access request
request_status Current status (mirrors trigger_event)
template_id UUID of the access request template
template_name Display name of the template
app_id UUID of the application being requested
app_name Display name of the application
requester_id UUID of the requesting user
requester_email Email of the requesting user
requester_name Display name of the requesting user
justification Justification text supplied by the requester

Example: access_request_event_logger

A reference script that logs the full payload for all events is available at yaegi/scripts/access_request_event_logger.go. Use it to verify what data is available before building a real integration.

package task

import (
    "config"
    "fmt"
    "strings"
)

func Run() (string, error) {
    event := config.Get("trigger_event")
    lines := []string{
        fmt.Sprintf("=== access_request.%s ===", event),
        fmt.Sprintf("  Request ID:      %s", config.Get("request_id")),
        fmt.Sprintf("  Status:          %s", config.Get("request_status")),
        fmt.Sprintf("  Template:        %s", config.Get("template_name")),
        fmt.Sprintf("  App:             %s", config.Get("app_name")),
        fmt.Sprintf("  Requester:       %s (%s)", config.Get("requester_name"), config.Get("requester_email")),
        fmt.Sprintf("  Justification:   %s", config.Get("justification")),
    }
    output := strings.Join(lines, "\n")
    fmt.Println(output)
    return output, nil
}

Terraform example

resource "justiam_scheduled_task" "access_logger" {
  name          = "Access Request Event Logger"
  task_type     = "script"
  schedule_type = "once"
  script        = file("${path.module}/scripts/access_request_event_logger.go")
}

resource "justiam_access_request_template" "db_access" {
  name    = "Database Read Access"
  app_id  = justiam_application.postgres.id

  trigger_task_id           = justiam_scheduled_task.access_logger.id
  trigger_task_failure_mode = "ignore"
}

Template Deletion

A template can only be deleted when it has no active (pending or approved) requests. Historical (closed) requests are preserved with their data intact but the template_id reference is nullified.


Setting Up Slack Notifications

Combine the event system with a script task for Slack alerts:

package task

import (
    "bytes"
    "config"
    "encoding/json"
    "idp"
    "net/http"
    "strings"
)

func Run() (string, error) {
    requestID := config.Get("request_id")
    requesterEmail := config.Get("requester_email")
    templateName := config.Get("template_name")

    approvers, _ := idp.GetAccessRequestApprovers(requestID)
    emails := make([]string, len(approvers))
    for i, a := range approvers {
        emails[i] = a.Email
    }

    msg := templateName + " access requested by " + requesterEmail +
        " — approvers: " + strings.Join(emails, ", ")

    body, _ := json.Marshal(map[string]string{"text": msg})
    http.Post(secrets.Get("SLACK_WEBHOOK"), "application/json", bytes.NewReader(body))
    return "notified", nil
}

Bind this script task to the access_request.submitted integration event. Event data fields (e.g. request_id, requester_email, template_name) are available directly via config.Get("request_id") or with the event. prefix via config.Get("event.request_id").