diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d494cd0..14010ee 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -7,6 +7,7 @@ on: - main paths: - 'cortexapps_cli/**' + - 'docker/**' - 'pyproject.toml' - 'poetry.lock' diff --git a/HISTORY.md b/HISTORY.md index 9f889ec..be45d37 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -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). +## [1.19.2](https://github.com/cortexapps/cli/releases/tag/1.19.2) - 2026-06-10 + +[Compare with 1.19.1](https://github.com/cortexapps/cli/compare/1.19.1...1.19.2) + +### 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 + +[Compare with 1.19.0](https://github.com/cortexapps/cli/compare/1.19.0...1.19.1) + +### 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 [Compare with 1.18.0](https://github.com/cortexapps/cli/compare/1.18.0...1.19.0) diff --git a/README.rst b/README.rst index 3f1f4d2..dc9c1ca 100644 --- a/README.rst +++ b/README.rst @@ -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 diff --git a/cortexapps_cli/cli.py b/cortexapps_cli/cli.py index 1327aab..5664c5c 100755 --- a/cortexapps_cli/cli.py +++ b/cortexapps_cli/cli.py @@ -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( @@ -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 diff --git a/cortexapps_cli/commands/users.py b/cortexapps_cli/commands/users.py new file mode 100644 index 0000000..cf9b26e --- /dev/null +++ b/cortexapps_cli/commands/users.py @@ -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") diff --git a/cortexapps_cli/commands/users_commands/roles.py b/cortexapps_cli/commands/users_commands/roles.py new file mode 100644 index 0000000..440264f --- /dev/null +++ b/cortexapps_cli/commands/users_commands/roles.py @@ -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) diff --git a/docker/Dockerfile b/docker/Dockerfile index 8f71e1a..3a55b9d 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -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 diff --git a/docs/superpowers/specs/2026-06-10-users-roles-command-design.md b/docs/superpowers/specs/2026-06-10-users-roles-command-design.md new file mode 100644 index 0000000..b3cb948 --- /dev/null +++ b/docs/superpowers/specs/2026-06-10-users-roles-command-design.md @@ -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 diff --git a/tests/test_users.py b/tests/test_users.py new file mode 100644 index 0000000..7d18f0a --- /dev/null +++ b/tests/test_users.py @@ -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"