Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 79 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,10 @@ pip install -e .

## Configuration

Duo has separate **Auth API** and **Universal Prompt (Web SDK)** integrations — configure each one you need:
Duo has separate **Admin API**, **Auth API**, and **Universal Prompt (Web SDK)** integrations — configure each one you need:

```bash
duo-cli configure --api admin
duo-cli configure --api auth
duo-cli configure --api universal
```
Expand All @@ -42,6 +43,11 @@ The interactive setup walks you through where to find credentials in the Duo Adm
Credentials are stored in `~/.duo-cli/config.json`. You can also use environment variables (useful for CI/agents):

```bash
# Admin API
export DUO_ADMIN_IKEY=DIXXXXXXXXXXXXXXXXXX
export DUO_ADMIN_SKEY=your-secret-key
export DUO_ADMIN_HOST=api-XXXXXXXX.duosecurity.com

# Auth API
export DUO_AUTH_IKEY=DIXXXXXXXXXXXXXXXXXX
export DUO_AUTH_SKEY=your-secret-key
Expand All @@ -56,6 +62,17 @@ export DUO_UNIVERSAL_HOST=api-XXXXXXXX.duosecurity.com
## Quick Start

```bash
# Verify Admin API credentials
duo-cli admin check

# List users, groups, phones, integrations, and policies
duo-cli admin users --limit 25
duo-cli admin groups --limit 25
duo-cli admin integrations --limit 25

# Inspect recent logs
duo-cli admin logs auth --days 7 --limit 20

# Verify credentials
duo-cli auth check

Expand All @@ -77,6 +94,66 @@ duo-cli -o json universal login jsmith

## Commands

### Admin API

These commands provide a first milestone of high-value Admin API coverage for operators and shell scripts. JSON output returns the full underlying API response, while table output shows a compact summary.

| Command | Description |
|---------|-------------|
| `duo-cli admin check` | Verify Admin API credentials and return account summary data |
| `duo-cli admin users [--username <username>]` | List users or filter to a single username |
| `duo-cli admin user <user_id>` | Fetch a single user |
| `duo-cli admin groups` | List groups |
| `duo-cli admin group <group_id>` | Fetch a single group |
| `duo-cli admin phones` | List phones |
| `duo-cli admin phone <phone_id>` | Fetch a single phone |
| `duo-cli admin integrations` | List integrations |
| `duo-cli admin integration <integration_key>` | Fetch a single integration |
| `duo-cli admin policies` | List policies |
| `duo-cli admin policy <policy_key>` | Fetch a single policy |
| `duo-cli admin logs auth` | Fetch recent authentication log events |
| `duo-cli admin logs administrator` | Fetch recent administrator log events |
| `duo-cli admin logs activity` | Fetch recent activity log events |
| `duo-cli admin raw <METHOD> <PATH>` | Call an Admin API path directly for long-tail coverage |

#### Admin API Examples

```bash
# Save Admin API credentials
duo-cli configure --api admin

# Confirm the Admin API integration works
duo-cli admin check

# Find a user by username
duo-cli admin users --username aateya

# Inspect a small slice of the tenant
duo-cli admin users --limit 10
duo-cli admin groups --limit 10
duo-cli admin phones --limit 10
duo-cli admin integrations --limit 10
duo-cli admin policies --limit 10

# Fetch a single object by ID/key
duo-cli admin user DUXXXXXXXXXXXXXXXXXX
duo-cli admin group DGXXXXXXXXXXXXXXXXXX
duo-cli admin phone DPXXXXXXXXXXXXXXXXXX
duo-cli admin integration DIXXXXXXXXXXXXXXXXXX
duo-cli admin policy POXXXXXXXXXXXXXXXXXX

# Look at recent logs
duo-cli admin logs auth --days 7 --limit 20
duo-cli admin logs administrator --days 7 --limit 20
duo-cli admin logs activity --days 7 --limit 20

# Use JSON for scripts
duo-cli -o json admin users --limit 5

# Call an endpoint directly when there is not a dedicated command yet
duo-cli admin raw GET /admin/v1/users -p limit=1
```

### Auth API

| Command | Description |
Expand Down Expand Up @@ -179,6 +256,7 @@ if "allow" in result.lower():
All commands support `--output json` for machine-readable output:

```bash
duo-cli -o json admin users --limit 10
duo-cli -o json auth preauth jsmith
duo-cli -o json universal login jsmith
```
Expand Down
130 changes: 130 additions & 0 deletions duo_cli/admin_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
"""Admin API wrapper used by the CLI."""

from datetime import datetime, timedelta, timezone
from typing import Dict, List, Optional, Sequence, Union

import duo_client

from duo_cli.config import get_client_kwargs

# Interactive CLI defaults chosen to keep output readable.
DEFAULT_LIST_LIMIT = 100
DEFAULT_LOG_LIMIT = 20


def _time_window(days: int, *, milliseconds: bool) -> tuple[int, int]:
now = datetime.now(tz=timezone.utc)
start = now - timedelta(days=days)

if milliseconds:
return int(start.timestamp() * 1000), int(now.timestamp() * 1000)
return int(start.timestamp()), int(now.timestamp())


class AdminClient:
"""Small adapter around ``duo_client.Admin`` for CLI-oriented defaults."""

def __init__(self, client: Optional[duo_client.Admin] = None):
self._client = client or duo_client.Admin(**get_client_kwargs("admin"))

def check(self) -> dict:
return self._client.get_info_summary()

def list_users(
self,
*,
limit: int = DEFAULT_LIST_LIMIT,
offset: int = 0,
username: Optional[str] = None,
) -> list[dict]:
if username:
return self._client.get_users_by_name(username)
return self._client.get_users(limit=limit, offset=offset)

def get_user(self, user_id: str) -> dict:
return self._client.get_user_by_id(user_id)

def list_groups(self, *, limit: int = DEFAULT_LIST_LIMIT, offset: int = 0) -> list[dict]:
return self._client.get_groups(limit=limit, offset=offset)

def get_group(self, group_id: str) -> dict:
return self._client.get_group(group_id, api_version=2)

def list_phones(self, *, limit: int = DEFAULT_LIST_LIMIT, offset: int = 0) -> list[dict]:
return self._client.get_phones(limit=limit, offset=offset)

def get_phone(self, phone_id: str) -> dict:
return self._client.get_phone_by_id(phone_id)

def list_integrations(
self,
*,
limit: int = DEFAULT_LIST_LIMIT,
offset: int = 0,
) -> list[dict]:
return self._client.get_integrations(limit=limit, offset=offset)

def get_integration(self, integration_key: str) -> dict:
return self._client.get_integration(integration_key)

def list_policies(
self,
*,
limit: int = DEFAULT_LIST_LIMIT,
offset: int = 0,
) -> list[dict]:
return self._client.get_policies_v2(limit=limit, offset=offset)

def get_policy(self, policy_key: str) -> dict:
return self._client.get_policy_v2(policy_key)

def authentication_logs(
self,
*,
days: int = 7,
limit: int = DEFAULT_LOG_LIMIT,
) -> dict:
mintime, maxtime = _time_window(days, milliseconds=True)
return self._client.get_authentication_log(
api_version=2,
mintime=mintime,
maxtime=maxtime,
limit=str(limit),
)

def administrator_logs(
self,
*,
days: int = 7,
limit: int = DEFAULT_LOG_LIMIT,
) -> list[dict]:
mintime, _ = _time_window(days, milliseconds=False)
return self._client.get_administrator_log(mintime=mintime)[:limit]

def activity_logs(
self,
*,
days: int = 7,
limit: int = DEFAULT_LOG_LIMIT,
) -> dict:
mintime, maxtime = _time_window(days, milliseconds=True)
return self._client.get_activity_logs(
mintime=mintime,
maxtime=maxtime,
limit=limit,
sort="DESC",
)

def raw(self, method: str, path: str, params: Dict[str, str]) -> Union[dict, List[dict]]:
return self._client.json_api_call(method.upper(), path, params)


def parse_params(items: Sequence[str]) -> Dict[str, str]:
"""Convert repeated ``key=value`` CLI parameters into a dict."""
params: Dict[str, str] = {}
for item in items:
if "=" not in item:
raise ValueError(f"parameter must be key=value, got: {item}")
key, value = item.split("=", 1)
params[key] = value
return params
Loading