# auth0_cli

CLI tool for searching, deleting, and suspending users across multiple Auth0 tenants. Credentials are fetched from AWS Secrets Manager. Also deployed as an AWS Lambda function and part of the employee offboarding automation Step Functions workflow.

## Features

- Search for a user by email across all 11 configured Auth0 tenants (searched sequentially, one tenant at a time)
- Delete a user by Auth0 ID from a specific tenant
- Suspend (block) a user by Auth0 ID in a specific tenant
- Per-tenant failures are absorbed — remaining tenants are always searched
- `dry_run` mode skips deletions/suspensions without failing
- Credentials fetched from AWS Secrets Manager at runtime

## Requirements

- Python 3.14+
- [uv](https://docs.astral.sh/uv/)
- AWS credentials in your environment (`AWS_PROFILE` or IAM role) with access to Secrets Manager

## Setup

```bash
cd lambda/auth0_cli
uv sync --group dev
```

## Environment variables

| Variable | Description | Example |
|---|---|---|
| `ENVIRONMENT` | Deployment environment (used for Sentry) | `dev`, `qa`, `prod` |
| `SENTRY_DSN` | Sentry DSN for error reporting (optional) | — |
| `AUTH0_TENANTS` | Comma-separated list of tenants to search (overrides default list) | `dev-orchard,qa-orchard` |
| `AWS_ACCESS_KEY_ID` | AWS access key (runtime credentials only; not read by app code) | — |
| `AWS_SECRET_ACCESS_KEY` | AWS secret key (runtime credentials only) | — |
| `AWS_SESSION_TOKEN` | AWS session token (for assumed roles) | — |

## CLI usage

```bash
# Assume shared AWS profile
awsume shared

# Search for a user by email across all configured tenants
uv run auth0-cli search-user-by-email --email user@example.com

# Delete a user by Auth0 ID from a specific tenant
uv run auth0-cli delete-user-by-id --user-id auth0|abc123 --tenant prod-orchard

# Dry run (no deletion performed)
uv run auth0-cli delete-user-by-id --user-id auth0|abc123 --tenant prod-orchard --dry-run

# Suspend (block) a user by Auth0 ID in a specific tenant
uv run auth0-cli suspend-user-by-id --user-id auth0|abc123 --tenant prod-orchard

# Enable debug logging
uv run auth0-cli --debug search-user-by-email --email user@example.com
```

## Lambda handler

`auth0_client/app.py` is the Lambda entry point. It routes on `event["action"]`:

### `search-user-by-email`

Searches all configured Auth0 tenants for a user by email. Per-tenant failures are absorbed and logged.

Request:

```json
{
  "action": "search-user-by-email",
  "email": "user@example.com",
  "ticket_id": "SYS-1234"
}
```

Response:

```json
{
  "email": "user@example.com",
  "ticket_id": "SYS-1234",
  "matches": [
    { "tenant": "prod-orchard", "user_id": "auth0|abc123" }
  ]
}
```

### `delete-user-by-id`

Deletes a user by Auth0 ID from a specific tenant. Set `"dry_run": true` to skip the deletion.

Request:

```json
{
  "action": "delete-user-by-id",
  "user_id": "auth0|abc123",
  "tenant": "prod-orchard",
  "dry_run": false
}
```

Response:

```json
{
  "tenant": "prod-orchard",
  "user_id": "auth0|abc123",
  "ticket_id": "SYS-1234",
  "deleted": true,
  "dry_run": false
}
```

A 404 from Auth0 (user already gone) is treated as idempotent success and still returns `"deleted": true`.

### `suspend-user-by-id`

Suspends (blocks) a user by Auth0 ID in a specific tenant, without deleting the account. Set `"dry_run": true` to skip the block.

Request:

```json
{
  "action": "suspend-user-by-id",
  "user_id": "auth0|abc123",
  "tenant": "prod-orchard",
  "dry_run": false
}
```

Response:

```json
{
  "tenant": "prod-orchard",
  "user_id": "auth0|abc123",
  "ticket_id": "SYS-1234",
  "suspended": true,
  "dry_run": false
}
```

## Docker

Start the Lambda locally and invoke it with curl:

```bash
docker compose up --build function

curl -X POST http://localhost:9000/2015-03-31/functions/function/invocations \
  -H 'Content-Type: application/json' \
  -d '{"action": "search-user-by-email", "email": "user@example.com"}'
```

Run unit tests and lint inside Docker (mirrors CI):

```bash
make docker_test
make docker_test_integration
```

## Tenants

By default, the following 11 tenants are searched:

`dev-delphi`, `dev-orchard`, `prod-delphi`, `prod-dna-apps`, `prod-orch-youtube`, `qa-orch-youtube`, `qa-orchard`, `sme-develop`, `sme-dna`, `sme-qa`, `workstation`

Override with the `AUTH0_TENANTS` environment variable (comma-separated):

```bash
AUTH0_TENANTS=dev-orchard,qa-orchard uv run auth0-cli search-user-by-email --email user@example.com
```

## AWS Secrets Manager

Credentials are fetched from Secrets Manager in the shared AWS account (`086679231553`) at:

```
shared/offboarding-automation/{tenant_name}/auth0_management_credentials
```

Each secret must be a JSON object with keys: `auth0_domain`, `auth0_client_id`, `auth0_client_secret`.

## Testing

```bash
uv run pytest tests/                                                                        # All tests
uv run pytest tests/unit/                                                                   # Unit tests only
uv run pytest tests/integration/                                                            # Integration tests only
uv run pytest tests/unit/test_auth0_client.py                                               # Single file
uv run pytest tests/unit/test_auth0_client.py::test_returns_user_list_when_email_exists     # Single test
```

## Lint & format

```bash
uv run ruff check auth0_client/ tests/
uv run ruff format auth0_client/ tests/
uv run mypy auth0_client/
```

(Or use `make lint` / `make format`, which rely on `[tool.ruff] src = ["auth0_client"]` in `pyproject.toml`.)

## Architecture

### Auth0 client (`auth0_client/auth0_client.py`)

Handles OAuth2 client-credentials token acquisition and user search/delete/block via the Auth0 Management API. Delete is `DELETE /api/v2/users/{id}` (204); suspend is `PATCH /api/v2/users/{id}` with `{"blocked": true}` (200). Tokens are cached in memory until 60 seconds before expiry.

### Secrets Manager client (`auth0_client/secrets_manager.py`)

Wraps `boto3` to fetch Auth0 tenant credentials from Secrets Manager. Called at request time, once per tenant; credentials are passed directly into `Auth0Client`.

### Configuration (`config.py`)

`Auth0Config` uses `pydantic-settings` and holds only the tenant list (`auth0_tenants`, exposed as the `tenant_list` property). Per-tenant Auth0 credentials are **not** held in config — they are fetched at request time from Secrets Manager by `SecretsManagerClient`.

## Project structure

```
lambda/auth0_cli/
├── config.py                   # Auth0Config (pydantic-settings, tenant list)
├── dev.py                      # Typer CLI entry point (auth0-cli script)
├── auth0_client/
│   ├── __init__.py
│   ├── app.py                  # Lambda handler
│   ├── auth0_client.py         # Auth0 Management API client
│   └── secrets_manager.py      # AWS Secrets Manager client
└── tests/
    ├── unit/                   # Fast, isolated unit tests
    └── integration/            # Handler-level tests against a live Lambda container
```

