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:
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:
- Background task (
Access Request Expiry) — runs every 5 minutes, removes expired temporary groups and firesaccess_request.expiredevents. - Token issuance —
GetGroupsandGetEffectiveMappingValuesfilter out groups whereexpires_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").