Skip to content
Merged
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
1 change: 1 addition & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ on:
- main
paths:
- 'cortexapps_cli/**'
- 'docker/**'
- 'pyproject.toml'
- 'poetry.lock'

Expand Down
16 changes: 16 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,22 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

<!-- insertion marker -->
## [1.19.2](https://github.com/cortexapps/cli/releases/tag/1.19.2) - 2026-06-10

<small>[Compare with 1.19.1](https://github.com/cortexapps/cli/compare/1.19.1...1.19.2)</small>

### Bug Fixes

- update Docker base image for CVE-2026-45447 and add docker/ to publish triggers #patch ([1bcef87](https://github.com/cortexapps/cli/commit/1bcef8718845cad9b15094a764392c5dcaa8804e) by Jeff Schnitter).

## [1.19.1](https://github.com/cortexapps/cli/releases/tag/1.19.1) - 2026-06-09

<small>[Compare with 1.19.0](https://github.com/cortexapps/cli/compare/1.19.0...1.19.1)</small>

### Bug Fixes

- remove invalid RST transition that breaks PyPI rendering #patch ([00e2d96](https://github.com/cortexapps/cli/commit/00e2d96500ddfb9325d8db8075612af4a0489d58) by Jeff Schnitter).

## [1.19.0](https://github.com/cortexapps/cli/releases/tag/1.19.0) - 2026-06-01

<small>[Compare with 1.18.0](https://github.com/cortexapps/cli/compare/1.18.0...1.19.0)</small>
Expand Down
2 changes: 0 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -453,8 +453,6 @@ This recipe creates YAML files for each Workflow. This may be helpful if you ar
cortex workflows get --tag $workflow --yaml > $workflow.yaml
done

====================================

.. |PyPI download month| image:: https://img.shields.io/pypi/dm/cortexapps-cli.svg
:target: https://pypi.python.org/pypi/cortexapps-cli/
.. |PyPI version shields.io| image:: https://img.shields.io/pypi/v/cortexapps-cli.svg
Expand Down
2 changes: 2 additions & 0 deletions cortexapps_cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import cortexapps_cli.commands.scorecards as scorecards
import cortexapps_cli.commands.secrets as secrets
import cortexapps_cli.commands.teams as teams
import cortexapps_cli.commands.users as users
import cortexapps_cli.commands.workflows as workflows

app = typer.Typer(
Expand Down Expand Up @@ -77,6 +78,7 @@
app.add_typer(scorecards.app, name="scorecards")
app.add_typer(secrets.app, name="secrets")
app.add_typer(teams.app, name="teams")
app.add_typer(users.app, name="users")
app.add_typer(workflows.app, name="workflows")

# global options
Expand Down
5 changes: 5 additions & 0 deletions cortexapps_cli/commands/users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import typer
import cortexapps_cli.commands.users_commands.roles as roles

app = typer.Typer(help="Users commands", no_args_is_help=True)
app.add_typer(roles.app, name="roles")
49 changes: 49 additions & 0 deletions cortexapps_cli/commands/users_commands/roles.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from typing import List, Optional
import typer
from cortexapps_cli.command_options import ListCommandOptions
from cortexapps_cli.utils import print_output_with_context

app = typer.Typer(help="Roles commands", no_args_is_help=True)

@app.command()
def list(
ctx: typer.Context,
email: Optional[List[str]] = typer.Option(None, "--email", "-e", help="Filter by email address; can be specified multiple times", show_default=False),
page: ListCommandOptions.page = None,
page_size: ListCommandOptions.page_size = 250,
table_output: ListCommandOptions.table_output = False,
csv_output: ListCommandOptions.csv_output = False,
columns: ListCommandOptions.columns = [],
no_headers: ListCommandOptions.no_headers = False,
filters: ListCommandOptions.filters = [],
sort: ListCommandOptions.sort = [],
):
"""
List user role assignments. The API key used to make the request must have the View Roles permission.
"""

client = ctx.obj["client"]

params = {
"page": page,
"pageSize": page_size,
}

if email:
params["email"] = ",".join(email)

if (table_output or csv_output) and not ctx.params.get('columns'):
ctx.params['columns'] = [
"Email=email",
"Name=name",
"Roles=roles",
]

# remove any params that are None
params = {k: v for k, v in params.items() if v is not None}

if page is None:
r = client.fetch("api/v1/users/roles", params=params)
else:
r = client.get("api/v1/users/roles", params=params)
print_output_with_context(ctx, r)
5 changes: 1 addition & 4 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
FROM python:3.13-slim

RUN pip install --upgrade pip
RUN apt update && apt install -y jq yq
# Installing this version to address security vulnerability reported in CVE-2024-33599
# This line should come out once the fix is included in a python image.
RUN apt install -y libc-bin
RUN apt update && apt upgrade -y && apt install -y jq yq
RUN useradd -m cortex
ADD config /home/cortex/.cortex/config

Expand Down
98 changes: 98 additions & 0 deletions docs/superpowers/specs/2026-06-10-users-roles-command-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# Users Roles Command Design

## Summary

Add a `cortex users roles list` command to support the `GET /api/v1/users/roles` endpoint. The command is structured as a subcommand group (`users` > `roles` > `list`) to accommodate future user and role management endpoints.

## API Endpoint

**`GET /api/v1/users/roles`**

Query parameters:
- `email` (optional): Comma-separated email addresses, case-insensitive
- `pageSize` (optional): 1-1000, defaults to 250
- `page` (optional): Zero-indexed, defaults to 0

Response:
```json
{
"page": 0,
"total": 100,
"totalPages": 1,
"users": [
{
"email": "user@example.com",
"name": "User Name",
"roles": [
{ "type": "BASIC", "role": "ADMIN" },
{ "type": "CUSTOM", "name": "My Role", "tag": "my-role" }
]
}
]
}
```

Requires Bearer token with "View Roles" permission.

## File Structure

```
cortexapps_cli/commands/
users.py # Top-level users Typer app, imports roles subcommand
users_commands/
__init__.py
roles.py # roles subcommand with `list` command
```

Registration in `cli.py`:
```python
import cortexapps_cli.commands.users as users
app.add_typer(users.app, name="users")
```

Follows the existing `scorecards` / `scorecards_commands/exemptions` pattern.

## Command: `cortex users roles list`

### Options

Standard `ListCommandOptions`:
- `--page, -p`: Page number (omit to fetch all pages)
- `--page-size, -z`: Results per page (default 250)
- `--table`: Table output
- `--csv`: CSV output
- `--columns, -C`: Column selection
- `--no-headers`: Suppress table/CSV headers
- `--filter, -F`: Row filtering
- `--sort, -S`: Row sorting

Custom:
- `--email, -e`: Repeatable option to filter by email address(es). Multiple values joined comma-separated for the API.

### Default Table Columns

When `--table` or `--csv` is used without explicit `--columns`:
- `Email=email`
- `Name=name`
- `Roles=roles`

### Behavior

- No `--page`: calls `client.fetch()` for all pages
- With `--page`: calls `client.get()` for a single page
- Output via `print_output_with_context()`

## Future Extensibility

- `users.py` will host top-level user commands (e.g., `users get`, `users create`)
- `users_commands/roles.py` will host additional role commands (e.g., `roles create`, `roles update`)

## Testing

Integration tests against jeff-sandbox tenant with Okta SCIM provisioned users. Test file: `tests/test_users.py`.

Tests:
- `test_users_roles_list`: List all user role assignments
- `test_users_roles_list_with_email_filter`: Filter by one or more emails
- `test_users_roles_list_table_output`: Verify table formatting
- `test_users_roles_list_pagination`: Single page retrieval
50 changes: 50 additions & 0 deletions tests/test_users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import pytest
from tests.helpers.utils import *

def test_users_roles_list():
response = cli(["users", "roles", "list"])
assert "users" in response, "Response should contain 'users' key"
assert len(response["users"]) > 0, "Should have at least one user with role assignments"

first_user = response["users"][0]
assert "email" in first_user, "User should have an email field"
assert "name" in first_user, "User should have a name field"
assert "roles" in first_user, "User should have a roles field"

def test_users_roles_list_with_email_filter():
# First, get a valid email from the full list
response = cli(["users", "roles", "list"])
email = response["users"][0]["email"]

# Filter by that email
response = cli(["users", "roles", "list", "-e", email])
assert len(response["users"]) == 1, "Should return exactly one user"
assert response["users"][0]["email"].lower() == email.lower(), "Returned user email should match filter"

def test_users_roles_list_with_multiple_email_filter():
# Get two emails from the full list
response = cli(["users", "roles", "list"])
if len(response["users"]) < 2:
pytest.skip("Need at least 2 users to test multiple email filter")

email1 = response["users"][0]["email"]
email2 = response["users"][1]["email"]

response = cli(["users", "roles", "list", "-e", email1, "-e", email2])
assert len(response["users"]) == 2, "Should return exactly two users"
returned_emails = {u["email"].lower() for u in response["users"]}
assert email1.lower() in returned_emails, "First filtered email should be in results"
assert email2.lower() in returned_emails, "Second filtered email should be in results"

def test_users_roles_list_table_output():
response = cli(["users", "roles", "list", "--table"], ReturnType.STDOUT)
assert "Email" in response, "Table output should contain Email header"
assert "Name" in response, "Table output should contain Name header"
assert "Roles" in response, "Table output should contain Roles header"

def test_users_roles_list_pagination():
response = cli(["users", "roles", "list", "-p", "0", "-z", "1"])
assert "users" in response, "Response should contain 'users' key"
assert len(response["users"]) <= 1, "Should return at most 1 user per page"
assert "page" in response, "Response should contain 'page' field"
assert "total" in response, "Response should contain 'total' field"