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
4 changes: 3 additions & 1 deletion .github/workflows/pre-commit-cache.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ on: # yamllint disable-line rule:truthy
# GitHub Actions does not support the non-standard syntax @monthly, @weekly, @daily, etc.
# - cron: "15 0 * * *" # daily at 00:15 UTC
- cron: "15 0 1 * *" # low activity repos can do just the first day of the month at 00:15 UTC
# to have an ability to run the workflow manually
workflow_dispatch: # yamllint disable-line rule:empty-values

permissions:
contents: write # required for trivy to upload artifacts
Expand All @@ -21,7 +23,7 @@ jobs:
steps:
- name: Mint ik-code-reader installation token
id: code_reader_token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0 - https://github.com/actions/create-github-app-token/releases
with:
client-id: ${{ vars.IK_CODE_READER_CLIENT_ID }}
private-key: ${{ secrets.IK_CODE_READER_PEM }}
Expand Down
15 changes: 13 additions & 2 deletions .github/workflows/release-notes.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ name: Release notes
on: # yamllint disable-line rule:truthy
push:
branches: ["dev", "develop", "main", "master"]
# to have an ability to run the workflow manually
workflow_dispatch: # yamllint disable-line rule:empty-values

# https://github.com/googleapis/release-please-action?tab=readme-ov-file#workflow-permissions
permissions:
Expand All @@ -19,7 +21,7 @@ jobs:
steps:
- name: Mint ik-code-writer installation token
id: code_writer_token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0 - https://github.com/actions/create-github-app-token/releases
with:
client-id: ${{ vars.IK_CODE_WRITER_CLIENT_ID }}
private-key: ${{ secrets.IK_CODE_WRITER_PEM }}
Expand All @@ -44,6 +46,15 @@ jobs:
PR_NUMBER=$(echo "${PR_JSON}" | jq -r '.number')
PR_TITLE=$(echo "${PR_JSON}" | jq -r '.title')

if [[ -z "${RELEASE_VERSION}" && -z "${PR_JSON}" ]]; then
echo "no release pending"
{
echo "version="
echo "pr_number="
} >> "$GITHUB_OUTPUT"
exit 0
fi

if [[ -z "${RELEASE_VERSION}" ]]; then
# RELEASE_VERSION="$(jq -r '.["."]' .release-please-manifest.json)" # <- requires code checkout
RELEASE_VERSION="$(echo "${PR_TITLE}" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')" # another option: | awk '{print $NF}'
Expand All @@ -56,7 +67,7 @@ jobs:

- name: actions/slack-notification
uses: slackapi/slack-github-action@dcb1066f776dd043e64d0e8ba94ca15cc7e1875d # v4.0.0 - https://github.com/slackapi/slack-github-action/releases
if: steps.release.outputs.releases_created || steps.release.outputs.prs_created
if: steps.release.outputs.releases_created == 'true' || steps.release.outputs.prs_created == 'true'
with:
token: ${{ secrets.SLACK_BOT_TOKEN }}
method: chat.postMessage
Expand Down
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ repos:
name: pre-commit/check-useless-excludes

- repo: ssh://git@github.com/indykite/ops-common
rev: "803c07b33cb5dd02a2301b259e81df655e1504ec" # using a branch name (or HEAD) is not supported
rev: "9e1d048e5eb2b8ecfda00a64315ef605f9e833a9" # using a branch name (or HEAD) is not supported
hooks:
# POST-CHECKOUT
# install explicitly with: pre-commit install -t post-checkout
Expand All @@ -65,7 +65,7 @@ repos:
#
# Renovate
- repo: https://github.com/renovatebot/pre-commit-hooks # ci:ignore:OK to validate locally only, otherwise it adds 850MB+ to unpacked cache
rev: "44.151.2" # frozen: 43.285.7
rev: "44.182.2"
hooks:
- id: renovate-config-validator
args: [--strict]
Expand Down
618 changes: 310 additions & 308 deletions Pipfile.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion indykite_sdk/audit/_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def list_spec(path: str, project_id: str, cursor: str | None, page_size: int | N


def jwks_spec(project_id: str) -> RequestSpec:
"""Build the JWKS request; ``project_id`` is required but does not select a key today.
"""Build the JWKS request for the keys that signed for ``project_id``.

The endpoint is public, so the request carries no credential.
"""
Expand Down
6 changes: 5 additions & 1 deletion indykite_sdk/audit/aio.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,11 @@ def iter_checkpoints(
)

async def jwks(self, project_id: str, *, timeout: Timeout = None) -> KeySet:
"""The public keys behind every signature (``GET /.well-known/jwks.json``); sent without a credential."""
"""The public keys behind every signature (``GET /.well-known/jwks.json``); sent without a credential.

Every key that ever signed for ``project_id``, current and retired, by
``kid``; see :meth:`indykite_sdk.AuditClient.jwks`.
"""
response = await self._send(_ops.jwks_spec(project_id), timeout=timeout)
return KeySet.model_validate(response.json())

Expand Down
14 changes: 10 additions & 4 deletions indykite_sdk/audit/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,10 +139,16 @@ def jwks(self, project_id: str, *, timeout: Timeout = None) -> KeySet:
"""The public keys behind the batch, manifest and checkpoint signatures (``GET /.well-known/jwks.json``).

The endpoint is public: the request carries no credential, and
:func:`fetch_jwks` reads the same keys without a client.
``project_id`` is required but does not select a key today.
Keep the set next to an exported trail: it names the keys the export
was signed with, and a ``kid`` missing from a later set was rotated out.
:func:`fetch_jwks` reads the same keys without a client. The set holds
every key that ever signed for ``project_id``, current and retired, by
``kid``, so any item of the trail can be verified against it. A project
with no signed data yet has no keys, and a project's first key, or one
added by a rotation, can take up to two minutes to appear. Keep the set
next to an exported trail.

The platform answers 503 with ``Retry-After`` while it loads a large key
set; the client retries those on its own, waiting the time the header
asks for, up to its ``max_attempts``.
"""
return KeySet.model_validate(self._send(_ops.jwks_spec(project_id), timeout=timeout).json())

Expand Down
33 changes: 29 additions & 4 deletions indykite_sdk/config/aio.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,12 @@ async def create_project(
db_connection: dict[str, Any] | None = None,
timeout: Timeout = None,
) -> CreateResult:
"""Create a project."""
"""Create a project with its own IKG.

``ikg_schema_version`` defaults to ``v1`` and cannot be changed after
creation. :data:`~indykite_sdk.config.IkgSchemaVersion` lists the values
the API defines; only ``v1`` is accepted today.
"""
body = {
"name": name,
"organization_id": organization_id,
Expand Down Expand Up @@ -580,7 +585,12 @@ async def create_audit_signing(
description: str | None = None,
timeout: Timeout = None,
) -> CreateResult:
"""Create an audit-signing configuration for a project."""
"""Create an audit-signing configuration for a project.

A project holds at most one: creating a second one raises
:class:`~indykite_sdk.ConflictError` (HTTP 409), so change the existing
one with :meth:`update_audit_signing` instead.
"""
body = {
"name": name,
"project_id": project_id,
Expand Down Expand Up @@ -718,7 +728,17 @@ async def list_token_introspects(
return await self._list("/token-introspects", ConfigResource, params, timeout)

async def create_token_introspect(self, body: dict[str, Any], *, timeout: Timeout = None) -> CreateResult:
"""Create a token-introspect configuration from its documented JSON body."""
"""Create a token-introspect configuration from its documented JSON body.

``ikg_node_type`` is required in projects on IKG schema ``v1``; projects
on schema ``v2`` require ``label`` and ``id_key`` instead. The body pairs
one matcher (``jwt_matcher`` or ``opaque_matcher``) with at most one
validation mode: ``offline_validation`` (local JWT signature
checks; ``discovery`` is ``oidc`` or ``rfc8414``), ``online_validation``
(userinfo endpoint) or ``online_oauth2_validation`` (RFC 7662
introspection endpoint with write-only ``secret_basic.client_secret``).
See :meth:`indykite_sdk.ConfigClient.create_token_introspect`.
"""
return await self._create("/token-introspects", body, timeout)

async def read_token_introspect(
Expand All @@ -735,7 +755,12 @@ async def read_token_introspect(
async def update_token_introspect(
self, introspect_id: str, body: dict[str, Any], *, etag: str, timeout: Timeout = None
) -> UpdateResult:
"""Update a token-introspect configuration (``If-Match`` guarded)."""
"""Update a token-introspect configuration (``If-Match`` guarded).

An ``online_oauth2_validation`` update may leave ``client_secret`` out
to keep the stored secret, unless ``introspection_endpoint`` or
``client_id`` changes.
"""
return await self._update("/token-introspects", introspect_id, body, etag, timeout)

async def delete_token_introspect(self, introspect_id: str, *, etag: str, timeout: Timeout = None) -> DeleteResult:
Expand Down
40 changes: 37 additions & 3 deletions indykite_sdk/config/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,9 @@ def create_project(
) -> CreateResult:
"""Create a project with its own IKG.

``ikg_schema_version`` defaults to ``v1`` and cannot be changed after creation.
``ikg_schema_version`` defaults to ``v1`` and cannot be changed after
creation. :data:`~indykite_sdk.config.IkgSchemaVersion` lists the values
the API defines; only ``v1`` is accepted today.
"""
body = {
"name": name,
Expand Down Expand Up @@ -627,6 +629,10 @@ def create_audit_signing(
require ``key_resource`` and ``kid``, plus whatever ``auth_params`` the
provider needs to reach the key. ``auth_params`` values are write-only:
reads return the keys with blank values.

A project holds at most one audit-signing configuration: creating a
second one raises :class:`~indykite_sdk.ConflictError` (HTTP 409), so
change the existing one with :meth:`update_audit_signing` instead.
"""
body = {
"name": name,
Expand Down Expand Up @@ -767,7 +773,30 @@ def list_token_introspects(
return self._list("/token-introspects", ConfigResource, params, timeout)

def create_token_introspect(self, body: dict[str, Any], *, timeout: Timeout = None) -> CreateResult:
"""Create a token-introspect configuration from its documented JSON body."""
"""Create a token-introspect configuration from its documented JSON body.

``ikg_node_type`` names the IKG node type the token's subject is matched
to; it is required in projects on IKG schema ``v1``. Projects on schema
``v2`` instead require ``label`` (the node label) and ``id_key`` (the
node property holding the subject claim), and ignore ``ikg_node_type``.

The body pairs one matcher (``jwt_matcher`` or ``opaque_matcher``) with
at most one validation mode:

- ``offline_validation`` checks JWT signatures locally against
``public_jwks``, or against the keys published by the issuer when no
keys are given. ``discovery`` names the metadata document that
locates them: ``oidc`` (the default, OpenID Connect discovery) or
``rfc8414`` (OAuth 2.0 authorization-server metadata). JWT tokens only.
- ``online_validation`` calls the issuer's userinfo endpoint; works with
JWT and opaque tokens.
- ``online_oauth2_validation`` calls an RFC 7662 introspection endpoint
with HTTP Basic client credentials, e.g.
``{"introspection_endpoint": "https://idp.example.com/introspect",
"secret_basic": {"client_id": "...", "client_secret": "..."}, "cache_ttl": 300}``.
Works with JWT and opaque tokens. ``client_secret`` is write-only and
required on create; reads return ``secret_basic`` with ``client_id`` only.
"""
return self._create("/token-introspects", body, timeout)

def read_token_introspect(
Expand All @@ -784,7 +813,12 @@ def read_token_introspect(
def update_token_introspect(
self, introspect_id: str, body: dict[str, Any], *, etag: str, timeout: Timeout = None
) -> UpdateResult:
"""Update a token-introspect configuration (``If-Match`` guarded)."""
"""Update a token-introspect configuration (``If-Match`` guarded).

An ``online_oauth2_validation`` update may leave ``client_secret`` out
to keep the stored secret, unless ``introspection_endpoint`` or
``client_id`` changes.
"""
return self._update("/token-introspects", introspect_id, body, etag, timeout)

def delete_token_introspect(self, introspect_id: str, *, etag: str, timeout: Timeout = None) -> DeleteResult:
Expand Down
2 changes: 2 additions & 0 deletions indykite_sdk/config/models/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
ConfigStatus = Literal["ACTIVE", "INACTIVE", "DRAFT"]

#: IKG schema version of a project; set once at creation (default ``v1``).
#: ``v2`` is rejected until the platform supports it.
IkgSchemaVersion = Literal["v1", "v2"]

#: Role of a service account.
Expand Down Expand Up @@ -78,6 +79,7 @@ class Project(_AuditedResource):
replica_region: str | None = None
ikg_size: str | None = None
ikg_status: str | None = None
#: One of :data:`IkgSchemaVersion`; cannot change after creation.
ikg_schema_version: str | None = None
db_connection: dict[str, Any] | None = None

Expand Down
Loading