capauth 0.3.7__tar.gz → 0.3.8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {capauth-0.3.7 → capauth-0.3.8}/CHANGELOG.md +12 -0
- {capauth-0.3.7 → capauth-0.3.8}/PKG-INFO +1 -1
- {capauth-0.3.7 → capauth-0.3.8}/docs/SKDASHBOARD_CONTROL_PLANE_POLICY.md +12 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/__init__.py +2 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/cli.py +16 -2
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/control_plane_authorizer.py +417 -23
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/delegated.py +89 -29
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/tokens.py +26 -3
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth.egg-info/PKG-INFO +1 -1
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth.egg-info/SOURCES.txt +1 -0
- capauth-0.3.8/tests/test_audience_ttl_seconds.py +128 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_control_plane_authorizer.py +298 -3
- {capauth-0.3.7 → capauth-0.3.8}/.cursorrules +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/.dockerignore +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/.github/workflows/ci.yml +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/.github/workflows/docs-check.yml +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/.github/workflows/nextcloud-appstore.yml +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/.github/workflows/publish.yml +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/.github/workflows/pytest.yml +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/.github/workflows/secret-scan.yml +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/.gitignore +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/.gitleaks-baseline.json +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/AGENTS.md +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/AI-ADVOCATE.md +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/ARCHITECTURE.md +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/CLAUDE.md +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/CODE_OF_CONDUCT.md +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/CONTRIBUTING.md +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/Dockerfile +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/Dockerfile.authentik-capauth +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/LICENSE +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/MANIFEST.in +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/MISSION.md +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/README.md +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/SECURITY.md +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/SKILL.md +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/SOP.md +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/authentik-custom/capauth_migrate.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/authentik-custom/imghdr.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/authentik-custom/user_settings.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/bin/cli.js +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/browser-extension/.gitignore +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/browser-extension/README.md +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/browser-extension/RELEASING.md +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/browser-extension/background.js +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/browser-extension/content_scripts/autofill.js +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/browser-extension/content_scripts/cross_origin_signing.js +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/browser-extension/content_scripts/detector.js +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/browser-extension/content_scripts/provider.js +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/browser-extension/content_scripts/provider_bridge.js +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/browser-extension/icons/icon.svg +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/browser-extension/icons/icon128.png +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/browser-extension/icons/icon16.png +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/browser-extension/icons/icon48.png +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/browser-extension/lib/bunker-e2e.js +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/browser-extension/lib/keyqr.js +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/browser-extension/lib/keyvault.js +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/browser-extension/lib/openpgp.js +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/browser-extension/lib/signer-backends.js +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/browser-extension/manifest.firefox.json +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/browser-extension/manifest.json +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/browser-extension/native-host/capauth_signer.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/browser-extension/native-host/com.capauth.signer.json +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/browser-extension/native-host/install.sh +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/browser-extension/options/options.html +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/browser-extension/options/options.js +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/browser-extension/package-lock.json +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/browser-extension/package.json +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/browser-extension/popup/popup.css +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/browser-extension/popup/popup.html +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/browser-extension/popup/popup.js +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/browser-extension/scripts/build.js +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/browser-extension/scripts/generate-icons.js +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/browser-extension/stages/AuthenticatorSelectionStage.js +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/browser-extension/stages/CapAuthStage.js +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/browser-extension/stages/QRDisplayStage.js +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/browser-extension/stages/SignatureVerificationStage.js +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/browser-extension/stages/StagePipeline.js +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/browser-extension/submission/STORE_LISTING.md +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/browser-extension/submission/privacy-policy.md +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/browser-extension/tests/fixtures/test_key.json +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/browser-extension/tests/unit/background.test.js +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/browser-extension/tests/unit/bunker_e2e.test.js +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/browser-extension/tests/unit/detector.test.js +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/browser-extension/tests/unit/keyqr.test.js +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/browser-extension/tests/unit/keyvault.test.js +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/browser-extension/tests/unit/openpgp.test.js +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/browser-extension/tests/unit/provider_api.test.js +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/browser-extension/tests/unit/provider_tierb.test.js +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/browser-extension/tests/unit/remote_bunker.test.js +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/browser-extension/tests/unit/signer_backends.test.js +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/browser-extension/vitest.config.js +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/deploy/capauth-service/.env.example +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/deploy/capauth-service/deploy.sh +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/deploy/capauth-service/docker-compose.yml +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/deploy/capauth-service/systemd/capauth-backup.service +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/deploy/capauth-service/systemd/capauth-backup.timer +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/deploy/forgejo-capauth/.env.example +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/deploy/forgejo-capauth/docker-compose.yml +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/deploy/forgejo-capauth/preflight.sh +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/docs/AIRGAPPED_SIGNING_WORKSTATION.md +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/docs/ARCHITECTURE.md +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/docs/AUTHENTIK_CUSTOM_STAGE.md +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/docs/AUTHENTIK_DEPLOYMENT_SKSSO.md +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/docs/AUTHENTIK_FORGEJO_DEPLOYMENT.md +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/docs/AUTHENTIK_STRATEGY_DECISION.md +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/docs/CAPAUTH_BUNKER_REMOTE_SIGNER.md +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/docs/CAPAUTH_OIDC_IDP.md +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/docs/CLAIMS.md +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/docs/COLD_MACHINE_BOOTSTRAP_AND_DR.md +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/docs/CRYPTO_SPEC.md +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/docs/DELEGATED_CAPABILITIES.md +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/docs/DOWNSTREAM_DURABLE_BACKENDS.md +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/docs/ENTERPRISE_MANAGED_KEYS.md +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/docs/HUMAN_IDENTITY_SETUP_AND_ROTATION.md +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/docs/IDENTITY_ESTATE.md +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/docs/INTEGRATION_BLUEPRINT.md +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/docs/M0-capauth-api-v1.md +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/docs/M0-capauth-importer-inventory.md +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/docs/PQC_CAPAUTH_REVISIT.md +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/docs/PQC_ROOT_MIGRATION.md +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/docs/PROTOCOL.md +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/docs/ROOT_ROTATION_CEREMONY.md +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/docs/TRUST_GRAPH_COORD_PROJECTION.md +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/docs/ZERO_KNOWLEDGE_PROFILE.md +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/docs/authentik-capauth.md +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/docs/deploy-plan/capauth-bulletproof-deploy.md +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/index.d.ts +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/index.js +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/openclaw-plugin.archived-2026-04-23/package.json +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/openclaw-plugin.archived-2026-04-23/src/index.ts +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/openclaw-plugin.archived-2026-04-23/src/openclaw.plugin.json +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/package.json +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/phone-signer/app.js +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/phone-signer/icons/icon-192.png +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/phone-signer/icons/icon-512.png +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/phone-signer/index.html +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/phone-signer/lib/bunker-e2e.js +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/phone-signer/lib/bunker-signer.js +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/phone-signer/lib/canonical.js +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/phone-signer/lib/keyqr.js +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/phone-signer/lib/keyvault.js +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/phone-signer/manifest.webmanifest +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/phone-signer/sw.js +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/phone-signer/vendor/openpgp.min.js +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/phone-signer/vendor/qrcode-generator.js +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/pyproject.toml +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/ruff-0.15.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/scripts/build_authentik_capauth_image.sh +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/scripts/capauth-backup.sh +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/scripts/capauth-restore.sh +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/scripts/gitleaks-scope.sh +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/scripts/migrate_canonical_subjects.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/scripts/pqc_ceremony_dryrun.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/scripts/provision_agent_profiles.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/setup.cfg +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/skill.yaml +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/agent_identity.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/apps.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/authentik/__init__.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/authentik/api.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/authentik/blueprints/capauth-pgp-login.yaml +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/authentik/claims_mapper.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/authentik/nonce_store.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/authentik/stage.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/authentik/urls.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/authentik/verifier.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/authz.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/control_plane.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/crypto/__init__.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/crypto/base.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/crypto/gnupg_backend.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/crypto/pgpy_backend.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/crypto/sequoia_backend.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/crypto/skpgp_backend.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/custody.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/did.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/discovery/__init__.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/discovery/base.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/discovery/file_discovery.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/discovery/mdns.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/discovery/syncthing.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/estate.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/exceptions.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/identity.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/identity_class.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integration.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/__init__.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/forgejo/__init__.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/forgejo/auth_flow.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/forgejo/cli.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/forgejo/config.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/forgejo/forgejo_api.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/forgejo/oidc_provider.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/forgejo/tests/__init__.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/forgejo/tests/test_auth_flow.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/forgejo/tests/test_cli.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/forgejo/tests/test_config.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/forgejo/tests/test_forgejo_api.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/forgejo/tests/test_oidc_provider.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/.gitignore +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/.nextcloudignore +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/CSR-PR.md +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/INFO-XML-CHANGES.md +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/PUBLISHING.md +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/README.md +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/appinfo/info.xml +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/appinfo/routes.php +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/build-release.sh +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/certificates/.gitignore +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/certificates/capauth.crt +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/certificates/capauth.csr +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/composer.json +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/css/login.css +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/js/login.js +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/krankerl.toml +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/lib/AlternativeLogin/CapAuthLogin.php +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/lib/AppInfo/Application.php +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/lib/BackgroundJob/PruneExpiredNonces.php +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/lib/Controller/LoginController.php +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/lib/Controller/TokenController.php +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/lib/Db/KeyRegistry.php +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/lib/Exception/CapAuthUnauthorizedException.php +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/lib/Middleware/PgpVerificationMiddleware.php +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/lib/Migration/Version000300Date20260301000000.php +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/lib/Provider/CapAuthProvider.php +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/lib/Service/ChallengeService.php +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/lib/Service/GroupSyncService.php +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/lib/Service/UserProvisioningService.php +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/lib/Service/VerifierService.php +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/lib/User/Backend.php +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/phpunit.xml +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/screenshots/README.md +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/screenshots/login.png +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/templates/login.php +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/tests/bootstrap.php +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/tests/integration/FullAuthFlowTest.php +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/tests/oidc/oidc_e2e_login_test.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/tests/unit/Controller/LoginControllerTest.php +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/tests/unit/Db/KeyRegistryTest.php +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/tests/unit/Provider/CapAuthProviderTest.php +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/tests/unit/Service/ChallengeServiceTest.php +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/tests/unit/Service/VerifierServiceTest.php +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/tests/unit/User/BackendTest.php +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/key_transparency.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/login.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/manifest.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/mesh.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/migrations/0001_initial.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/migrations/0002_widen_fingerprint_to_64.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/migrations/__init__.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/models.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/pairing/__init__.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/pairing/canonicalize.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/pairing/kernel.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/pairing/operator_session.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/pairing/proof.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/pairing/records.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/pairing/store.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/pairing/window.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/pma.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/pqc_confidentiality.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/pqc_identity.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/pqc_root_identity.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/pqc_tokens.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/profile.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/provisioning.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/registry.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/seal.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/service/__init__.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/service/app.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/service/bunker.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/service/bunker_e2e.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/service/keystore.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/service/oidc/__init__.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/service/oidc/clients.example.json +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/service/oidc/clients.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/service/oidc/passkey.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/service/oidc/provider.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/service/oidc/signing_key.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/service/oidc/store.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/service/push.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/service/server.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/subject.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/sync.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/testing.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/trust/__init__.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/trust/calibration.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth/trust/graph.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth.egg-info/dependency_links.txt +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth.egg-info/entry_points.txt +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth.egg-info/requires.txt +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/src/capauth.egg-info/top_level.txt +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/stages/ak-stage-capauth/.gitignore +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/stages/ak-stage-capauth/package-lock.json +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/stages/ak-stage-capauth/package.json +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/stages/ak-stage-capauth/src/CapAuthStage.test.js +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/stages/ak-stage-capauth/src/CapAuthStage.vue +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/stages/ak-stage-capauth/src/index.js +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/stages/ak-stage-capauth/vite.config.js +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/__init__.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/conftest.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/fixtures/bunker_e2e_v1_vector.json +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/fixtures/canonical_nonce_v2_vector.json +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_agent_identity.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_agentrun_capabilities.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_audience_mint_helper.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_audience_tokens.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_authz.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_authz_legacy_grace.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_authz_signature_gate.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_backup_restore.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_bunker.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_bunker_e2e.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_canonicalize.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_capauth_service_deploy.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_change_capabilities.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_claims_mapper.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_control_plane.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_control_plane_capabilities.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_crypto.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_custody.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_delegated_capabilities.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_device_key_proof.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_did_capability_relationships.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_enrollment_proof_helper.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_estate.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_fingerprint_v6.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_forgejo_preflight.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_home_resolution.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_identity.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_identity_class.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_identity_ttl.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_integration.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_key_revocation_expiry.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_key_transparency.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_login.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_manifest.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_mesh.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_models_pqc_q0.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_nextcloud_app.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_no_subapp_dependency.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_nonce_store.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_oidc_idp.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_oidc_idp_e2e.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_operator_session.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_pairing.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_pairing_base_dir_env.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_passkey.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_pma.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_pqc_confidentiality.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_pqc_identity.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_pqc_root_integration.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_pqc_t3_gate.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_pqc_tokens.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_profile.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_provisioning.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_push.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_py313_compat.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_qr_login.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_real_pgp_e2e.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_register_cli.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_registry.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_seal.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_sequoia_backend.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_service.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_service_authz_decide.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_service_v6_fingerprint.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_skpgp_backend.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_subject.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_sync.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_syncthing_discovery.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_testing_helper.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_token_gc.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_token_store_flag.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_tokens.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_trust_calibration.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_trust_graph.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_trust_graph_coord_audit.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_verifier.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_version_honesty.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tests/test_zk_profile.py +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/tools/build-sq.sh +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/urllib.error +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/web/README.md +0 -0
- {capauth-0.3.7 → capauth-0.3.8}/web/stages/capauth/CapAuthStage.ts +0 -0
|
@@ -6,6 +6,18 @@ All notable changes to `capauth` are documented here. The format is based on
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
- Added an opaque two-phase control-plane currentness verifier for protected
|
|
10
|
+
downstream owner reads. It binds two ordered request-local checks to the exact
|
|
11
|
+
authorizer-issued context, preserves deny versus unavailable, detects current
|
|
12
|
+
issuer, principal, ancestor and leaf revocation, and expiry changes, and
|
|
13
|
+
releases all credential-bearing state on completion or failure (card
|
|
14
|
+
`c7981a0a`).
|
|
15
|
+
|
|
16
|
+
- Added a signed, non-persistent audience-token lifetime in seconds, strictly
|
|
17
|
+
bounded to 1 through 300 seconds. The new path rejects persistence, unsigned
|
|
18
|
+
issuance, and conflicting hour-based lifetimes while preserving the existing
|
|
19
|
+
one-hour default and explicit hour-based API.
|
|
20
|
+
|
|
9
21
|
- Scoped pull-request and push secret scans to protected `main` plus the exact
|
|
10
22
|
event commit range so unrelated abandoned refs cannot poison clean changes.
|
|
11
23
|
Scheduled and manual runs retain an explicit all-ref scan, and a runnable
|
|
@@ -103,6 +103,18 @@ domain-separated authenticated identity reference. It omits the raw owner
|
|
|
103
103
|
decision and its provider-controlled reason text, as well as the bearer, raw
|
|
104
104
|
token, signature, and capability chain.
|
|
105
105
|
|
|
106
|
+
Callers that must perform a protected owner read after authorization use
|
|
107
|
+
`authorize_with_currentness`. It returns the same sanitized result plus an
|
|
108
|
+
opaque request-local `ControlPlaneCurrentnessVerifier`. The caller must pass the
|
|
109
|
+
exact context object to `check_before_owner_read`, keep the owner result private,
|
|
110
|
+
then pass the same object to `check_after_owner_read` before releasing output.
|
|
111
|
+
The locked verifier enforces that order, consumes two distinct authorizer-bound
|
|
112
|
+
receipts, and returns `allow`, `deny`, or `unavailable` without policy detail.
|
|
113
|
+
Any wrong order, copied context, current-state failure, or close invalidates its
|
|
114
|
+
unused receipt and releases the privately held capability chain. Middleware must
|
|
115
|
+
call `close` in `finally`. This brackets but does not replace the owner system's
|
|
116
|
+
own atomic policy-revision read boundary.
|
|
117
|
+
|
|
106
118
|
Every non-allow result contains only `allow`, broad truth state, broad decision
|
|
107
119
|
code, and a null context. Owner reason text, resource identity, principal,
|
|
108
120
|
policy revision, and credential details are not returned on denial, Unknown, or
|
|
@@ -32,6 +32,7 @@ from .control_plane import (
|
|
|
32
32
|
from .control_plane_authorizer import (
|
|
33
33
|
MAX_CONTROL_PLANE_BEARER_BYTES,
|
|
34
34
|
ControlPlaneAuthorizationResultV1,
|
|
35
|
+
ControlPlaneCurrentnessVerifier,
|
|
35
36
|
ControlPlaneDecisionAuthorizer,
|
|
36
37
|
ControlPlaneInvocationV1,
|
|
37
38
|
OwnerPolicyProvider,
|
|
@@ -188,6 +189,7 @@ __all__ = [
|
|
|
188
189
|
"MAX_CONTROL_PLANE_BEARER_BYTES",
|
|
189
190
|
"ClientKind",
|
|
190
191
|
"ControlPlaneAuthorizationResultV1",
|
|
192
|
+
"ControlPlaneCurrentnessVerifier",
|
|
191
193
|
"ControlPlaneBinding",
|
|
192
194
|
"ControlPlaneDecision",
|
|
193
195
|
"ControlPlaneDecisionAuthorizer",
|
|
@@ -1696,10 +1696,16 @@ def token() -> None:
|
|
|
1696
1696
|
)
|
|
1697
1697
|
@click.option(
|
|
1698
1698
|
"--ttl-hours",
|
|
1699
|
-
default=
|
|
1699
|
+
default=None,
|
|
1700
1700
|
type=int,
|
|
1701
1701
|
help="Token lifetime in hours (default: 1).",
|
|
1702
1702
|
)
|
|
1703
|
+
@click.option(
|
|
1704
|
+
"--ttl-seconds",
|
|
1705
|
+
default=None,
|
|
1706
|
+
type=click.IntRange(1, 300),
|
|
1707
|
+
help="Non-persistent signed token lifetime in seconds (1-300).",
|
|
1708
|
+
)
|
|
1703
1709
|
@click.option(
|
|
1704
1710
|
"--no-sign",
|
|
1705
1711
|
is_flag=True,
|
|
@@ -1719,7 +1725,8 @@ def token_mint_audience(
|
|
|
1719
1725
|
agent: Optional[str],
|
|
1720
1726
|
audience: str,
|
|
1721
1727
|
scopes: tuple[str, ...],
|
|
1722
|
-
ttl_hours: int,
|
|
1728
|
+
ttl_hours: Optional[int],
|
|
1729
|
+
ttl_seconds: Optional[int],
|
|
1723
1730
|
no_sign: bool,
|
|
1724
1731
|
export: bool,
|
|
1725
1732
|
) -> None:
|
|
@@ -1736,14 +1743,21 @@ def token_mint_audience(
|
|
|
1736
1743
|
|
|
1737
1744
|
scope_list = list(scopes) if scopes else None
|
|
1738
1745
|
|
|
1746
|
+
if ttl_seconds is not None and ttl_hours is not None:
|
|
1747
|
+
raise click.UsageError("--ttl-seconds and --ttl-hours are mutually exclusive")
|
|
1748
|
+
if ttl_seconds is not None and no_sign:
|
|
1749
|
+
raise click.UsageError("--ttl-seconds requires signing")
|
|
1750
|
+
|
|
1739
1751
|
try:
|
|
1740
1752
|
tok = mint_agent_audience_token(
|
|
1741
1753
|
agent=agent,
|
|
1742
1754
|
audience=audience,
|
|
1743
1755
|
scopes=scope_list,
|
|
1744
1756
|
ttl_hours=ttl_hours,
|
|
1757
|
+
ttl_seconds=ttl_seconds,
|
|
1745
1758
|
home=ctx.obj.get("home"),
|
|
1746
1759
|
sign=not no_sign,
|
|
1760
|
+
store=ttl_seconds is None,
|
|
1747
1761
|
)
|
|
1748
1762
|
except (CapAuthError, ValueError) as exc:
|
|
1749
1763
|
console.print(f"[bold red]Error:[/] {exc}")
|
|
@@ -4,9 +4,12 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import base64
|
|
6
6
|
import hashlib
|
|
7
|
+
import hmac
|
|
7
8
|
import json
|
|
9
|
+
import secrets
|
|
8
10
|
from collections.abc import Callable
|
|
9
11
|
from datetime import datetime, timedelta, timezone
|
|
12
|
+
from threading import Lock
|
|
10
13
|
from typing import Literal, Protocol
|
|
11
14
|
|
|
12
15
|
from pydantic import Field, field_serializer, model_validator
|
|
@@ -25,6 +28,7 @@ from .control_plane import (
|
|
|
25
28
|
validate_request_boundary,
|
|
26
29
|
)
|
|
27
30
|
from .delegated import (
|
|
31
|
+
AuthorizationCurrentnessReceipt,
|
|
28
32
|
AuthorizationDecision,
|
|
29
33
|
AuthorizationDeniedError,
|
|
30
34
|
AuthorizationReceiptError,
|
|
@@ -42,6 +46,7 @@ from .delegated import (
|
|
|
42
46
|
UTC = timezone.utc
|
|
43
47
|
MAX_CONTROL_PLANE_BEARER_BYTES = 64 * 1024
|
|
44
48
|
MAX_CONTROL_PLANE_TTL = timedelta(minutes=5)
|
|
49
|
+
MAX_CONTROL_PLANE_CURRENTNESS_VERIFIERS = 1024
|
|
45
50
|
|
|
46
51
|
|
|
47
52
|
class ControlPlaneInvocationV1(StrictValue):
|
|
@@ -176,6 +181,190 @@ class ControlPlaneAuthorizationResultV1(StrictValue):
|
|
|
176
181
|
return self
|
|
177
182
|
|
|
178
183
|
|
|
184
|
+
_CURRENTNESS_FACTORY = object()
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class ControlPlaneCurrentnessVerifier:
|
|
188
|
+
"""Opaque request-local verifier for two downstream currentness checks."""
|
|
189
|
+
|
|
190
|
+
__slots__ = (
|
|
191
|
+
"_authorizer",
|
|
192
|
+
"_context",
|
|
193
|
+
"_context_fingerprint",
|
|
194
|
+
"_issuer",
|
|
195
|
+
"_issue_mac",
|
|
196
|
+
"_issue_nonce",
|
|
197
|
+
"_lock",
|
|
198
|
+
"_owns_receipts",
|
|
199
|
+
"_phase",
|
|
200
|
+
"_presented",
|
|
201
|
+
"_prior",
|
|
202
|
+
"_receipts",
|
|
203
|
+
"_request",
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
def __init__(
|
|
207
|
+
self,
|
|
208
|
+
factory_token: object,
|
|
209
|
+
*,
|
|
210
|
+
issuer: "ControlPlaneDecisionAuthorizer",
|
|
211
|
+
issue_nonce: bytes,
|
|
212
|
+
issue_mac: bytes,
|
|
213
|
+
authorizer: CapabilityAuthorizer,
|
|
214
|
+
presented: PresentedCapability,
|
|
215
|
+
request: AuthorizationRequest,
|
|
216
|
+
prior: AuthorizationDecision,
|
|
217
|
+
context: SanitizedControlPlaneDecisionV1,
|
|
218
|
+
receipts: tuple[AuthorizationCurrentnessReceipt, ...],
|
|
219
|
+
) -> None:
|
|
220
|
+
if (
|
|
221
|
+
factory_token is not _CURRENTNESS_FACTORY
|
|
222
|
+
or type(issuer) is not ControlPlaneDecisionAuthorizer
|
|
223
|
+
or len(issue_nonce) != 32
|
|
224
|
+
or len(issue_mac) != 32
|
|
225
|
+
or type(authorizer) is not CapabilityAuthorizer
|
|
226
|
+
or not isinstance(presented, PresentedCapability)
|
|
227
|
+
or not isinstance(request, AuthorizationRequest)
|
|
228
|
+
or not isinstance(prior, AuthorizationDecision)
|
|
229
|
+
or not isinstance(context, SanitizedControlPlaneDecisionV1)
|
|
230
|
+
or len(receipts) != 2
|
|
231
|
+
or any(not isinstance(value, AuthorizationCurrentnessReceipt) for value in receipts)
|
|
232
|
+
):
|
|
233
|
+
raise TypeError("control-plane currentness verifiers are authorizer-issued")
|
|
234
|
+
object.__setattr__(self, "_authorizer", authorizer)
|
|
235
|
+
object.__setattr__(self, "_issuer", issuer)
|
|
236
|
+
object.__setattr__(self, "_issue_nonce", bytes(issue_nonce))
|
|
237
|
+
object.__setattr__(self, "_issue_mac", bytes(issue_mac))
|
|
238
|
+
object.__setattr__(self, "_presented", presented)
|
|
239
|
+
object.__setattr__(self, "_request", request)
|
|
240
|
+
object.__setattr__(self, "_prior", prior)
|
|
241
|
+
object.__setattr__(self, "_context", context)
|
|
242
|
+
object.__setattr__(self, "_context_fingerprint", _context_fingerprint(context))
|
|
243
|
+
object.__setattr__(self, "_receipts", tuple(receipts))
|
|
244
|
+
object.__setattr__(self, "_lock", Lock())
|
|
245
|
+
object.__setattr__(self, "_owns_receipts", False)
|
|
246
|
+
object.__setattr__(self, "_phase", 0)
|
|
247
|
+
|
|
248
|
+
def __setattr__(self, name, value) -> None:
|
|
249
|
+
del name, value
|
|
250
|
+
raise AttributeError("control-plane currentness verifier is immutable")
|
|
251
|
+
|
|
252
|
+
def check_before_owner_read(self, context: SanitizedControlPlaneDecisionV1) -> DecisionState:
|
|
253
|
+
"""Consume the pre-read proof for the exact issued context."""
|
|
254
|
+
|
|
255
|
+
return self._check(context, expected_phase=0)
|
|
256
|
+
|
|
257
|
+
def check_after_owner_read(self, context: SanitizedControlPlaneDecisionV1) -> DecisionState:
|
|
258
|
+
"""Consume the post-read proof before protected output is released."""
|
|
259
|
+
|
|
260
|
+
return self._check(context, expected_phase=1)
|
|
261
|
+
|
|
262
|
+
def _check(
|
|
263
|
+
self,
|
|
264
|
+
context: SanitizedControlPlaneDecisionV1,
|
|
265
|
+
*,
|
|
266
|
+
expected_phase: int,
|
|
267
|
+
) -> DecisionState:
|
|
268
|
+
with self._lock:
|
|
269
|
+
if (
|
|
270
|
+
self._phase != expected_phase
|
|
271
|
+
or context is not self._context
|
|
272
|
+
or len(self._receipts) != 2
|
|
273
|
+
):
|
|
274
|
+
self._close_locked()
|
|
275
|
+
return DecisionState.DENY
|
|
276
|
+
try:
|
|
277
|
+
fingerprint = _context_fingerprint(context)
|
|
278
|
+
except Exception:
|
|
279
|
+
self._close_locked()
|
|
280
|
+
return DecisionState.DENY
|
|
281
|
+
if fingerprint != self._context_fingerprint:
|
|
282
|
+
self._close_locked()
|
|
283
|
+
return DecisionState.DENY
|
|
284
|
+
try:
|
|
285
|
+
issued = self._issuer._claim_currentness_verifier(
|
|
286
|
+
self._issue_nonce,
|
|
287
|
+
self._issue_mac,
|
|
288
|
+
context,
|
|
289
|
+
verifier=self,
|
|
290
|
+
expected_phase=expected_phase,
|
|
291
|
+
)
|
|
292
|
+
except Exception:
|
|
293
|
+
self._close_locked()
|
|
294
|
+
return DecisionState.UNAVAILABLE
|
|
295
|
+
if not issued:
|
|
296
|
+
self._close_locked()
|
|
297
|
+
return DecisionState.DENY
|
|
298
|
+
object.__setattr__(self, "_owns_receipts", True)
|
|
299
|
+
receipt = self._receipts[expected_phase]
|
|
300
|
+
try:
|
|
301
|
+
current = self._authorizer.revalidate_current(
|
|
302
|
+
self._presented,
|
|
303
|
+
self._request,
|
|
304
|
+
self._prior,
|
|
305
|
+
receipt,
|
|
306
|
+
)
|
|
307
|
+
state = DecisionState.ALLOW if current is self._prior else DecisionState.DENY
|
|
308
|
+
except AuthorizationDeniedError as exc:
|
|
309
|
+
self._authorizer.discard_currentness_receipts((receipt,))
|
|
310
|
+
state = _capauth_denial(exc.decision.reason).state
|
|
311
|
+
except AuthorizationReceiptError:
|
|
312
|
+
self._authorizer.discard_currentness_receipts((receipt,))
|
|
313
|
+
state = DecisionState.DENY
|
|
314
|
+
except AuthorizationReceiptUnavailableError:
|
|
315
|
+
self._authorizer.discard_currentness_receipts((receipt,))
|
|
316
|
+
state = DecisionState.UNAVAILABLE
|
|
317
|
+
except Exception:
|
|
318
|
+
self._authorizer.discard_currentness_receipts((receipt,))
|
|
319
|
+
state = DecisionState.UNAVAILABLE
|
|
320
|
+
if state is not DecisionState.ALLOW or expected_phase == 1:
|
|
321
|
+
self._close_locked()
|
|
322
|
+
else:
|
|
323
|
+
object.__setattr__(self, "_phase", 1)
|
|
324
|
+
return state
|
|
325
|
+
|
|
326
|
+
def close(self) -> None:
|
|
327
|
+
"""Invalidate unused proofs and release request credential material."""
|
|
328
|
+
|
|
329
|
+
with self._lock:
|
|
330
|
+
self._close_locked()
|
|
331
|
+
|
|
332
|
+
def _close_locked(self) -> None:
|
|
333
|
+
if self._phase == 2:
|
|
334
|
+
return
|
|
335
|
+
registered = self._issuer._discard_currentness_verifier(
|
|
336
|
+
self._issue_nonce,
|
|
337
|
+
verifier=self,
|
|
338
|
+
)
|
|
339
|
+
if self._owns_receipts or registered:
|
|
340
|
+
self._authorizer.discard_currentness_receipts(tuple(self._receipts))
|
|
341
|
+
object.__setattr__(self, "_receipts", ())
|
|
342
|
+
object.__setattr__(self, "_presented", None)
|
|
343
|
+
object.__setattr__(self, "_request", None)
|
|
344
|
+
object.__setattr__(self, "_prior", None)
|
|
345
|
+
object.__setattr__(self, "_context", None)
|
|
346
|
+
object.__setattr__(self, "_context_fingerprint", b"")
|
|
347
|
+
object.__setattr__(self, "_issue_nonce", b"")
|
|
348
|
+
object.__setattr__(self, "_issue_mac", b"")
|
|
349
|
+
object.__setattr__(self, "_owns_receipts", False)
|
|
350
|
+
object.__setattr__(self, "_phase", 2)
|
|
351
|
+
|
|
352
|
+
def __repr__(self) -> str:
|
|
353
|
+
return "<opaque control-plane currentness verifier>"
|
|
354
|
+
|
|
355
|
+
__str__ = __repr__
|
|
356
|
+
|
|
357
|
+
def __copy__(self):
|
|
358
|
+
raise TypeError("control-plane currentness verifier cannot be copied")
|
|
359
|
+
|
|
360
|
+
def __deepcopy__(self, memo):
|
|
361
|
+
del memo
|
|
362
|
+
raise TypeError("control-plane currentness verifier cannot be copied")
|
|
363
|
+
|
|
364
|
+
def __reduce__(self):
|
|
365
|
+
raise TypeError("control-plane currentness verifier cannot be serialized")
|
|
366
|
+
|
|
367
|
+
|
|
179
368
|
def export_control_plane_bearer(presented: PresentedCapability) -> str:
|
|
180
369
|
"""Encode delegated transport in the canonical padded browser handoff form."""
|
|
181
370
|
|
|
@@ -207,7 +396,15 @@ def parse_control_plane_bearer(bearer: str) -> PresentedCapability:
|
|
|
207
396
|
class ControlPlaneDecisionAuthorizer:
|
|
208
397
|
"""Compose delegated CapAuth and resource-owner policy without storing requests."""
|
|
209
398
|
|
|
210
|
-
__slots__ = (
|
|
399
|
+
__slots__ = (
|
|
400
|
+
"_allowed_origins",
|
|
401
|
+
"_authorizer",
|
|
402
|
+
"_clock",
|
|
403
|
+
"_owner_policy",
|
|
404
|
+
"_verifier_key",
|
|
405
|
+
"_verifier_lock",
|
|
406
|
+
"_verifier_states",
|
|
407
|
+
)
|
|
211
408
|
|
|
212
409
|
def __init__(
|
|
213
410
|
self,
|
|
@@ -225,6 +422,118 @@ class ControlPlaneDecisionAuthorizer:
|
|
|
225
422
|
self._owner_policy = owner_policy
|
|
226
423
|
self._allowed_origins = frozenset(allowed_origins)
|
|
227
424
|
self._clock = clock or (lambda: datetime.now(UTC))
|
|
425
|
+
self._verifier_key = secrets.token_bytes(32)
|
|
426
|
+
self._verifier_lock = Lock()
|
|
427
|
+
self._verifier_states = {}
|
|
428
|
+
|
|
429
|
+
def _register_currentness_verifier(
|
|
430
|
+
self,
|
|
431
|
+
context: SanitizedControlPlaneDecisionV1,
|
|
432
|
+
) -> tuple[bytes, bytes]:
|
|
433
|
+
now = _utc(self._clock())
|
|
434
|
+
fingerprint = _context_fingerprint(context)
|
|
435
|
+
with self._verifier_lock:
|
|
436
|
+
self._verifier_states = {
|
|
437
|
+
nonce: state for nonce, state in self._verifier_states.items() if state[0] > now
|
|
438
|
+
}
|
|
439
|
+
if len(self._verifier_states) >= MAX_CONTROL_PLANE_CURRENTNESS_VERIFIERS:
|
|
440
|
+
raise RuntimeError("control-plane currentness verifier capacity exhausted")
|
|
441
|
+
for _attempt in range(16):
|
|
442
|
+
nonce = secrets.token_bytes(32)
|
|
443
|
+
if nonce in self._verifier_states:
|
|
444
|
+
continue
|
|
445
|
+
mac = self._currentness_verifier_mac(nonce, fingerprint)
|
|
446
|
+
self._verifier_states[nonce] = (
|
|
447
|
+
context.expires_at,
|
|
448
|
+
0,
|
|
449
|
+
id(context),
|
|
450
|
+
fingerprint,
|
|
451
|
+
mac,
|
|
452
|
+
None,
|
|
453
|
+
)
|
|
454
|
+
return nonce, mac
|
|
455
|
+
raise RuntimeError("control-plane currentness verifier nonce unavailable")
|
|
456
|
+
|
|
457
|
+
def _bind_currentness_verifier(
|
|
458
|
+
self,
|
|
459
|
+
nonce: bytes,
|
|
460
|
+
mac: bytes,
|
|
461
|
+
context: SanitizedControlPlaneDecisionV1,
|
|
462
|
+
verifier: ControlPlaneCurrentnessVerifier,
|
|
463
|
+
) -> bool:
|
|
464
|
+
fingerprint = _context_fingerprint(context)
|
|
465
|
+
with self._verifier_lock:
|
|
466
|
+
state = self._verifier_states.get(nonce)
|
|
467
|
+
valid = (
|
|
468
|
+
state is not None
|
|
469
|
+
and state[1] == 0
|
|
470
|
+
and state[2] == id(context)
|
|
471
|
+
and state[3] == fingerprint
|
|
472
|
+
and hmac.compare_digest(state[4], mac)
|
|
473
|
+
and state[5] is None
|
|
474
|
+
)
|
|
475
|
+
if not valid:
|
|
476
|
+
self._verifier_states.pop(nonce, None)
|
|
477
|
+
return False
|
|
478
|
+
self._verifier_states[nonce] = (*state[:5], id(verifier))
|
|
479
|
+
return True
|
|
480
|
+
|
|
481
|
+
def _claim_currentness_verifier(
|
|
482
|
+
self,
|
|
483
|
+
nonce: bytes,
|
|
484
|
+
mac: bytes,
|
|
485
|
+
context: SanitizedControlPlaneDecisionV1,
|
|
486
|
+
*,
|
|
487
|
+
verifier: ControlPlaneCurrentnessVerifier,
|
|
488
|
+
expected_phase: int,
|
|
489
|
+
) -> bool:
|
|
490
|
+
now = _utc(self._clock())
|
|
491
|
+
fingerprint = _context_fingerprint(context)
|
|
492
|
+
with self._verifier_lock:
|
|
493
|
+
state = self._verifier_states.get(nonce)
|
|
494
|
+
self._verifier_states = {
|
|
495
|
+
value: item for value, item in self._verifier_states.items() if item[0] > now
|
|
496
|
+
}
|
|
497
|
+
if state is None or state[0] <= now:
|
|
498
|
+
return False
|
|
499
|
+
valid_mac = hmac.compare_digest(
|
|
500
|
+
mac,
|
|
501
|
+
self._currentness_verifier_mac(nonce, fingerprint),
|
|
502
|
+
)
|
|
503
|
+
valid = (
|
|
504
|
+
valid_mac
|
|
505
|
+
and state[1] == expected_phase
|
|
506
|
+
and state[2] == id(context)
|
|
507
|
+
and state[3] == fingerprint
|
|
508
|
+
and hmac.compare_digest(state[4], mac)
|
|
509
|
+
and state[5] == id(verifier)
|
|
510
|
+
)
|
|
511
|
+
if not valid:
|
|
512
|
+
return False
|
|
513
|
+
if expected_phase == 0:
|
|
514
|
+
self._verifier_states[nonce] = (state[0], 1, *state[2:])
|
|
515
|
+
else:
|
|
516
|
+
self._verifier_states.pop(nonce, None)
|
|
517
|
+
return True
|
|
518
|
+
|
|
519
|
+
def _discard_currentness_verifier(
|
|
520
|
+
self,
|
|
521
|
+
nonce: bytes,
|
|
522
|
+
*,
|
|
523
|
+
verifier: ControlPlaneCurrentnessVerifier | None = None,
|
|
524
|
+
) -> bool:
|
|
525
|
+
with self._verifier_lock:
|
|
526
|
+
state = self._verifier_states.get(nonce)
|
|
527
|
+
if verifier is not None and state is not None and state[5] != id(verifier):
|
|
528
|
+
return False
|
|
529
|
+
return self._verifier_states.pop(nonce, None) is not None
|
|
530
|
+
|
|
531
|
+
def _currentness_verifier_mac(self, nonce: bytes, fingerprint: bytes) -> bytes:
|
|
532
|
+
return hmac.new(
|
|
533
|
+
self._verifier_key,
|
|
534
|
+
b"capauth-control-plane-currentness-verifier/v1\0" + nonce + fingerprint,
|
|
535
|
+
hashlib.sha256,
|
|
536
|
+
).digest()
|
|
228
537
|
|
|
229
538
|
def authorize(
|
|
230
539
|
self,
|
|
@@ -233,9 +542,41 @@ class ControlPlaneDecisionAuthorizer:
|
|
|
233
542
|
) -> ControlPlaneAuthorizationResultV1:
|
|
234
543
|
"""Return one sanitized typed result and never retain the bearer."""
|
|
235
544
|
|
|
545
|
+
result, verifier = self._authorize(bearer, invocation, issue_verifier=False)
|
|
546
|
+
if verifier is not None:
|
|
547
|
+
verifier.close()
|
|
548
|
+
return result
|
|
549
|
+
|
|
550
|
+
def authorize_with_currentness(
|
|
551
|
+
self,
|
|
552
|
+
bearer: str,
|
|
553
|
+
invocation: ControlPlaneInvocationV1,
|
|
554
|
+
) -> tuple[ControlPlaneAuthorizationResultV1, ControlPlaneCurrentnessVerifier | None]:
|
|
555
|
+
"""Return a sanitized result and two-use opaque downstream verifier."""
|
|
556
|
+
|
|
557
|
+
return self._authorize(bearer, invocation, issue_verifier=True)
|
|
558
|
+
|
|
559
|
+
def _authorize(
|
|
560
|
+
self,
|
|
561
|
+
bearer: str,
|
|
562
|
+
invocation: ControlPlaneInvocationV1,
|
|
563
|
+
*,
|
|
564
|
+
issue_verifier: bool,
|
|
565
|
+
) -> tuple[ControlPlaneAuthorizationResultV1, ControlPlaneCurrentnessVerifier | None]:
|
|
566
|
+
active_receipts = ()
|
|
567
|
+
downstream_receipts = ()
|
|
568
|
+
registered_nonce = b""
|
|
569
|
+
|
|
570
|
+
def closed(
|
|
571
|
+
result: ControlPlaneAuthorizationResultV1,
|
|
572
|
+
) -> tuple[ControlPlaneAuthorizationResultV1, None]:
|
|
573
|
+
self._authorizer.discard_currentness_receipts(active_receipts + downstream_receipts)
|
|
574
|
+
self._discard_currentness_verifier(registered_nonce)
|
|
575
|
+
return result, None
|
|
576
|
+
|
|
236
577
|
denied = _result(DecisionState.DENY, DecisionCode.CAPAUTH_DENIED)
|
|
237
578
|
if not isinstance(invocation, ControlPlaneInvocationV1):
|
|
238
|
-
return denied
|
|
579
|
+
return closed(denied)
|
|
239
580
|
try:
|
|
240
581
|
presented = parse_control_plane_bearer(bearer)
|
|
241
582
|
leaf_raw = presented.credentials_for_verification()[-1]
|
|
@@ -245,7 +586,7 @@ class ControlPlaneDecisionAuthorizer:
|
|
|
245
586
|
expires_at = _utc(leaf.token.payload.expires_at)
|
|
246
587
|
now = _utc(self._clock())
|
|
247
588
|
except Exception:
|
|
248
|
-
return denied
|
|
589
|
+
return closed(denied)
|
|
249
590
|
|
|
250
591
|
boundary_code = validate_request_boundary(
|
|
251
592
|
binding,
|
|
@@ -254,9 +595,9 @@ class ControlPlaneDecisionAuthorizer:
|
|
|
254
595
|
as_of=now,
|
|
255
596
|
)
|
|
256
597
|
if boundary_code is not None:
|
|
257
|
-
return _result(DecisionState.DENY, boundary_code)
|
|
598
|
+
return closed(_result(DecisionState.DENY, boundary_code))
|
|
258
599
|
if not issued_at <= now < expires_at:
|
|
259
|
-
return _result(DecisionState.DENY, DecisionCode.EXPIRED)
|
|
600
|
+
return closed(_result(DecisionState.DENY, DecisionCode.EXPIRED))
|
|
260
601
|
|
|
261
602
|
request = AuthorizationRequest(
|
|
262
603
|
principal=binding.principal,
|
|
@@ -265,49 +606,64 @@ class ControlPlaneDecisionAuthorizer:
|
|
|
265
606
|
)
|
|
266
607
|
try:
|
|
267
608
|
capauth, receipt = self._authorizer.authorize_with_receipt(presented, request)
|
|
609
|
+
active_receipts = (receipt,)
|
|
268
610
|
except AuthorizationDeniedError as exc:
|
|
269
|
-
return _capauth_denial(exc.decision.reason)
|
|
611
|
+
return closed(_capauth_denial(exc.decision.reason))
|
|
270
612
|
except AuthorizationReceiptUnavailableError:
|
|
271
|
-
return _result(DecisionState.UNAVAILABLE, DecisionCode.CAPAUTH_UNAVAILABLE)
|
|
613
|
+
return closed(_result(DecisionState.UNAVAILABLE, DecisionCode.CAPAUTH_UNAVAILABLE))
|
|
272
614
|
except Exception:
|
|
273
|
-
return _result(DecisionState.UNAVAILABLE, DecisionCode.CAPAUTH_UNAVAILABLE)
|
|
615
|
+
return closed(_result(DecisionState.UNAVAILABLE, DecisionCode.CAPAUTH_UNAVAILABLE))
|
|
274
616
|
|
|
275
617
|
try:
|
|
276
618
|
now = _utc(self._clock())
|
|
277
619
|
except Exception:
|
|
278
|
-
return _result(DecisionState.UNAVAILABLE, DecisionCode.CAPAUTH_UNAVAILABLE)
|
|
620
|
+
return closed(_result(DecisionState.UNAVAILABLE, DecisionCode.CAPAUTH_UNAVAILABLE))
|
|
279
621
|
if not issued_at <= now < expires_at:
|
|
280
|
-
return _result(DecisionState.DENY, DecisionCode.EXPIRED)
|
|
622
|
+
return closed(_result(DecisionState.DENY, DecisionCode.EXPIRED))
|
|
281
623
|
|
|
282
624
|
first = self._owner_decision(binding, capauth)
|
|
283
625
|
if first is None:
|
|
284
|
-
return
|
|
626
|
+
return closed(
|
|
627
|
+
_result(DecisionState.UNAVAILABLE, DecisionCode.OWNER_POLICY_UNAVAILABLE)
|
|
628
|
+
)
|
|
285
629
|
first_joined = join_policy_decisions(binding, capauth, first)
|
|
286
630
|
if not first_joined.allow:
|
|
287
|
-
return _result(first_joined.state, first_joined.code)
|
|
631
|
+
return closed(_result(first_joined.state, first_joined.code))
|
|
288
632
|
try:
|
|
289
|
-
|
|
633
|
+
if issue_verifier:
|
|
634
|
+
capauth, downstream_receipts = self._authorizer.revalidate_current_with_receipts(
|
|
635
|
+
presented,
|
|
636
|
+
request,
|
|
637
|
+
capauth,
|
|
638
|
+
receipt,
|
|
639
|
+
count=2,
|
|
640
|
+
)
|
|
641
|
+
else:
|
|
642
|
+
capauth = self._authorizer.revalidate_current(presented, request, capauth, receipt)
|
|
643
|
+
active_receipts = ()
|
|
290
644
|
except AuthorizationReceiptError:
|
|
291
|
-
return _result(DecisionState.DENY, DecisionCode.CAPAUTH_DENIED)
|
|
645
|
+
return closed(_result(DecisionState.DENY, DecisionCode.CAPAUTH_DENIED))
|
|
292
646
|
except AuthorizationDeniedError as exc:
|
|
293
647
|
if exc.decision.reason is DecisionReason.EXPIRED:
|
|
294
|
-
return _result(DecisionState.DENY, DecisionCode.EXPIRED)
|
|
295
|
-
return _capauth_denial(exc.decision.reason)
|
|
648
|
+
return closed(_result(DecisionState.DENY, DecisionCode.EXPIRED))
|
|
649
|
+
return closed(_capauth_denial(exc.decision.reason))
|
|
296
650
|
except Exception:
|
|
297
|
-
return _result(DecisionState.UNAVAILABLE, DecisionCode.CAPAUTH_UNAVAILABLE)
|
|
651
|
+
return closed(_result(DecisionState.UNAVAILABLE, DecisionCode.CAPAUTH_UNAVAILABLE))
|
|
298
652
|
second = self._owner_decision(binding, capauth)
|
|
299
653
|
if second is None:
|
|
300
|
-
return
|
|
654
|
+
return closed(
|
|
655
|
+
_result(DecisionState.UNAVAILABLE, DecisionCode.OWNER_POLICY_UNAVAILABLE)
|
|
656
|
+
)
|
|
301
657
|
if first != second:
|
|
302
|
-
return _result(DecisionState.DENY, DecisionCode.BINDING_MISMATCH)
|
|
658
|
+
return closed(_result(DecisionState.DENY, DecisionCode.BINDING_MISMATCH))
|
|
303
659
|
joined = join_policy_decisions(binding, capauth, second)
|
|
304
660
|
if not joined.allow:
|
|
305
|
-
return _result(joined.state, joined.code)
|
|
661
|
+
return closed(_result(joined.state, joined.code))
|
|
306
662
|
|
|
307
663
|
try:
|
|
308
664
|
now = _utc(self._clock())
|
|
309
665
|
if not issued_at <= now < expires_at:
|
|
310
|
-
return _result(DecisionState.DENY, DecisionCode.EXPIRED)
|
|
666
|
+
return closed(_result(DecisionState.DENY, DecisionCode.EXPIRED))
|
|
311
667
|
context = SanitizedControlPlaneDecisionV1(
|
|
312
668
|
binding=binding,
|
|
313
669
|
boundary=invocation.boundary,
|
|
@@ -318,13 +674,40 @@ class ControlPlaneDecisionAuthorizer:
|
|
|
318
674
|
expires_at=expires_at,
|
|
319
675
|
)
|
|
320
676
|
except Exception:
|
|
321
|
-
return _result(DecisionState.DENY, DecisionCode.BINDING_MISMATCH)
|
|
322
|
-
|
|
677
|
+
return closed(_result(DecisionState.DENY, DecisionCode.BINDING_MISMATCH))
|
|
678
|
+
result = ControlPlaneAuthorizationResultV1(
|
|
323
679
|
allow=True,
|
|
324
680
|
state=DecisionState.ALLOW,
|
|
325
681
|
code=DecisionCode.ALLOW,
|
|
326
682
|
context=context,
|
|
327
683
|
)
|
|
684
|
+
if not issue_verifier:
|
|
685
|
+
return result, None
|
|
686
|
+
try:
|
|
687
|
+
registered_nonce, issue_mac = self._register_currentness_verifier(context)
|
|
688
|
+
verifier = ControlPlaneCurrentnessVerifier(
|
|
689
|
+
_CURRENTNESS_FACTORY,
|
|
690
|
+
issuer=self,
|
|
691
|
+
issue_nonce=registered_nonce,
|
|
692
|
+
issue_mac=issue_mac,
|
|
693
|
+
authorizer=self._authorizer,
|
|
694
|
+
presented=presented,
|
|
695
|
+
request=request,
|
|
696
|
+
prior=capauth,
|
|
697
|
+
context=context,
|
|
698
|
+
receipts=downstream_receipts,
|
|
699
|
+
)
|
|
700
|
+
if not self._bind_currentness_verifier(
|
|
701
|
+
registered_nonce,
|
|
702
|
+
issue_mac,
|
|
703
|
+
context,
|
|
704
|
+
verifier,
|
|
705
|
+
):
|
|
706
|
+
raise RuntimeError("control-plane currentness verifier binding failed")
|
|
707
|
+
registered_nonce = b""
|
|
708
|
+
except Exception:
|
|
709
|
+
return closed(_result(DecisionState.UNAVAILABLE, DecisionCode.CAPAUTH_UNAVAILABLE))
|
|
710
|
+
return result, verifier
|
|
328
711
|
|
|
329
712
|
def _owner_decision(
|
|
330
713
|
self,
|
|
@@ -425,6 +808,16 @@ def _identity_ref(binding: ControlPlaneBinding, decision: AuthorizationDecision)
|
|
|
425
808
|
return f"sha256:{hashlib.sha256(encoded).hexdigest()}"
|
|
426
809
|
|
|
427
810
|
|
|
811
|
+
def _context_fingerprint(context: SanitizedControlPlaneDecisionV1) -> bytes:
|
|
812
|
+
encoded = json.dumps(
|
|
813
|
+
context.model_dump(mode="json"),
|
|
814
|
+
sort_keys=True,
|
|
815
|
+
separators=(",", ":"),
|
|
816
|
+
ensure_ascii=True,
|
|
817
|
+
).encode("ascii")
|
|
818
|
+
return hashlib.sha256(encoded).digest()
|
|
819
|
+
|
|
820
|
+
|
|
428
821
|
def _utc(value: datetime | None) -> datetime:
|
|
429
822
|
if value is None or value.tzinfo is None or value.utcoffset() != timedelta(0):
|
|
430
823
|
raise ValueError("time must use UTC offset zero")
|
|
@@ -449,6 +842,7 @@ def _result(
|
|
|
449
842
|
__all__ = [
|
|
450
843
|
"MAX_CONTROL_PLANE_BEARER_BYTES",
|
|
451
844
|
"ControlPlaneAuthorizationResultV1",
|
|
845
|
+
"ControlPlaneCurrentnessVerifier",
|
|
452
846
|
"ControlPlaneDecisionAuthorizer",
|
|
453
847
|
"ControlPlaneInvocationV1",
|
|
454
848
|
"OwnerPolicyProvider",
|