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
16 changes: 16 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,21 @@ jobs:
echo "code=${{ steps.filter.outputs.code }}" >> $GITHUB_OUTPUT
fi

lint:
name: Lint
needs: [changes]
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Install Task
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0

- name: Lint Python SDK
run: task lint

test:
name: Test
needs:
Expand All @@ -79,6 +94,7 @@ jobs:
if: ${{ !cancelled() && !contains(needs.*.result, 'cancelled') && !contains(needs.*.result, 'failure') }}
needs:
- changes
- lint
- test
- release
runs-on: ubuntu-latest
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/reusable-release-sdk.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ jobs:

- name: Build the Python package
run: |
task sdk:build:python
task build

- name: Publish the Python SDK
env:
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
run: |
task sdk:release:python
task release
2 changes: 1 addition & 1 deletion .github/workflows/reusable-test-sdk.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ jobs:
env:
CLIENT_ID: "https://github.com/${{ github.repository }}/.github/workflows/reusable-test-sdk.yaml@${{ github.ref }}"
run: |
task sdk:deps:cicd:iodc-token-generation >> $GITHUB_ENV
task deps:cicd:iodc-token-generation >> $GITHUB_ENV

- name: Test Python SDK
env:
Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ __pycache__/
*.pyd
.venv/

# Caches
.pytest_cache
.ruff_cache
.mypy_cache

# MacOS
.DS_Store

Expand Down
105 changes: 65 additions & 40 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,49 +26,33 @@ tasks:
# SDK tasks (top-level, user-facing)
# ---------------------------------------------------------------------------

sdk:deps:cicd:iodc-token-generation:
desc: Get Fulcio OIDC token for CICD
requires:
vars: [CLIENT_ID]
cmds:
- |
OIDC_TOKEN=$(curl -s -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=sigstore" | jq -r '.value')

CLIENT_ID="{{.CLIENT_ID}}"
PROVIDER_URL="https://token.actions.githubusercontent.com"

echo "OIDC_PROVIDER_URL=${PROVIDER_URL}"
echo "CLIENT_ID=${CLIENT_ID}"
echo "OIDC_TOKEN=${OIDC_TOKEN}"

sdk:build:python:
build:
desc: Build python client SDK package
deps:
- task: sdk:deps:python
- task: deps
cmds:
- "{{.UV_BIN}} build"

sdk:test-env:create:
test-env:create:
desc: Create Kubernetes cluster test environment
cmds:
- task: test-env:kubernetes:local
- task: test-env:deploy
vars:
DIRECTORY_SERVER_PUBLICATION_SCHEDULER_INTERVAL: 1s
DIRECTORY_SERVER_OASF_API_VALIDATION_SCHEMA_URL: "https://schema.oasf.outshift.com"

sdk:test-env:delete:
test-env:delete:
desc: Delete Kubernetes cluster test environment
cmds:
- task: test-env:kubernetes:local:cleanup
- task: test-env:deploy:cleanup

sdk:test:python:
desc: Test python client SDK package
deps:
- task: sdk:deps:python
- task: deps
cmds:
- defer: { task: sdk:test-env:delete }
- task: sdk:test-env:create
- defer: { task: test-env:delete }
- task: test-env:create
- |
export DIRCTL_IMAGE_TAG="{{ .DIRCTL_IMAGE_TAG }}"
export COSIGN_PATH="$(printf "%s" "${COSIGN_PATH:-{{ .COSIGN_BIN }}}")"
Expand All @@ -81,7 +65,7 @@ tasks:
'{{ .UV_BIN }}' sync
'{{ .UV_BIN }}' run python example.py

sdk:deps:python:
deps:
desc: Install deps for python SDK package
deps:
- task: deps:uv
Expand All @@ -90,7 +74,7 @@ tasks:
cmds:
- "{{.UV_BIN}} sync --all-packages"

sdk:deps:python:example:
deps:example:
desc: Install deps for Python SDK example package
internal: true
dir: ./examples
Expand All @@ -99,13 +83,38 @@ tasks:
cmds:
- "{{.UV_BIN}} sync --all-packages"

sdk:tidy:
deps:packages:
desc: Install Python package dependencies (uv sync only)
internal: true
deps:
- task: deps:uv
cmds:
- "{{.UV_BIN}} sync --all-packages"

lint:
desc: Lint Python SDK (ruff check + format check + mypy)
deps:
- task: deps:packages
cmds:
- "{{.UV_BIN}} run ruff check dir-sdk-python examples"
- "{{.UV_BIN}} run ruff format --check dir-sdk-python examples"
- "{{.UV_BIN}} run mypy dir-sdk-python examples"

lint:fix:
desc: Auto-fix Python lint and formatting (local dev)
deps:
- task: deps:packages
cmds:
- "{{.UV_BIN}} run ruff check --fix dir-sdk-python examples"
- "{{.UV_BIN}} run ruff format dir-sdk-python examples"

deps:tidy:
desc: Install deps for SDK packages
cmds:
- task: sdk:deps:python
- task: sdk:deps:python:example
- task: deps
- task: deps:example

sdk:release:python:
release:
desc: Release python client SDK package
env:
UV_PUBLISH_TOKEN: "{{ .UV_PUBLISH_TOKEN }}"
Expand All @@ -124,7 +133,7 @@ tasks:
# Test environment (kind cluster + helm deploy of dir-server)
# ---------------------------------------------------------------------------

test-env:kubernetes:setup-cluster:
test-env:setup-cluster:
desc: Create a kind cluster and load Docker images
deps:
- deps:helm
Expand Down Expand Up @@ -162,11 +171,11 @@ tasks:
{{ .KIND_BIN }} export kubeconfig --name {{ .KIND_CLUSTER_NAME }}
{{ .KUBECTL_BIN }} cluster-info

test-env:kubernetes:local:
test-env:deploy:
aliases: [deploy:local]
desc: Deploy a local Directory server in Kubernetes
deps:
- task: test-env:kubernetes:setup-cluster
- task: test-env:setup-cluster
vars:
KIND_EXTRA_PORT_MAPPING: "{{.API_HOSTPORT}}:{{.API_NODEPORT}},{{.METRICS_HOSTPORT}}:{{.METRICS_NODEPORT}}"
vars:
Expand All @@ -184,11 +193,11 @@ tasks:
# TODO: make logic idempotent so that running functional tests does not change previous contexts

# Generate credentials and htpasswd file (using defaults)
- task: test-env:kubernetes:gen-htpasswd-creds
- task: test-env:htpasswd-creds:gen

# Cleanup credentials on exit (using defaults)
- defer:
task: test-env:kubernetes:cleanup-htpasswd-creds
task: test-env:htpasswd-creds:cleanup

# Deploy chart
- |
Expand Down Expand Up @@ -226,7 +235,7 @@ tasks:
--wait-for-jobs \
--timeout "15m"

test-env:kubernetes:local:cleanup:
test-env:deploy:cleanup:
desc: Cleanup Kubernetes environment for local deployment
deps:
- deps:kind
Expand All @@ -235,7 +244,7 @@ tasks:
cmds:
- "{{ .KIND_BIN }} delete cluster --name {{ .KIND_CLUSTER_NAME }}"

test-env:kubernetes:gen-htpasswd-creds:
test-env:htpasswd-creds:gen:
desc: Generate htpasswd credentials and files for DIR deployment
vars:
HTPASSWD_USERNAME: '{{ .HTPASSWD_USERNAME | default "apiserver" }}'
Expand Down Expand Up @@ -268,7 +277,7 @@ tasks:
{{ .HTPASSWD_SYNC_USERNAME }}:${HTPASSWD_SYNC}
EOF

test-env:kubernetes:cleanup-htpasswd-creds:
test-env:htpasswd-creds:cleanup:
desc: Cleanup htpasswd credentials and files
vars:
CREDS_FILE: '{{ .CREDS_FILE | default "/tmp/dir-htpasswd-creds.env" }}'
Expand Down Expand Up @@ -389,9 +398,25 @@ tasks:
RENOVATE_OPTS_DEFAULT: >-
--persist-repo-data 'true'
--allowed-post-upgrade-commands '[".*"]'
--post-upgrade-tasks '{"commands": ["task sdk:tidy"], "executionMode": "branch"}'
--post-upgrade-tasks '{"commands": ["task deps:tidy"], "executionMode": "branch"}'
RENOVATE_OPTS: "{{ .RENOVATE_OPTS | default .RENOVATE_OPTS_DEFAULT }}"
RENOVATE_PLATFORM_EFFECTIVE:
sh: printf '%s' "${RENOVATE_PLATFORM:-local}"
cmds:
- renovate {{ .RENOVATE_OPTS }} --platform "{{ .RENOVATE_PLATFORM_EFFECTIVE }}"

deps:cicd:iodc-token-generation:
desc: Get Fulcio OIDC token for CICD
requires:
vars: [CLIENT_ID]
cmds:
- |
OIDC_TOKEN=$(curl -s -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=sigstore" | jq -r '.value')

CLIENT_ID="{{.CLIENT_ID}}"
PROVIDER_URL="https://token.actions.githubusercontent.com"

echo "OIDC_PROVIDER_URL=${PROVIDER_URL}"
echo "CLIENT_ID=${CLIENT_ID}"
echo "OIDC_TOKEN=${OIDC_TOKEN}"
4 changes: 3 additions & 1 deletion dir-sdk-python/agntcy/dir_sdk/client/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Copyright AGNTCY Contributors (https://github.com/agntcy)
# SPDX-License-Identifier: Apache-2.0

from agntcy.dir_sdk.client.auth.oauth_pkce import OAuthPkceError
from agntcy.dir_sdk.client.client import Client
from agntcy.dir_sdk.client.config import Config
from agntcy.dir_sdk.client.auth.oauth_pkce import OAuthPkceError as OAuthPkceError

__all__ = ["Client", "Config", "OAuthPkceError"]
13 changes: 11 additions & 2 deletions dir-sdk-python/agntcy/dir_sdk/client/auth/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,16 @@
"""Authentication/session helpers for the Directory client."""

from agntcy.dir_sdk.client.auth.oauth_pkce import OAuthTokenHolder
from agntcy.dir_sdk.client.auth.session import OAuthSessionManager, cached_token_from_response
from agntcy.dir_sdk.client.auth.session import (
OAuthSessionManager,
cached_token_from_response,
)
from agntcy.dir_sdk.client.auth.token_cache import CachedToken, TokenCache

__all__ = ["CachedToken", "OAuthSessionManager", "OAuthTokenHolder", "TokenCache", "cached_token_from_response"]
__all__ = [
"CachedToken",
"OAuthSessionManager",
"OAuthTokenHolder",
"TokenCache",
"cached_token_from_response",
]
19 changes: 9 additions & 10 deletions dir-sdk-python/agntcy/dir_sdk/client/auth/oauth_pkce.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,12 @@
import threading
import webbrowser
from http.server import BaseHTTPRequestHandler, HTTPServer
from typing import Any
from typing import Any, cast
from urllib.parse import parse_qs, urlencode, urlparse

import httpx
from authlib.oauth2.rfc7636 import create_s256_code_challenge

from agntcy.dir_sdk.client.config import Config
from authlib.oauth2.rfc7636 import create_s256_code_challenge

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -44,7 +43,7 @@ def fetch_openid_configuration(
with httpx.Client(verify=verify, timeout=timeout) as client:
response = client.get(url)
response.raise_for_status()
data = response.json()
data = cast(dict[str, Any], response.json())
if "authorization_endpoint" not in data or "token_endpoint" not in data:
msg = "OpenID configuration missing authorization_endpoint or token_endpoint"
raise OAuthPkceError(msg)
Expand All @@ -70,7 +69,7 @@ def _form_post(
detail = response.text[:500] if response.text else ""
msg = f"Token HTTP {response.status_code}: {detail}"
raise OAuthPkceError(msg) from e
return response.json()
return cast(dict[str, Any], response.json())


class OAuthTokenHolder:
Expand Down Expand Up @@ -99,7 +98,7 @@ def get_access_token(self) -> str:
"or call Client.authenticate_oauth_pkce()"
)
raise RuntimeError(msg)
return self._access_token # type: ignore[return-value]
return self._access_token


def exchange_authorization_code(
Expand Down Expand Up @@ -184,7 +183,9 @@ def do_GET(self) -> None: # noqa: N802
try:
req = urlparse(self.path)
if req.path != path:
error_holder.append("redirect path does not match oidc_redirect_uri")
error_holder.append(
"redirect path does not match oidc_redirect_uri"
)
self.send_error(404, "Not Found")
return
qs = parse_qs(req.query)
Expand Down Expand Up @@ -213,9 +214,7 @@ def do_GET(self) -> None: # noqa: N802

def _ok_page(self, message: str) -> None:
body = (
"<!DOCTYPE html><html><body><p>"
+ message
+ "</p></body></html>"
"<!DOCTYPE html><html><body><p>" + message + "</p></body></html>"
).encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
Expand Down
8 changes: 5 additions & 3 deletions dir-sdk-python/agntcy/dir_sdk/client/auth/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,21 @@

from datetime import UTC, datetime, timedelta

from agntcy.dir_sdk.client.config import Config
from agntcy.dir_sdk.client.auth.oauth_pkce import (
OAuthTokenHolder,
fetch_openid_configuration,
run_loopback_pkce_login,
)
from agntcy.dir_sdk.client.auth.token_cache import CachedToken, TokenCache
from agntcy.dir_sdk.client.config import Config


def cached_token_from_response(config: Config, payload: dict[str, object]) -> CachedToken:
def cached_token_from_response(
config: Config, payload: dict[str, object]
) -> CachedToken:
expires_at = None
expires_in = payload.get("expires_in")
if expires_in is not None:
if isinstance(expires_in, (int, float, str)):
expires_at = datetime.now(UTC) + timedelta(seconds=int(expires_in))

refresh_token = payload.get("refresh_token")
Expand Down
Loading
Loading