> ## Documentation Index
> Fetch the complete documentation index at: https://docs.lumos.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Access requests and tasks

> How Lumos creates approval and provisioning tasks for an access request, how to follow a request until it finishes, and how to build an approval bot that acts on tasks from code.

An access request moves through approval, provisioning and, for time-based access, removal. Where
a step needs a person, Lumos creates a [task](https://docs.lumos.com/reference/v1/objects/task.md) and assigns it. This guide
follows a request through those steps and builds a bot that approves requests from code.
[The Access Request object](https://docs.lumos.com/reference/v1/objects/access-request.md) and
[the Task object](https://docs.lumos.com/reference/v1/objects/task.md) list every field.

## How a request moves

Filing a request starts its [approval workflow](https://docs.lumos.com/reference/v1/objects/workflow.md). From there:

* **Approval.** Each approval stage creates an `access-request-approval` task for the stage's
  approvers. A request that needs no approval moves directly to `approved`.
* **Provisioning.** When Lumos can grant the access itself, for example through the app's
  integration, provisioning runs automatically. Otherwise Lumos creates a task, such as
  `manual-provisioning` or `account-creation`, for someone to grant it by hand. The app's admins get
  that task unless the workflow names someone else.
* **Removal.** When time-based access ends, Lumos removes it automatically or creates a
  deprovisioning task, such as `manual-deprovisioning` or `account-deprovisioning`, for someone to
  remove it by hand.

```mermaid theme={null}
flowchart LR
    filed["Filed<br/>pending"]
    approval["Approval<br/>pending-manager-approval<br/>pending-approval"]
    approved["approved"]
    provisioning["Provisioning<br/>pending-provisioning<br/>pending-manual-provisioning"]
    closed["manager-denied<br/>denied<br/>expired"]
    cancelled["cancelled"]
    completed["completed"]
    declined["denied-provisioning"]
    removal["Removal<br/>pending-deprovisioning<br/>pending-manual-deprovisioning"]
    removed["time-based-expired"]
    filed --> approval
    filed -->|no approval needed| approved
    approval --> approved
    approved --> provisioning
    approval -->|denied, or not answered in time| closed
    filed & approval & approved & provisioning -.-> cancelled
    provisioning --> completed
    provisioning -->|declined| declined
    completed -->|time-based access ends| removal
    removal --> removed
```

[Statuses](https://docs.lumos.com/reference/v1/objects/access-request.md#statuses) describes every status, and which ones
you can cancel from.

## File a request and follow it

The code on this page uses Python 3 and the `requests` library. It reads your API token from the
`LUMOS_API_TOKEN` environment variable, as [Authentication](https://docs.lumos.com/reference/v1/authentication.md)
describes, and stops on any error, including a `429`.
[Rate limits](https://docs.lumos.com/reference/v1/authentication.md#rate-limits) explains how to wait and retry instead.

[Create Access Request](https://docs.lumos.com/api-reference/v1/access-requests/create-access-request.md) takes the app, the
entitlements you want on it, and a business justification:

```python File a request theme={null}
import os
import time

import requests

BASE_URL = "https://api.lumos.com"
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['LUMOS_API_TOKEN']}"

response = session.post(
    f"{BASE_URL}/v1/access-requests",
    json={
        "app_id": "1a2b3c4d-5e6f-7890-abcd-ef1234567890",
        "entitlement_ids": ["4d5e6f70-8192-4a3b-8c4d-5e6f70819203"],
        "business_justification": "Maintaining the release pipeline this quarter.",
        "access_length": "30 days",
    },
)
response.raise_for_status()
# One submission can create more than one request, so read the whole list.
request_ids = [request["id"] for request in response.json()["access_requests"]]
```

`entitlement_ids` takes entitlement IDs, as
[List Entitlements](https://docs.lumos.com/api-reference/v1/entitlements/list-entitlements.md) returns them. Omit it to
request the app itself. [Filing a request](https://docs.lumos.com/reference/v1/objects/access-request.md#filing-a-request)
covers the other fields and the errors each can return, and
[Time-based access](https://docs.lumos.com/reference/v1/objects/access-request.md#time-based-access) covers
`access_length`.

The API doesn't send an event when a request changes status or when Lumos creates a task. To follow
a request, poll it. Each poll counts against your
[rate limit](https://docs.lumos.com/reference/v1/authentication.md#rate-limits), so increase the wait after each poll:

```python Follow a request theme={null}
IN_PROGRESS = {
    "pending",
    "pending-manager-approval",
    "manager-approved",
    "pending-approval",
    "approved",
    "pending-provisioning",
    "pending-manual-provisioning",
    "provisioned",
    "pending-deprovisioning",
    "pending-manual-deprovisioning",
    "reverting",
}


def follow(request_id, max_delay=300, max_wait=24 * 60 * 60):
    """Poll a request until it leaves the in-progress statuses, doubling the wait each time.

    Raises TimeoutError if the request is still in progress after max_wait seconds.
    """
    deadline = time.monotonic() + max_wait
    delay = 10
    while True:
        response = session.get(f"{BASE_URL}/v1/access-requests/{request_id}")
        response.raise_for_status()
        request = response.json()
        if request["status"] not in IN_PROGRESS:
            return request
        if time.monotonic() + delay > deadline:
            raise TimeoutError(f"Request {request_id} is still {request['status']}")
        time.sleep(delay)
        delay = min(delay * 2, max_delay)


for request_id in request_ids:
    request = follow(request_id)
    print(request["id"], request["status"])
```

`follow()` returns when the request reaches a status that isn't cancellable, such as `completed`,
`denied`, `expired`, or `cancelled`. A request for time-based access moves back to an in-progress
status when its access ends. A request can wait on a person for days, so `follow()` raises
`TimeoutError` if the request is still in progress after `max_wait` seconds.

### The tasks on a request

Two calls return a request's tasks:

* [Get Access Request](https://docs.lumos.com/api-reference/v1/access-requests/get-access-request.md) with `expand=tasks`
  returns them in any status.
* [List Tasks](https://docs.lumos.com/api-reference/v1/tasks/list-tasks.md) with `access_request_id` returns the open ones,
  unless you pass `status`.

Both return only the tasks you can see, as
[Which tasks you can see](https://docs.lumos.com/reference/v1/objects/task.md#which-tasks-you-can-see) describes. A
requester who follows their own request usually sees no tasks at all, because approval tasks go to
the approvers. A provisioning or deprovisioning task for one requested entitlement carries that
entitlement's ID, as [Entitlement IDs on tasks](https://docs.lumos.com/reference/v1/objects/task.md#entitlement-ids-on-tasks)
describes.

## An approval bot

An approval bot finds its tasks with [List Tasks](https://docs.lumos.com/api-reference/v1/tasks/list-tasks.md) and approves
them with [Perform Task Action](https://docs.lumos.com/api-reference/v1/tasks/perform-task-action.md). A token acts as its
user, so a bot sees and acts on the same tasks its user can.
[Which tasks you can see](https://docs.lumos.com/reference/v1/objects/task.md#which-tasks-you-can-see) lists the rules for
each role and kind of assignment.

A bot running as the current approver can read the request with
[Get Access Request](https://docs.lumos.com/api-reference/v1/access-requests/get-access-request.md), using the task's
`access_request_id`. Once the request no longer waits on the bot, the bot needs another way to see
it, as [Who can see a request](https://docs.lumos.com/reference/v1/objects/access-request.md#who-can-see-a-request)
describes.

This script approves requests for a list of apps and leaves every other request for a person:

```python Approval bot theme={null}
import os
import time

import requests

BASE_URL = "https://api.lumos.com"
POLL_SECONDS = 60

# Requests for these apps are approved. Everything else is left for a person.
AUTO_APPROVE_APP_IDS = {"1a2b3c4d-5e6f-7890-abcd-ef1234567890"}

# Error codes that mean someone already handled the task. Any other error stops the bot.
ALREADY_HANDLED = {
    "task.already_in_status",
}

session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['LUMOS_API_TOKEN']}"


def pending_approval_tasks(user_id):
    """Yield the pending approval tasks assigned to user_id, with the actions that user can take."""
    params = {
        "assigned_user_ids": user_id,
        "type": "access-request-approval",
        "status": "pending",
        "expand": "actions",
        "page_size": 100,
    }
    while True:
        response = session.get(f"{BASE_URL}/v1/tasks", params=params)
        response.raise_for_status()
        page = response.json()
        for task in page["tasks"]:
            if task["type"] == "access-request-approval" and task["status"] == "pending":
                yield task
        token = page.get("next_page_token")
        if not token:
            return
        params["page_token"] = token


def should_approve(task):
    return task["app"] is not None and task["app"]["id"] in AUTO_APPROVE_APP_IDS


def approve(task, action):
    """Approve a task. Returns False if someone else handled it first."""
    body = {
        "action_type": action["action_type"],
        "transition_to": action["transitions"][0]["transition_to"],
    }
    response = session.post(f"{BASE_URL}/v1/tasks/{task['id']}:perform-action", json=body)
    if response.status_code in (400, 409):
        if response.json().get("error", {}).get("code") in ALREADY_HANDLED:
            return False
    response.raise_for_status()
    return True


def run_once(user_id):
    for task in pending_approval_tasks(user_id):
        if not should_approve(task):
            continue
        # The list only offers the actions the bot's user can take.
        action = next(
            (a for a in task["actions"] if a["action_type"] == "approve-access-request"),
            None,
        )
        if action is None:
            continue
        if approve(task, action):
            print(f"Approved task {task['id']}")
        else:
            print(f"Task {task['id']} was already handled")


me = session.get(f"{BASE_URL}/v1/users/current")
me.raise_for_status()
user_id = me.json()["id"]
while True:
    run_once(user_id)
    time.sleep(POLL_SECONDS)
```

Run it with the token in the environment:

```bash theme={null}
export LUMOS_API_TOKEN='lsk_your_token'
python approval_bot.py
```

A misspelled filter returns `400` rather than a wider list, as
[Filtering the list](https://docs.lumos.com/reference/v1/objects/task.md#filtering-the-list) describes. The bot checks each
task's `type` and `status` before it acts.

Manager approvals are `access-request-approval` tasks too. Some older manager approvals that are
still open have the type `manager-approval` instead. List that type as well if the bot should
handle them.
