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
8 changes: 6 additions & 2 deletions docs/cli/reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -278,15 +278,15 @@ nemo auth access-keys [OPTIONS] COMMAND [ARGS]...

**Commands:**

* `create`: Create a Scoped Access Key for the currently...
* `create`: Create a user-bound or service-bound Scoped Access Key.
* `list`: List Scoped Access Keys owned by the currently...
* `revoke`: Revoke a Scoped Access Key owned by the currently...
* `suspend`: Temporarily suspend a Scoped Access Key owned by the...
* `unsuspend`: Restore a suspended Scoped Access Key owned by the...

##### nemo auth access-keys create

Create a Scoped Access Key for the currently authenticated user.
Create a user-bound or service-bound Scoped Access Key.

**Usage:**

Expand All @@ -299,6 +299,7 @@ nemo auth access-keys create [OPTIONS]
* `--name, -n`: Optional human-readable label for the Scoped Access Key.
* `--description, -d`: Optional description for the Scoped Access Key.
* `--expires-in`: Scoped Access Key lifetime in seconds. Use 'none' to request no expiration.
* `--service-account`: Bind the key to a non-human service account (PlatformAdmin only).

**Help:**

Expand All @@ -308,6 +309,9 @@ nemo auth access-keys create [OPTIONS]

List Scoped Access Keys owned by the currently authenticated user.

PlatformAdmins also see every service-bound Scoped Access Key, not just the
ones they personally created.

**Usage:**

```shell
Expand Down
31 changes: 31 additions & 0 deletions openapi/ga/individual/platform.openapi.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

31 changes: 31 additions & 0 deletions openapi/ga/openapi.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

31 changes: 31 additions & 0 deletions openapi/openapi.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -850,12 +850,20 @@ def create_access_key(
help="Scoped Access Key lifetime in seconds. Use 'none' to request no expiration.",
),
] = None,
service_account: Annotated[
str | None,
typer.Option(
"--service-account",
help="Bind the key to a non-human service account (PlatformAdmin only).",
),
] = None,
) -> None:
"""Create a Scoped Access Key for the currently authenticated user."""
"""Create a user-bound or service-bound Scoped Access Key."""
expires_in_was_set, parsed_expires_in = _parse_access_key_expires_in(expires_in)
request = AccessKeyCreateRequest(
name=name,
description=description,
service_account_id=service_account,
**({"expires_in_seconds": parsed_expires_in} if expires_in_was_set else {}),
)
try:
Expand All @@ -877,7 +885,11 @@ def list_access_keys(
typer.Option("--page-size", min=1, max=100, help="Number of keys to retrieve per page."),
] = 100,
) -> None:
"""List Scoped Access Keys owned by the currently authenticated user."""
"""List Scoped Access Keys owned by the currently authenticated user.

PlatformAdmins also see every service-bound Scoped Access Key, not just the
ones they personally created.
"""
try:
listed = _access_key_issuer(ctx).list(page=page, page_size=page_size)
except AccessKeyFeatureDisabledError as exc:
Expand All @@ -894,6 +906,8 @@ def list_access_keys(
Column("jti", None),
Column("name", None),
Column("description", None),
Column("entity_type", None),
Column("principal", None),
Column("status", None),
Column("issuer", None),
Column("audiences", None),
Expand Down
21 changes: 21 additions & 0 deletions packages/nemo_platform_ext/tests/cli/commands/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,27 @@ def test_auth_access_keys_create_sends_optional_metadata_and_expiration(monkeypa
)


def test_auth_access_keys_create_sends_service_account(monkeypatch: pytest.MonkeyPatch):
fake_platform_client = MagicMock()
fake_access_keys_client = MagicMock()
fake_access_keys_client.create_access_key.return_value.data.return_value = _created_access_key("otel")
monkeypatch.setattr("nemo_platform_ext.cli.core.context.CLIContext.get_client", lambda self: fake_platform_client)
monkeypatch.setattr(
"nemo_platform_ext.cli.commands.auth.client_from_platform",
lambda platform, client_cls: fake_access_keys_client,
)

result = runner.invoke(
app,
["auth", "access-keys", "create", "--name", "otel", "--service-account", "otel-collector"],
)

assert_exit_code(result, 0)
fake_access_keys_client.create_access_key.assert_called_once_with(
body=AccessKeyCreateRequest(name="otel", service_account_id="otel-collector")
)


def test_auth_access_keys_create_sends_explicit_null_expiration(monkeypatch: pytest.MonkeyPatch):
fake_platform_client = MagicMock()
fake_access_keys_client = MagicMock()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

AccessKeyStatus = Literal["ACTIVE", "EXPIRED", "REVOKED", "SUSPENDED"]
AccessKeyReversibleStatus = Literal["ACTIVE", "EXPIRED", "SUSPENDED"]
AccessKeyEntityType = Literal["USER", "SERVICE_ACCOUNT"]


class AccessKeyListQueryParams(TypedDict, total=False):
Expand Down Expand Up @@ -46,6 +47,17 @@ class AccessKeyCreateRequest(BaseModel):
"to be disabled."
),
)
service_account_id: str | None = Field(
default=None,
min_length=1,
max_length=240,
pattern=r"^[a-zA-Z0-9][a-zA-Z0-9._+/-]*$",
json_schema_extra={"nullable": True},
description=(
"Optional non-human service account to bind the key to. Service-bound keys can only be "
"created by a PlatformAdmin and authenticate as service-account:<id>."
),
)


class AccessKeyMetadataResponse(BaseModel):
Expand All @@ -63,6 +75,10 @@ class AccessKeyMetadataResponse(BaseModel):
description="Human-readable description of the Scoped Access Key.",
)
principal: str = Field(description="Principal ID stamped into the token.")
entity_type: AccessKeyEntityType = Field(
default="USER",
description="Whether the key is bound to a user or a non-human service account.",
)
status: AccessKeyStatus
issuer: str = Field(description="Issuer stamped into the Scoped Access Key JWT.")
audiences: list[str] = Field(
Expand Down
Loading
Loading