diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 69d849f..4b521a4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ jobs: lint: timeout-minutes: 10 name: lint - runs-on: ${{ github.repository == 'stainless-sdks/keycard-api-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + runs-on: ${{ startsWith(github.repository, 'stainless-sdks/') && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata') steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -41,7 +41,7 @@ jobs: permissions: contents: read id-token: write - runs-on: ${{ github.repository == 'stainless-sdks/keycard-api-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + runs-on: ${{ startsWith(github.repository, 'stainless-sdks/') && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -78,7 +78,7 @@ jobs: test: timeout-minutes: 10 name: test - runs-on: ${{ github.repository == 'stainless-sdks/keycard-api-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + runs-on: ${{ startsWith(github.repository, 'stainless-sdks/') && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 diff --git a/.release-please-manifest.json b/.release-please-manifest.json index b4e9013..6db19b9 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.16.0" + ".": "0.17.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index ffd5937..ff76afe 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 106 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/keycard/keycard-api-cdf14bee924cea2eaca3c1aaed26521cb91369d9462d22c21a0306b5786f4706.yml -openapi_spec_hash: d8349acc1adec880977b6167a1e866bc -config_hash: 7adc6b24545570dcc4a1bf0f714aa3e0 +configured_endpoints: 134 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/keycard/keycard-api-236932ddd6439f9f87000693aa35b5051bcc103b03b90ef1943a6900dd351c47.yml +openapi_spec_hash: 18f11f0260e682d915be1ba3b8218f5f +config_hash: 319496280ba37f9e3b2babb83e8ca71f diff --git a/CHANGELOG.md b/CHANGELOG.md index cefd0e4..76d7417 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,53 @@ # Changelog +## 0.17.0 (2026-08-26) + +Full Changelog: [v0.16.0...v0.17.0](https://github.com/keycardai/keycard-python/compare/v0.16.0...v0.17.0) + +### ⚠ BREAKING CHANGES + +* retire POST /organizations/{id}/token + +### Features + +* **ACC-709:** application assignees + expose role assignments ([6a76dbf](https://github.com/keycardai/keycard-python/commit/6a76dbf04cbd9d16c1054a262a7a42a01668f035)) +* add external_id_claim to provider openid protocol ([9ea5e49](https://github.com/keycardai/keycard-python/commit/9ea5e498695a9f88991515aa306f3b045711d0e4)) +* **applications:** allow unified-gateway and mcp-server traits (ECO-128) ([414ac68](https://github.com/keycardai/keycard-python/commit/414ac68a77bb5c54837b7d4146c2e8b8735f6370)) +* **CX-782:** paginate + search application dependencies ([02e6bde](https://github.com/keycardai/keycard-python/commit/02e6bdef7ec3aa0c3313b12f1dc814510c83c773)) +* **CX-860:** paginate application provided-resources endpoint ([aac9e86](https://github.com/keycardai/keycard-python/commit/aac9e86b2a4801ae35fc773b4cc1fd0748becf06)) +* **CX-862:** remove legacy traits[]/traits[all] aliases ([23984b7](https://github.com/keycardai/keycard-python/commit/23984b713fb45653d6a09e8eb2b88a679ef8a7ae)) +* filter users by identifier in management list ([147af02](https://github.com/keycardai/keycard-python/commit/147af020e8e8c31678928137fb85386b54dd4ec6)) +* **ID-365:** support additional SSO provider configuration options ([526b229](https://github.com/keycardai/keycard-python/commit/526b22958acc91a11569ba3f8453a004eb03f6f5)) +* **ID-456:** groups API create + get ([976fbbd](https://github.com/keycardai/keycard-python/commit/976fbbd318ab99629796bc28ddf2dc0dcfad562e)) +* **ID-478:** zones.external_sync_enabled column and update API ([fd4134c](https://github.com/keycardai/keycard-python/commit/fd4134c71676e377b1acc47dbb197a6b437a9697)) +* **ID-480:** external_sync_tokens table and management APIs ([0e04c06](https://github.com/keycardai/keycard-python/commit/0e04c066c57f96bef597416d4a4a1f77ec6c43fc)) +* **providers:** add filter[id] to list endpoint (ID-547) ([20da67a](https://github.com/keycardai/keycard-python/commit/20da67a5559d2188a4b127f6d30ce3ae43e20751)) +* **providers:** store brand icon_url in provider metadata ([f298c38](https://github.com/keycardai/keycard-python/commit/f298c382bdbe7e68b8a1556f36fe466b6155a5a8)) +* **resources:** add filter[owner_type] to the resource list (ECO-225) ([ede883a](https://github.com/keycardai/keycard-python/commit/ede883a754b07dfd8beff31afd57860d8d65814b)) +* **resources:** add search, sort, and filter[id] to the paginated resource list ([ca1681d](https://github.com/keycardai/keycard-python/commit/ca1681d46b70bd5e2913a4a8906793bc55c34ca7)) +* **resources:** filter the resource list by trait ([6cab4f0](https://github.com/keycardai/keycard-python/commit/6cab4f0985ac778837b272353cf1f729b88bd9d9)) +* retire POST /organizations/{id}/token ([8a65582](https://github.com/keycardai/keycard-python/commit/8a65582d81e48738200067e7e8ca7ec02590563c)) +* **sdk:** expose zone roles, groups, and role assignments in Stainless config ([45970b1](https://github.com/keycardai/keycard-python/commit/45970b10df4451e04810706fba32366153487896)) +* **sdk:** generate a client for the existing /policy/bundle endpoint ([518c473](https://github.com/keycardai/keycard-python/commit/518c473dcb23f0d68e849838a070187addf422a9)) +* **stlc:** configurable CI runner and private-production-repo support in workflow templates ([1c22f7e](https://github.com/keycardai/keycard-python/commit/1c22f7ea4ae904c4864a794dc4aaa46afa7d7e0e)) + + +### Bug Fixes + +* **ACC-613:** preserve source order of policies in draft/convert cedar_json ([6d27d21](https://github.com/keycardai/keycard-python/commit/6d27d213ac66fb9f1890409a380924c3edb44b99)) +* **ci:** resolve Stainless error diagnostics and enforce fail_on: error ([8a01934](https://github.com/keycardai/keycard-python/commit/8a01934b6ceb8636db998f0498d86ccb810cf639)) +* **deps:** close critical Dependabot alerts ([fe31508](https://github.com/keycardai/keycard-python/commit/fe315084c22655d49d199b33a0cb16c17e4bbbb1)) +* exact-match identifier filter on resources management list ([846f24e](https://github.com/keycardai/keycard-python/commit/846f24efdb2492e4ea0741167ebc7153af5d0d1e)) +* **ID-448:** keyset cursor drops same-millisecond rows (precision mismatch) ([dad9ddb](https://github.com/keycardai/keycard-python/commit/dad9ddb0165f422ef1d05d851d4fdf912ee1d43b)) +* **internal:** resolve build failures ([15cb317](https://github.com/keycardai/keycard-python/commit/15cb317c1d1ea8462b30bff1411e52a554df0fb0)) +* **sdk:** generate /policy/bundle body as raw binary, not multipart ([d516a7f](https://github.com/keycardai/keycard-python/commit/d516a7f7b207aa4eed33d1a401721b9aa1ecd5d9)) + + +### Chores + +* de-dup and align types across API specs ([bf16103](https://github.com/keycardai/keycard-python/commit/bf161039858f079eabd6abf9102fa439633b9bc9)) +* Fixes found during Terraform work ([8389f30](https://github.com/keycardai/keycard-python/commit/8389f304ffae7c4bbe512de541a6896469be273f)) + ## 0.16.0 (2026-06-30) Full Changelog: [v0.15.0...v0.16.0](https://github.com/keycardai/keycard-python/compare/v0.15.0...v0.16.0) diff --git a/README.md b/README.md index cf222f1..e364722 100644 --- a/README.md +++ b/README.md @@ -25,23 +25,34 @@ pip install keycardai_api The full API of this library can be found in [api.md](api.md). ```python +import os from keycardai_api import KeycardAPI -client = KeycardAPI() +client = KeycardAPI( + api_key=os.environ.get("KEYCARD_API_API_KEY"), # This is the default and can be omitted +) zones = client.zones.list() print(zones.items) ``` +While you can provide an `api_key` keyword argument, +we recommend using [python-dotenv](https://pypi.org/project/python-dotenv/) +to add `KEYCARD_API_API_KEY="My API Key"` to your `.env` file +so that your API Key is not stored in source control. + ## Async usage Simply import `AsyncKeycardAPI` instead of `KeycardAPI` and use `await` with each API call: ```python +import os import asyncio from keycardai_api import AsyncKeycardAPI -client = AsyncKeycardAPI() +client = AsyncKeycardAPI( + api_key=os.environ.get("KEYCARD_API_API_KEY"), # This is the default and can be omitted +) async def main() -> None: @@ -68,6 +79,7 @@ pip install keycardai_api[aiohttp] Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`: ```python +import os import asyncio from keycardai_api import DefaultAioHttpClient from keycardai_api import AsyncKeycardAPI @@ -75,6 +87,7 @@ from keycardai_api import AsyncKeycardAPI async def main() -> None: async with AsyncKeycardAPI( + api_key=os.environ.get("KEYCARD_API_API_KEY"), # This is the default and can be omitted http_client=DefaultAioHttpClient(), ) as client: zones = await client.zones.list() diff --git a/api.md b/api.md index 48d50a6..d89eeb2 100644 --- a/api.md +++ b/api.md @@ -60,6 +60,20 @@ Methods: - client.zones.applications.dependencies.add(dependency_id, \*, zone_id, id, \*\*params) -> None - client.zones.applications.dependencies.remove(dependency_id, \*, zone_id, id) -> None +### Roles + +Types: + +```python +from keycardai_api.types.zones.applications import RoleListResponse +``` + +Methods: + +- client.zones.applications.roles.list(application_id, \*, zone_id, \*\*params) -> RoleListResponse +- client.zones.applications.roles.assign(application_id, \*, zone_id, \*\*params) -> RoleAssignment +- client.zones.applications.roles.revoke(role_id, \*, zone_id, application_id, \*\*params) -> None + ## ApplicationCredentials Types: @@ -106,7 +120,7 @@ Methods: Types: ```python -from keycardai_api.types.zones import Provider, ProviderListResponse +from keycardai_api.types.zones import Provider, ValidationResult, ProviderListResponse ``` Methods: @@ -116,6 +130,7 @@ Methods: - client.zones.providers.update(id, \*, zone_id, \*\*params) -> Provider - client.zones.providers.list(zone_id, \*\*params) -> ProviderListResponse - client.zones.providers.delete(id, \*, zone_id) -> None +- client.zones.providers.validate(id, \*, zone_id) -> ValidationResult ## Resources @@ -171,8 +186,24 @@ from keycardai_api.types.zones import User, UserListResponse Methods: -- client.zones.users.retrieve(id, \*, zone_id) -> User -- client.zones.users.list(zone_id, \*\*params) -> UserListResponse +- client.zones.users.retrieve(id, \*, zone_id, \*\*params) -> User +- client.zones.users.update(id, \*, zone_id, \*\*params) -> User +- client.zones.users.list(zone_id, \*\*params) -> UserListResponse +- client.zones.users.delete(id, \*, zone_id) -> None + +### Roles + +Types: + +```python +from keycardai_api.types.zones.users import RoleAssignment, RoleAssignmentCreate, RoleListResponse +``` + +Methods: + +- client.zones.users.roles.list(user_id, \*, zone_id, \*\*params) -> RoleListResponse +- client.zones.users.roles.assign(user_id, \*, zone_id, \*\*params) -> RoleAssignment +- client.zones.users.roles.revoke(role_id, \*, zone_id, user_id, \*\*params) -> None ## Members @@ -190,6 +221,66 @@ Methods: - client.zones.members.delete(organization_user_id, \*, zone_id) -> None - client.zones.members.add(zone_id, \*\*params) -> ZoneMember +## Roles + +Types: + +```python +from keycardai_api.types.zones import Role, RoleCreate, RoleUpdate, RoleListResponse +``` + +Methods: + +- client.zones.roles.create(zone_id, \*\*params) -> Role +- client.zones.roles.retrieve(role_id, \*, zone_id) -> Role +- client.zones.roles.update(role_id, \*, zone_id, \*\*params) -> Role +- client.zones.roles.list(zone_id, \*\*params) -> RoleListResponse +- client.zones.roles.delete(role_id, \*, zone_id) -> None + +## Groups + +Types: + +```python +from keycardai_api.types.zones import Group, GroupCreate, GroupUpdate, GroupListResponse +``` + +Methods: + +- client.zones.groups.create(zone_id, \*\*params) -> Group +- client.zones.groups.retrieve(group_id, \*, zone_id, \*\*params) -> Group +- client.zones.groups.update(group_id, \*, zone_id, \*\*params) -> Group +- client.zones.groups.list(zone_id, \*\*params) -> GroupListResponse +- client.zones.groups.delete(group_id, \*, zone_id) -> None + +### Members + +Types: + +```python +from keycardai_api.types.zones.groups import GroupMember, GroupMemberCreate, MemberListResponse +``` + +Methods: + +- client.zones.groups.members.list(group_id, \*, zone_id, \*\*params) -> MemberListResponse +- client.zones.groups.members.add(group_id, \*, zone_id, \*\*params) -> GroupMember +- client.zones.groups.members.remove(user_id, \*, zone_id, group_id) -> None + +### Roles + +Types: + +```python +from keycardai_api.types.zones.groups import RoleListResponse +``` + +Methods: + +- client.zones.groups.roles.list(group_id, \*, zone_id, \*\*params) -> RoleListResponse +- client.zones.groups.roles.add(group_id, \*, zone_id, \*\*params) -> RoleAssignment +- client.zones.groups.roles.remove(role_id, \*, zone_id, group_id, \*\*params) -> None + ## Secrets Types: @@ -261,46 +352,6 @@ Methods: - client.zones.policies.versions.list(policy_id, \*, zone_id, \*\*params) -> VersionListResponse - client.zones.policies.versions.archive(version_id, \*, zone_id, policy_id) -> PolicyVersion -## Packages - -Types: - -```python -from keycardai_api.types.zones import ( - InputState, - Package, - PackageDraft, - PackageInputBinding, - PackageList, - PackageOutputBinding, - PackageSource, -) -``` - -### Versions - -Types: - -```python -from keycardai_api.types.zones.packages import PackageVersion, PackageVersionList -``` - -## Installs - -Types: - -```python -from keycardai_api.types.zones import Install, InstallList, InstallStatus -``` - -## CatalogTasks - -Types: - -```python -from keycardai_api.types.zones import Task, TaskOperation, TaskStatus -``` - ## PolicySets Types: @@ -369,7 +420,7 @@ Methods: - client.organizations.retrieve(organization_id, \*\*params) -> Organization - client.organizations.update(organization_id, \*\*params) -> Organization - client.organizations.list(\*\*params) -> OrganizationListResponse -- client.organizations.exchange_token(organization_id) -> TokenResponse +- client.organizations.delete(organization_id) -> None - client.organizations.list_identities(organization_id, \*\*params) -> OrganizationListIdentitiesResponse - client.organizations.list_roles(organization_id, \*\*params) -> OrganizationListRolesResponse @@ -470,3 +521,11 @@ Methods: - client.invitations.retrieve(token) -> InvitationRetrieveResponse - client.invitations.accept(token) -> InvitationAcceptResponse + +# PolicyBundle + +Methods: + +- client.policy_bundle.retrieve() -> BinaryAPIResponse +- client.policy_bundle.update(body, \*\*params) -> BinaryAPIResponse +- client.policy_bundle.reset() -> None diff --git a/pyproject.toml b/pyproject.toml index 0f72996..a2de0a3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "keycardai_api" -version = "0.16.0" +version = "0.17.0" description = "The official Python library for the keycard-api API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/scripts/lint b/scripts/lint index 97303ed..7b9389c 100755 --- a/scripts/lint +++ b/scripts/lint @@ -13,7 +13,7 @@ else fi echo "==> Running pyright" -uv run pyright +uv run pyright -p . echo "==> Running mypy" uv run mypy . diff --git a/src/keycardai_api/_client.py b/src/keycardai_api/_client.py index 103db47..d76b6ee 100644 --- a/src/keycardai_api/_client.py +++ b/src/keycardai_api/_client.py @@ -37,9 +37,10 @@ ) if TYPE_CHECKING: - from .resources import zones, invitations, organizations + from .resources import zones, invitations, organizations, policy_bundle from .resources.invitations import InvitationsResource, AsyncInvitationsResource from .resources.zones.zones import ZonesResource, AsyncZonesResource + from .resources.policy_bundle import PolicyBundleResource, AsyncPolicyBundleResource from .resources.organizations.organizations import OrganizationsResource, AsyncOrganizationsResource __all__ = [ @@ -147,6 +148,35 @@ def invitations(self) -> InvitationsResource: return InvitationsResource(self) + @cached_property + def policy_bundle(self) -> PolicyBundleResource: + """Per-user Policy Bundle resource. + + Allows clients (typically the Keycard CLI) + to GET, PUT, and DELETE the effective Policy Set for the calling user + on a zone. The bundle is encoded with a content-negotiated codec (currently + only `application/vnd.keycard.policy-bundle.v1+tar+gzip`). + + ## Archive layout + + The bundle is a gzip-compressed tar archive with this logical layout: + + | Entry | Required on PUT | Notes | + |-------|-----------------|-------| + | `manifest.json` | **Yes** | See `PolicyBundleManifest`. The only source of the authoritative `schema.version`. | + | `schema.cedarschema` | No | Convenience snapshot of the Cedar schema. **Ignored on PUT** — the server validates policies against its own attested schema for `manifest.schema.version`. **Always present on GET.** | + | `policies/.cedar` | — | One Cedar policy per file; the filename stem is the policy's public ID. | + + Decode rules: duplicate entries and unrecognized/nested entries are + rejected (`bundle_invalid`). On PUT the manifest's `sha` fields and + `policies[]` list are advisory — the server recomputes every digest from + the archived bytes and derives the policy set from the `policies/` files. + On GET every digest is authoritative. + """ + from .resources.policy_bundle import PolicyBundleResource + + return PolicyBundleResource(self) + @cached_property def with_raw_response(self) -> KeycardAPIWithRawResponse: return KeycardAPIWithRawResponse(self) @@ -160,12 +190,27 @@ def with_streaming_response(self) -> KeycardAPIWithStreamedResponse: def qs(self) -> Querystring: return Querystring(array_format="repeat") + @override + def _auth_headers(self, security: SecurityOptions) -> dict[str, str]: + headers: dict[str, str] = {} + if security.get("bearer_auth", False): + for key, value in self._bearer_auth.items(): + headers.setdefault(key, value) + return headers + @override def _custom_auth(self, security: SecurityOptions) -> httpx.Auth | None: if security.get("o_auth2", False) and self._o_auth2 is not None: return self._o_auth2 return None + @property + def _bearer_auth(self) -> dict[str, str]: + api_key = self.api_key + if api_key is None: + return {} + return {"Authorization": f"Bearer {api_key}"} + @property def _o_auth2(self) -> httpx.Auth | None: if self.client_id and self.client_secret: @@ -377,6 +422,35 @@ def invitations(self) -> AsyncInvitationsResource: return AsyncInvitationsResource(self) + @cached_property + def policy_bundle(self) -> AsyncPolicyBundleResource: + """Per-user Policy Bundle resource. + + Allows clients (typically the Keycard CLI) + to GET, PUT, and DELETE the effective Policy Set for the calling user + on a zone. The bundle is encoded with a content-negotiated codec (currently + only `application/vnd.keycard.policy-bundle.v1+tar+gzip`). + + ## Archive layout + + The bundle is a gzip-compressed tar archive with this logical layout: + + | Entry | Required on PUT | Notes | + |-------|-----------------|-------| + | `manifest.json` | **Yes** | See `PolicyBundleManifest`. The only source of the authoritative `schema.version`. | + | `schema.cedarschema` | No | Convenience snapshot of the Cedar schema. **Ignored on PUT** — the server validates policies against its own attested schema for `manifest.schema.version`. **Always present on GET.** | + | `policies/.cedar` | — | One Cedar policy per file; the filename stem is the policy's public ID. | + + Decode rules: duplicate entries and unrecognized/nested entries are + rejected (`bundle_invalid`). On PUT the manifest's `sha` fields and + `policies[]` list are advisory — the server recomputes every digest from + the archived bytes and derives the policy set from the `policies/` files. + On GET every digest is authoritative. + """ + from .resources.policy_bundle import AsyncPolicyBundleResource + + return AsyncPolicyBundleResource(self) + @cached_property def with_raw_response(self) -> AsyncKeycardAPIWithRawResponse: return AsyncKeycardAPIWithRawResponse(self) @@ -390,12 +464,27 @@ def with_streaming_response(self) -> AsyncKeycardAPIWithStreamedResponse: def qs(self) -> Querystring: return Querystring(array_format="repeat") + @override + def _auth_headers(self, security: SecurityOptions) -> dict[str, str]: + headers: dict[str, str] = {} + if security.get("bearer_auth", False): + for key, value in self._bearer_auth.items(): + headers.setdefault(key, value) + return headers + @override def _custom_auth(self, security: SecurityOptions) -> httpx.Auth | None: if security.get("o_auth2", False) and self._o_auth2 is not None: return self._o_auth2 return None + @property + def _bearer_auth(self) -> dict[str, str]: + api_key = self.api_key + if api_key is None: + return {} + return {"Authorization": f"Bearer {api_key}"} + @property def _o_auth2(self) -> httpx.Auth | None: if self.client_id and self.client_secret: @@ -538,6 +627,35 @@ def invitations(self) -> invitations.InvitationsResourceWithRawResponse: return InvitationsResourceWithRawResponse(self._client.invitations) + @cached_property + def policy_bundle(self) -> policy_bundle.PolicyBundleResourceWithRawResponse: + """Per-user Policy Bundle resource. + + Allows clients (typically the Keycard CLI) + to GET, PUT, and DELETE the effective Policy Set for the calling user + on a zone. The bundle is encoded with a content-negotiated codec (currently + only `application/vnd.keycard.policy-bundle.v1+tar+gzip`). + + ## Archive layout + + The bundle is a gzip-compressed tar archive with this logical layout: + + | Entry | Required on PUT | Notes | + |-------|-----------------|-------| + | `manifest.json` | **Yes** | See `PolicyBundleManifest`. The only source of the authoritative `schema.version`. | + | `schema.cedarschema` | No | Convenience snapshot of the Cedar schema. **Ignored on PUT** — the server validates policies against its own attested schema for `manifest.schema.version`. **Always present on GET.** | + | `policies/.cedar` | — | One Cedar policy per file; the filename stem is the policy's public ID. | + + Decode rules: duplicate entries and unrecognized/nested entries are + rejected (`bundle_invalid`). On PUT the manifest's `sha` fields and + `policies[]` list are advisory — the server recomputes every digest from + the archived bytes and derives the policy set from the `policies/` files. + On GET every digest is authoritative. + """ + from .resources.policy_bundle import PolicyBundleResourceWithRawResponse + + return PolicyBundleResourceWithRawResponse(self._client.policy_bundle) + class AsyncKeycardAPIWithRawResponse: _client: AsyncKeycardAPI @@ -563,6 +681,35 @@ def invitations(self) -> invitations.AsyncInvitationsResourceWithRawResponse: return AsyncInvitationsResourceWithRawResponse(self._client.invitations) + @cached_property + def policy_bundle(self) -> policy_bundle.AsyncPolicyBundleResourceWithRawResponse: + """Per-user Policy Bundle resource. + + Allows clients (typically the Keycard CLI) + to GET, PUT, and DELETE the effective Policy Set for the calling user + on a zone. The bundle is encoded with a content-negotiated codec (currently + only `application/vnd.keycard.policy-bundle.v1+tar+gzip`). + + ## Archive layout + + The bundle is a gzip-compressed tar archive with this logical layout: + + | Entry | Required on PUT | Notes | + |-------|-----------------|-------| + | `manifest.json` | **Yes** | See `PolicyBundleManifest`. The only source of the authoritative `schema.version`. | + | `schema.cedarschema` | No | Convenience snapshot of the Cedar schema. **Ignored on PUT** — the server validates policies against its own attested schema for `manifest.schema.version`. **Always present on GET.** | + | `policies/.cedar` | — | One Cedar policy per file; the filename stem is the policy's public ID. | + + Decode rules: duplicate entries and unrecognized/nested entries are + rejected (`bundle_invalid`). On PUT the manifest's `sha` fields and + `policies[]` list are advisory — the server recomputes every digest from + the archived bytes and derives the policy set from the `policies/` files. + On GET every digest is authoritative. + """ + from .resources.policy_bundle import AsyncPolicyBundleResourceWithRawResponse + + return AsyncPolicyBundleResourceWithRawResponse(self._client.policy_bundle) + class KeycardAPIWithStreamedResponse: _client: KeycardAPI @@ -588,6 +735,35 @@ def invitations(self) -> invitations.InvitationsResourceWithStreamingResponse: return InvitationsResourceWithStreamingResponse(self._client.invitations) + @cached_property + def policy_bundle(self) -> policy_bundle.PolicyBundleResourceWithStreamingResponse: + """Per-user Policy Bundle resource. + + Allows clients (typically the Keycard CLI) + to GET, PUT, and DELETE the effective Policy Set for the calling user + on a zone. The bundle is encoded with a content-negotiated codec (currently + only `application/vnd.keycard.policy-bundle.v1+tar+gzip`). + + ## Archive layout + + The bundle is a gzip-compressed tar archive with this logical layout: + + | Entry | Required on PUT | Notes | + |-------|-----------------|-------| + | `manifest.json` | **Yes** | See `PolicyBundleManifest`. The only source of the authoritative `schema.version`. | + | `schema.cedarschema` | No | Convenience snapshot of the Cedar schema. **Ignored on PUT** — the server validates policies against its own attested schema for `manifest.schema.version`. **Always present on GET.** | + | `policies/.cedar` | — | One Cedar policy per file; the filename stem is the policy's public ID. | + + Decode rules: duplicate entries and unrecognized/nested entries are + rejected (`bundle_invalid`). On PUT the manifest's `sha` fields and + `policies[]` list are advisory — the server recomputes every digest from + the archived bytes and derives the policy set from the `policies/` files. + On GET every digest is authoritative. + """ + from .resources.policy_bundle import PolicyBundleResourceWithStreamingResponse + + return PolicyBundleResourceWithStreamingResponse(self._client.policy_bundle) + class AsyncKeycardAPIWithStreamedResponse: _client: AsyncKeycardAPI @@ -613,6 +789,35 @@ def invitations(self) -> invitations.AsyncInvitationsResourceWithStreamingRespon return AsyncInvitationsResourceWithStreamingResponse(self._client.invitations) + @cached_property + def policy_bundle(self) -> policy_bundle.AsyncPolicyBundleResourceWithStreamingResponse: + """Per-user Policy Bundle resource. + + Allows clients (typically the Keycard CLI) + to GET, PUT, and DELETE the effective Policy Set for the calling user + on a zone. The bundle is encoded with a content-negotiated codec (currently + only `application/vnd.keycard.policy-bundle.v1+tar+gzip`). + + ## Archive layout + + The bundle is a gzip-compressed tar archive with this logical layout: + + | Entry | Required on PUT | Notes | + |-------|-----------------|-------| + | `manifest.json` | **Yes** | See `PolicyBundleManifest`. The only source of the authoritative `schema.version`. | + | `schema.cedarschema` | No | Convenience snapshot of the Cedar schema. **Ignored on PUT** — the server validates policies against its own attested schema for `manifest.schema.version`. **Always present on GET.** | + | `policies/.cedar` | — | One Cedar policy per file; the filename stem is the policy's public ID. | + + Decode rules: duplicate entries and unrecognized/nested entries are + rejected (`bundle_invalid`). On PUT the manifest's `sha` fields and + `policies[]` list are advisory — the server recomputes every digest from + the archived bytes and derives the policy set from the `policies/` files. + On GET every digest is authoritative. + """ + from .resources.policy_bundle import AsyncPolicyBundleResourceWithStreamingResponse + + return AsyncPolicyBundleResourceWithStreamingResponse(self._client.policy_bundle) + Client = KeycardAPI diff --git a/src/keycardai_api/_models.py b/src/keycardai_api/_models.py index 13d0794..90bf844 100644 --- a/src/keycardai_api/_models.py +++ b/src/keycardai_api/_models.py @@ -872,6 +872,7 @@ def _create_pydantic_model(type_: _T) -> Type[RootModel[_T]]: class SecurityOptions(TypedDict, total=False): + bearer_auth: bool o_auth2: bool @@ -903,7 +904,10 @@ class FinalRequestOptions(pydantic.BaseModel): idempotency_key: Union[str, None] = None post_parser: Union[Callable[[Any], Any], NotGiven] = NotGiven() follow_redirects: Union[bool, None] = None - security: SecurityOptions = {"o_auth2": True} + security: SecurityOptions = { + "bearer_auth": True, + "o_auth2": True, + } content: Union[bytes, bytearray, IO[bytes], Iterable[bytes], AsyncIterable[bytes], None] = None # It should be noted that we cannot use `json` here as that would override diff --git a/src/keycardai_api/_version.py b/src/keycardai_api/_version.py index ff81ceb..cb30031 100644 --- a/src/keycardai_api/_version.py +++ b/src/keycardai_api/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "keycardai_api" -__version__ = "0.16.0" # x-release-please-version +__version__ = "0.17.0" # x-release-please-version diff --git a/src/keycardai_api/resources/__init__.py b/src/keycardai_api/resources/__init__.py index d7c6a99..3e222d8 100644 --- a/src/keycardai_api/resources/__init__.py +++ b/src/keycardai_api/resources/__init__.py @@ -24,6 +24,14 @@ OrganizationsResourceWithStreamingResponse, AsyncOrganizationsResourceWithStreamingResponse, ) +from .policy_bundle import ( + PolicyBundleResource, + AsyncPolicyBundleResource, + PolicyBundleResourceWithRawResponse, + AsyncPolicyBundleResourceWithRawResponse, + PolicyBundleResourceWithStreamingResponse, + AsyncPolicyBundleResourceWithStreamingResponse, +) __all__ = [ "ZonesResource", @@ -44,4 +52,10 @@ "AsyncInvitationsResourceWithRawResponse", "InvitationsResourceWithStreamingResponse", "AsyncInvitationsResourceWithStreamingResponse", + "PolicyBundleResource", + "AsyncPolicyBundleResource", + "PolicyBundleResourceWithRawResponse", + "AsyncPolicyBundleResourceWithRawResponse", + "PolicyBundleResourceWithStreamingResponse", + "AsyncPolicyBundleResourceWithStreamingResponse", ] diff --git a/src/keycardai_api/resources/organizations/organizations.py b/src/keycardai_api/resources/organizations/organizations.py index 49df542..3c38117 100644 --- a/src/keycardai_api/resources/organizations/organizations.py +++ b/src/keycardai_api/resources/organizations/organizations.py @@ -24,7 +24,7 @@ organization_list_roles_params, organization_list_identities_params, ) -from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from ..._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given from ..._utils import path_template, maybe_transform, strip_not_given, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource @@ -54,7 +54,6 @@ from ...types.role_scope import RoleScope from ...types.organization import Organization from ...types.organizations import OrganizationRole -from ...types.token_response import TokenResponse from .service_accounts.service_accounts import ( ServiceAccountsResource, AsyncServiceAccountsResource, @@ -290,7 +289,7 @@ def list( cast_to=OrganizationListResponse, ) - def exchange_token( + def delete( self, organization_id: str, *, @@ -301,9 +300,9 @@ def exchange_token( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> TokenResponse: + ) -> None: """ - Exchange user token for organization-scoped M2M token + Deletes the organization and all zones. Args: organization_id: Organization ID or label identifier @@ -318,13 +317,14 @@ def exchange_token( """ if not organization_id: raise ValueError(f"Expected a non-empty value for `organization_id` but received {organization_id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} extra_headers = {**strip_not_given({"X-Client-Request-ID": x_client_request_id}), **(extra_headers or {})} - return self._post( - path_template("/organizations/{organization_id}/token", organization_id=organization_id), + return self._delete( + path_template("/organizations/{organization_id}", organization_id=organization_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=TokenResponse, + cast_to=NoneType, ) def list_identities( @@ -688,7 +688,7 @@ async def list( cast_to=OrganizationListResponse, ) - async def exchange_token( + async def delete( self, organization_id: str, *, @@ -699,9 +699,9 @@ async def exchange_token( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> TokenResponse: + ) -> None: """ - Exchange user token for organization-scoped M2M token + Deletes the organization and all zones. Args: organization_id: Organization ID or label identifier @@ -716,13 +716,14 @@ async def exchange_token( """ if not organization_id: raise ValueError(f"Expected a non-empty value for `organization_id` but received {organization_id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} extra_headers = {**strip_not_given({"X-Client-Request-ID": x_client_request_id}), **(extra_headers or {})} - return await self._post( - path_template("/organizations/{organization_id}/token", organization_id=organization_id), + return await self._delete( + path_template("/organizations/{organization_id}", organization_id=organization_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=TokenResponse, + cast_to=NoneType, ) async def list_identities( @@ -881,8 +882,8 @@ def __init__(self, organizations: OrganizationsResource) -> None: self.list = to_raw_response_wrapper( organizations.list, ) - self.exchange_token = to_raw_response_wrapper( - organizations.exchange_token, + self.delete = to_raw_response_wrapper( + organizations.delete, ) self.list_identities = to_raw_response_wrapper( organizations.list_identities, @@ -924,8 +925,8 @@ def __init__(self, organizations: AsyncOrganizationsResource) -> None: self.list = async_to_raw_response_wrapper( organizations.list, ) - self.exchange_token = async_to_raw_response_wrapper( - organizations.exchange_token, + self.delete = async_to_raw_response_wrapper( + organizations.delete, ) self.list_identities = async_to_raw_response_wrapper( organizations.list_identities, @@ -967,8 +968,8 @@ def __init__(self, organizations: OrganizationsResource) -> None: self.list = to_streamed_response_wrapper( organizations.list, ) - self.exchange_token = to_streamed_response_wrapper( - organizations.exchange_token, + self.delete = to_streamed_response_wrapper( + organizations.delete, ) self.list_identities = to_streamed_response_wrapper( organizations.list_identities, @@ -1010,8 +1011,8 @@ def __init__(self, organizations: AsyncOrganizationsResource) -> None: self.list = async_to_streamed_response_wrapper( organizations.list, ) - self.exchange_token = async_to_streamed_response_wrapper( - organizations.exchange_token, + self.delete = async_to_streamed_response_wrapper( + organizations.delete, ) self.list_identities = async_to_streamed_response_wrapper( organizations.list_identities, diff --git a/src/keycardai_api/resources/organizations/sso_connection.py b/src/keycardai_api/resources/organizations/sso_connection.py index 4a8a44f..002dbed 100644 --- a/src/keycardai_api/resources/organizations/sso_connection.py +++ b/src/keycardai_api/resources/organizations/sso_connection.py @@ -104,7 +104,7 @@ def update( client_id: str | Omit = omit, client_secret: str | Omit = omit, identifier: str | Omit = omit, - protocols: Optional[SSOConnectionProtocolParam] | Omit = omit, + protocols: Optional[sso_connection_update_params.Protocols] | Omit = omit, x_client_request_id: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -125,7 +125,8 @@ def update( identifier: SSO provider identifier (e.g., issuer URL) - protocols: Protocol configuration for SSO connection + protocols: Protocol configuration for an SSO connection update. Omit a protocol to leave it + unchanged. extra_headers: Send extra headers @@ -329,7 +330,7 @@ async def update( client_id: str | Omit = omit, client_secret: str | Omit = omit, identifier: str | Omit = omit, - protocols: Optional[SSOConnectionProtocolParam] | Omit = omit, + protocols: Optional[sso_connection_update_params.Protocols] | Omit = omit, x_client_request_id: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -350,7 +351,8 @@ async def update( identifier: SSO provider identifier (e.g., issuer URL) - protocols: Protocol configuration for SSO connection + protocols: Protocol configuration for an SSO connection update. Omit a protocol to leave it + unchanged. extra_headers: Send extra headers diff --git a/src/keycardai_api/resources/policy_bundle.py b/src/keycardai_api/resources/policy_bundle.py new file mode 100644 index 0000000..e68008f --- /dev/null +++ b/src/keycardai_api/resources/policy_bundle.py @@ -0,0 +1,542 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os + +import httpx + +from .._files import read_file_content, async_read_file_content +from .._types import ( + Body, + Omit, + Query, + Headers, + NoneType, + NotGiven, + BinaryTypes, + FileContent, + AsyncBinaryTypes, + omit, + not_given, +) +from .._utils import strip_not_given +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + BinaryAPIResponse, + AsyncBinaryAPIResponse, + StreamedBinaryAPIResponse, + AsyncStreamedBinaryAPIResponse, + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + to_custom_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_custom_streamed_response_wrapper, + async_to_custom_raw_response_wrapper, + async_to_custom_streamed_response_wrapper, +) +from .._base_client import make_request_options + +__all__ = ["PolicyBundleResource", "AsyncPolicyBundleResource"] + + +class PolicyBundleResource(SyncAPIResource): + """Per-user Policy Bundle resource. + + Allows clients (typically the Keycard CLI) + to GET, PUT, and DELETE the effective Policy Set for the calling user + on a zone. The bundle is encoded with a content-negotiated codec (currently + only `application/vnd.keycard.policy-bundle.v1+tar+gzip`). + + ## Archive layout + + The bundle is a gzip-compressed tar archive with this logical layout: + + | Entry | Required on PUT | Notes | + |-------|-----------------|-------| + | `manifest.json` | **Yes** | See `PolicyBundleManifest`. The only source of the authoritative `schema.version`. | + | `schema.cedarschema` | No | Convenience snapshot of the Cedar schema. **Ignored on PUT** — the server validates policies against its own attested schema for `manifest.schema.version`. **Always present on GET.** | + | `policies/.cedar` | — | One Cedar policy per file; the filename stem is the policy's public ID. | + + Decode rules: duplicate entries and unrecognized/nested entries are + rejected (`bundle_invalid`). On PUT the manifest's `sha` fields and + `policies[]` list are advisory — the server recomputes every digest from + the archived bytes and derives the policy set from the `policies/` files. + On GET every digest is authoritative. + """ + + @cached_property + def with_raw_response(self) -> PolicyBundleResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/keycardai/keycard-python#accessing-raw-response-data-eg-headers + """ + return PolicyBundleResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> PolicyBundleResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/keycardai/keycard-python#with_streaming_response + """ + return PolicyBundleResourceWithStreamingResponse(self) + + def retrieve( + self, + *, + if_none_match: str | Omit = omit, + x_client_request_id: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> BinaryAPIResponse: + """ + Returns the effective Policy Bundle for the user identified by the zone-issued + resource-scoped token. When no user-scope binding exists, one will be generated + from the default set. + + The response body is a binary archive in the codec selected via the `Accept` + header. The only codec supported today is + `application/vnd.keycard.policy-bundle.v1+tar+gzip`. Clients SHOULD send an + explicit `Accept` header; absent one, the server defaults to the tar+gzip codec. + + Supports conditional fetch via `If-None-Match`: when the supplied ETag matches + the current bundle, the server responds `304 Not Modified` with no body. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})} + extra_headers = { + **strip_not_given( + { + "If-None-Match": if_none_match, + "X-Client-Request-ID": x_client_request_id, + } + ), + **(extra_headers or {}), + } + return self._get( + "/policy/bundle", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + security={"bearer_auth": True}, + ), + cast_to=BinaryAPIResponse, + ) + + def update( + self, + body: FileContent | BinaryTypes, + *, + if_match: str | Omit = omit, + x_client_request_id: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> BinaryAPIResponse: + """ + Accepts an edited Policy Bundle archive and applies it as the active user-scope + PolicySetVersion for the calling user. + + The user's policy set is seeded from the system-default policies on first + access, forked into customer-owned policies; a user bundle therefore contains + only customer-owned policies. Applying an edit creates a new version of the + affected policy, and a `new_policy` entry adds a further customer-owned policy. + Platform-owned catalog policies are never edited in place by this operation. + + The request body codec is determined from `Content-Type`. The only codec + supported today is `application/vnd.keycard.policy-bundle.v1+tar+gzip`. + + Supports optimistic concurrency via `If-Match`: when supplied, the server + applies the bundle only if the supplied ETag matches the current bundle ETag; + otherwise responds `412 Precondition Failed`. + + On success the server returns the materialized bundle (in the same codec) and + its new `ETag`. + + Args: + body: tar+gzip Policy Bundle archive. `manifest.json` is **required** (see + `PolicyBundleManifest`); `schema.cedarschema` is **optional and ignored** — the + server validates against its attested schema for `manifest.schema.version`. The + manifest's `policies[]` list is authoritative for the resulting set: each entry + must have a matching `policies/.cedar` (or, for a `new_policy` entry, + `policies/.cedar`) member, and a member with no manifest entry is + dropped. Only the `sha` fields are advisory and recomputed server-side. + Duplicate or unrecognized entries are rejected with `bundle_invalid`. See the + **PolicyBundle** tag for the layout. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})} + extra_headers = { + **strip_not_given( + { + "If-Match": if_match, + "X-Client-Request-ID": x_client_request_id, + } + ), + **(extra_headers or {}), + } + extra_headers = {"Content-Type": "application/octet-stream", **(extra_headers or {})} + return self._put( + "/policy/bundle", + content=read_file_content(body) if isinstance(body, os.PathLike) else body, + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + security={"bearer_auth": True}, + ), + cast_to=BinaryAPIResponse, + ) + + def reset( + self, + *, + x_client_request_id: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> None: + """ + Archives the PolicySet for the calling user (if any), causing subsequent + `GET /policy/bundle` requests to fall back to the default user policies. + Idempotent: returns `204 No Content` even when no user-scope binding exists. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + extra_headers = {**strip_not_given({"X-Client-Request-ID": x_client_request_id}), **(extra_headers or {})} + return self._delete( + "/policy/bundle", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + security={"bearer_auth": True}, + ), + cast_to=NoneType, + ) + + +class AsyncPolicyBundleResource(AsyncAPIResource): + """Per-user Policy Bundle resource. + + Allows clients (typically the Keycard CLI) + to GET, PUT, and DELETE the effective Policy Set for the calling user + on a zone. The bundle is encoded with a content-negotiated codec (currently + only `application/vnd.keycard.policy-bundle.v1+tar+gzip`). + + ## Archive layout + + The bundle is a gzip-compressed tar archive with this logical layout: + + | Entry | Required on PUT | Notes | + |-------|-----------------|-------| + | `manifest.json` | **Yes** | See `PolicyBundleManifest`. The only source of the authoritative `schema.version`. | + | `schema.cedarschema` | No | Convenience snapshot of the Cedar schema. **Ignored on PUT** — the server validates policies against its own attested schema for `manifest.schema.version`. **Always present on GET.** | + | `policies/.cedar` | — | One Cedar policy per file; the filename stem is the policy's public ID. | + + Decode rules: duplicate entries and unrecognized/nested entries are + rejected (`bundle_invalid`). On PUT the manifest's `sha` fields and + `policies[]` list are advisory — the server recomputes every digest from + the archived bytes and derives the policy set from the `policies/` files. + On GET every digest is authoritative. + """ + + @cached_property + def with_raw_response(self) -> AsyncPolicyBundleResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/keycardai/keycard-python#accessing-raw-response-data-eg-headers + """ + return AsyncPolicyBundleResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncPolicyBundleResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/keycardai/keycard-python#with_streaming_response + """ + return AsyncPolicyBundleResourceWithStreamingResponse(self) + + async def retrieve( + self, + *, + if_none_match: str | Omit = omit, + x_client_request_id: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AsyncBinaryAPIResponse: + """ + Returns the effective Policy Bundle for the user identified by the zone-issued + resource-scoped token. When no user-scope binding exists, one will be generated + from the default set. + + The response body is a binary archive in the codec selected via the `Accept` + header. The only codec supported today is + `application/vnd.keycard.policy-bundle.v1+tar+gzip`. Clients SHOULD send an + explicit `Accept` header; absent one, the server defaults to the tar+gzip codec. + + Supports conditional fetch via `If-None-Match`: when the supplied ETag matches + the current bundle, the server responds `304 Not Modified` with no body. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})} + extra_headers = { + **strip_not_given( + { + "If-None-Match": if_none_match, + "X-Client-Request-ID": x_client_request_id, + } + ), + **(extra_headers or {}), + } + return await self._get( + "/policy/bundle", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + security={"bearer_auth": True}, + ), + cast_to=AsyncBinaryAPIResponse, + ) + + async def update( + self, + body: FileContent | AsyncBinaryTypes, + *, + if_match: str | Omit = omit, + x_client_request_id: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AsyncBinaryAPIResponse: + """ + Accepts an edited Policy Bundle archive and applies it as the active user-scope + PolicySetVersion for the calling user. + + The user's policy set is seeded from the system-default policies on first + access, forked into customer-owned policies; a user bundle therefore contains + only customer-owned policies. Applying an edit creates a new version of the + affected policy, and a `new_policy` entry adds a further customer-owned policy. + Platform-owned catalog policies are never edited in place by this operation. + + The request body codec is determined from `Content-Type`. The only codec + supported today is `application/vnd.keycard.policy-bundle.v1+tar+gzip`. + + Supports optimistic concurrency via `If-Match`: when supplied, the server + applies the bundle only if the supplied ETag matches the current bundle ETag; + otherwise responds `412 Precondition Failed`. + + On success the server returns the materialized bundle (in the same codec) and + its new `ETag`. + + Args: + body: tar+gzip Policy Bundle archive. `manifest.json` is **required** (see + `PolicyBundleManifest`); `schema.cedarschema` is **optional and ignored** — the + server validates against its attested schema for `manifest.schema.version`. The + manifest's `policies[]` list is authoritative for the resulting set: each entry + must have a matching `policies/.cedar` (or, for a `new_policy` entry, + `policies/.cedar`) member, and a member with no manifest entry is + dropped. Only the `sha` fields are advisory and recomputed server-side. + Duplicate or unrecognized entries are rejected with `bundle_invalid`. See the + **PolicyBundle** tag for the layout. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})} + extra_headers = { + **strip_not_given( + { + "If-Match": if_match, + "X-Client-Request-ID": x_client_request_id, + } + ), + **(extra_headers or {}), + } + extra_headers = {"Content-Type": "application/octet-stream", **(extra_headers or {})} + return await self._put( + "/policy/bundle", + content=await async_read_file_content(body) if isinstance(body, os.PathLike) else body, + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + security={"bearer_auth": True}, + ), + cast_to=AsyncBinaryAPIResponse, + ) + + async def reset( + self, + *, + x_client_request_id: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> None: + """ + Archives the PolicySet for the calling user (if any), causing subsequent + `GET /policy/bundle` requests to fall back to the default user policies. + Idempotent: returns `204 No Content` even when no user-scope binding exists. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + extra_headers = {**strip_not_given({"X-Client-Request-ID": x_client_request_id}), **(extra_headers or {})} + return await self._delete( + "/policy/bundle", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + security={"bearer_auth": True}, + ), + cast_to=NoneType, + ) + + +class PolicyBundleResourceWithRawResponse: + def __init__(self, policy_bundle: PolicyBundleResource) -> None: + self._policy_bundle = policy_bundle + + self.retrieve = to_custom_raw_response_wrapper( + policy_bundle.retrieve, + BinaryAPIResponse, + ) + self.update = to_custom_raw_response_wrapper( + policy_bundle.update, + BinaryAPIResponse, + ) + self.reset = to_raw_response_wrapper( + policy_bundle.reset, + ) + + +class AsyncPolicyBundleResourceWithRawResponse: + def __init__(self, policy_bundle: AsyncPolicyBundleResource) -> None: + self._policy_bundle = policy_bundle + + self.retrieve = async_to_custom_raw_response_wrapper( + policy_bundle.retrieve, + AsyncBinaryAPIResponse, + ) + self.update = async_to_custom_raw_response_wrapper( + policy_bundle.update, + AsyncBinaryAPIResponse, + ) + self.reset = async_to_raw_response_wrapper( + policy_bundle.reset, + ) + + +class PolicyBundleResourceWithStreamingResponse: + def __init__(self, policy_bundle: PolicyBundleResource) -> None: + self._policy_bundle = policy_bundle + + self.retrieve = to_custom_streamed_response_wrapper( + policy_bundle.retrieve, + StreamedBinaryAPIResponse, + ) + self.update = to_custom_streamed_response_wrapper( + policy_bundle.update, + StreamedBinaryAPIResponse, + ) + self.reset = to_streamed_response_wrapper( + policy_bundle.reset, + ) + + +class AsyncPolicyBundleResourceWithStreamingResponse: + def __init__(self, policy_bundle: AsyncPolicyBundleResource) -> None: + self._policy_bundle = policy_bundle + + self.retrieve = async_to_custom_streamed_response_wrapper( + policy_bundle.retrieve, + AsyncStreamedBinaryAPIResponse, + ) + self.update = async_to_custom_streamed_response_wrapper( + policy_bundle.update, + AsyncStreamedBinaryAPIResponse, + ) + self.reset = async_to_streamed_response_wrapper( + policy_bundle.reset, + ) diff --git a/src/keycardai_api/resources/zones/__init__.py b/src/keycardai_api/resources/zones/__init__.py index d90aef1..e3ef8d7 100644 --- a/src/keycardai_api/resources/zones/__init__.py +++ b/src/keycardai_api/resources/zones/__init__.py @@ -1,5 +1,13 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. +from .roles import ( + RolesResource, + AsyncRolesResource, + RolesResourceWithRawResponse, + AsyncRolesResourceWithRawResponse, + RolesResourceWithStreamingResponse, + AsyncRolesResourceWithStreamingResponse, +) from .users import ( UsersResource, AsyncUsersResource, @@ -16,6 +24,14 @@ ZonesResourceWithStreamingResponse, AsyncZonesResourceWithStreamingResponse, ) +from .groups import ( + GroupsResource, + AsyncGroupsResource, + GroupsResourceWithRawResponse, + AsyncGroupsResourceWithRawResponse, + GroupsResourceWithStreamingResponse, + AsyncGroupsResourceWithStreamingResponse, +) from .members import ( MembersResource, AsyncMembersResource, @@ -168,6 +184,18 @@ "AsyncMembersResourceWithRawResponse", "MembersResourceWithStreamingResponse", "AsyncMembersResourceWithStreamingResponse", + "RolesResource", + "AsyncRolesResource", + "RolesResourceWithRawResponse", + "AsyncRolesResourceWithRawResponse", + "RolesResourceWithStreamingResponse", + "AsyncRolesResourceWithStreamingResponse", + "GroupsResource", + "AsyncGroupsResource", + "GroupsResourceWithRawResponse", + "AsyncGroupsResourceWithRawResponse", + "GroupsResourceWithStreamingResponse", + "AsyncGroupsResourceWithStreamingResponse", "SecretsResource", "AsyncSecretsResource", "SecretsResourceWithRawResponse", diff --git a/src/keycardai_api/resources/zones/applications/__init__.py b/src/keycardai_api/resources/zones/applications/__init__.py index 90081d4..c617814 100644 --- a/src/keycardai_api/resources/zones/applications/__init__.py +++ b/src/keycardai_api/resources/zones/applications/__init__.py @@ -1,5 +1,13 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. +from .roles import ( + RolesResource, + AsyncRolesResource, + RolesResourceWithRawResponse, + AsyncRolesResourceWithRawResponse, + RolesResourceWithStreamingResponse, + AsyncRolesResourceWithStreamingResponse, +) from .applications import ( ApplicationsResource, AsyncApplicationsResource, @@ -24,6 +32,12 @@ "AsyncDependenciesResourceWithRawResponse", "DependenciesResourceWithStreamingResponse", "AsyncDependenciesResourceWithStreamingResponse", + "RolesResource", + "AsyncRolesResource", + "RolesResourceWithRawResponse", + "AsyncRolesResourceWithRawResponse", + "RolesResourceWithStreamingResponse", + "AsyncRolesResourceWithStreamingResponse", "ApplicationsResource", "AsyncApplicationsResource", "ApplicationsResourceWithRawResponse", diff --git a/src/keycardai_api/resources/zones/applications/applications.py b/src/keycardai_api/resources/zones/applications/applications.py index 17d88d6..6181399 100644 --- a/src/keycardai_api/resources/zones/applications/applications.py +++ b/src/keycardai_api/resources/zones/applications/applications.py @@ -7,6 +7,14 @@ import httpx +from .roles import ( + RolesResource, + AsyncRolesResource, + RolesResourceWithRawResponse, + AsyncRolesResourceWithRawResponse, + RolesResourceWithStreamingResponse, + AsyncRolesResourceWithStreamingResponse, +) from ...._types import Body, Omit, Query, Headers, NoneType, NotGiven, SequenceNotStr, omit, not_given from ...._utils import path_template, maybe_transform, async_maybe_transform from ...._compat import cached_property @@ -35,7 +43,6 @@ from ...._base_client import make_request_options from ....types.zones.application import Application from ....types.zones.metadata_param import MetadataParam -from ....types.zones.application_trait import ApplicationTrait from ....types.zones.metadata_update_param import MetadataUpdateParam from ....types.zones.application_list_response import ApplicationListResponse from ....types.zones.application_list_resources_response import ApplicationListResourcesResponse @@ -49,6 +56,10 @@ class ApplicationsResource(SyncAPIResource): def dependencies(self) -> DependenciesResource: return DependenciesResource(self._client) + @cached_property + def roles(self) -> RolesResource: + return RolesResource(self._client) + @cached_property def with_raw_response(self) -> ApplicationsResourceWithRawResponse: """ @@ -260,8 +271,6 @@ def list( query_name: Union[str, SequenceNotStr[str]] | Omit = omit, slug: str | Omit = omit, sort: str | Omit = omit, - traits: List[ApplicationTrait] | Omit = omit, - traits_all: List[ApplicationTrait] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -306,12 +315,6 @@ def list( sort: Comma-separated sort fields. Prefix with - for descending. Allowed: created_at, name, identifier - traits: Filter by traits (OR matching - returns applications with any of the specified - traits) - - traits_all: Filter by traits (AND matching - returns applications with all of the specified - traits) - extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -344,8 +347,6 @@ def list( "query_name": query_name, "slug": slug, "sort": sort, - "traits": traits, - "traits_all": traits_all, }, application_list_params.ApplicationListParams, ), @@ -516,6 +517,10 @@ class AsyncApplicationsResource(AsyncAPIResource): def dependencies(self) -> AsyncDependenciesResource: return AsyncDependenciesResource(self._client) + @cached_property + def roles(self) -> AsyncRolesResource: + return AsyncRolesResource(self._client) + @cached_property def with_raw_response(self) -> AsyncApplicationsResourceWithRawResponse: """ @@ -727,8 +732,6 @@ async def list( query_name: Union[str, SequenceNotStr[str]] | Omit = omit, slug: str | Omit = omit, sort: str | Omit = omit, - traits: List[ApplicationTrait] | Omit = omit, - traits_all: List[ApplicationTrait] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -773,12 +776,6 @@ async def list( sort: Comma-separated sort fields. Prefix with - for descending. Allowed: created_at, name, identifier - traits: Filter by traits (OR matching - returns applications with any of the specified - traits) - - traits_all: Filter by traits (AND matching - returns applications with all of the specified - traits) - extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -811,8 +808,6 @@ async def list( "query_name": query_name, "slug": slug, "sort": sort, - "traits": traits, - "traits_all": traits_all, }, application_list_params.ApplicationListParams, ), @@ -1008,6 +1003,10 @@ def __init__(self, applications: ApplicationsResource) -> None: def dependencies(self) -> DependenciesResourceWithRawResponse: return DependenciesResourceWithRawResponse(self._applications.dependencies) + @cached_property + def roles(self) -> RolesResourceWithRawResponse: + return RolesResourceWithRawResponse(self._applications.roles) + class AsyncApplicationsResourceWithRawResponse: def __init__(self, applications: AsyncApplicationsResource) -> None: @@ -1039,6 +1038,10 @@ def __init__(self, applications: AsyncApplicationsResource) -> None: def dependencies(self) -> AsyncDependenciesResourceWithRawResponse: return AsyncDependenciesResourceWithRawResponse(self._applications.dependencies) + @cached_property + def roles(self) -> AsyncRolesResourceWithRawResponse: + return AsyncRolesResourceWithRawResponse(self._applications.roles) + class ApplicationsResourceWithStreamingResponse: def __init__(self, applications: ApplicationsResource) -> None: @@ -1070,6 +1073,10 @@ def __init__(self, applications: ApplicationsResource) -> None: def dependencies(self) -> DependenciesResourceWithStreamingResponse: return DependenciesResourceWithStreamingResponse(self._applications.dependencies) + @cached_property + def roles(self) -> RolesResourceWithStreamingResponse: + return RolesResourceWithStreamingResponse(self._applications.roles) + class AsyncApplicationsResourceWithStreamingResponse: def __init__(self, applications: AsyncApplicationsResource) -> None: @@ -1100,3 +1107,7 @@ def __init__(self, applications: AsyncApplicationsResource) -> None: @cached_property def dependencies(self) -> AsyncDependenciesResourceWithStreamingResponse: return AsyncDependenciesResourceWithStreamingResponse(self._applications.dependencies) + + @cached_property + def roles(self) -> AsyncRolesResourceWithStreamingResponse: + return AsyncRolesResourceWithStreamingResponse(self._applications.roles) diff --git a/src/keycardai_api/resources/zones/applications/roles.py b/src/keycardai_api/resources/zones/applications/roles.py new file mode 100644 index 0000000..7b6935f --- /dev/null +++ b/src/keycardai_api/resources/zones/applications/roles.py @@ -0,0 +1,537 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List, Union +from typing_extensions import Literal + +import httpx + +from ...._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given +from ...._utils import path_template, maybe_transform, async_maybe_transform +from ...._compat import cached_property +from ...._resource import SyncAPIResource, AsyncAPIResource +from ...._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ...._base_client import make_request_options +from ....types.zones.applications import role_list_params, role_assign_params, role_revoke_params +from ....types.zones.users.role_assignment import RoleAssignment +from ....types.zones.applications.role_list_response import RoleListResponse + +__all__ = ["RolesResource", "AsyncRolesResource"] + + +class RolesResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> RolesResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/keycardai/keycard-python#accessing-raw-response-data-eg-headers + """ + return RolesResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> RolesResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/keycardai/keycard-python#with_streaming_response + """ + return RolesResourceWithStreamingResponse(self) + + def list( + self, + application_id: str, + *, + zone_id: str, + after: str | Omit = omit, + before: str | Omit = omit, + expand: Union[Literal["total_count"], List[Literal["total_count"]]] | Omit = omit, + limit: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> RoleListResponse: + """Returns the roles assigned to the specified application within the zone. + + The + full result set is currently returned in a single page; the + `after`/`before`/`limit` cursor parameters are reserved and not yet enforced, + and `pagination` cursors are always null. + + Args: + after: Cursor for forward pagination + + before: Cursor for backward pagination + + limit: Maximum number of items to return + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not zone_id: + raise ValueError(f"Expected a non-empty value for `zone_id` but received {zone_id!r}") + if not application_id: + raise ValueError(f"Expected a non-empty value for `application_id` but received {application_id!r}") + return self._get( + path_template( + "/zones/{zone_id}/applications/{application_id}/roles", zone_id=zone_id, application_id=application_id + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "after": after, + "before": before, + "expand": expand, + "limit": limit, + }, + role_list_params.RoleListParams, + ), + ), + cast_to=RoleListResponse, + ) + + def assign( + self, + application_id: str, + *, + zone_id: str, + owner_type: Literal["platform", "customer"] | Omit = omit, + role_id: str | Omit = omit, + role_identifier: str | Omit = omit, + scope_id: str | Omit = omit, + scope_type: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> RoleAssignment: + """Assigns a role to the application. + + Provide exactly one of role_id or + role_identifier; when role_identifier is used, owner_type is required to + disambiguate roles that share an identifier across owner types (and must be + omitted with role_id). An optional (scope_type, scope_id) pair scopes the grant; + only platform roles on the org zone may carry a scope, and a `zone` scope must + reference a different zone in the same organization. + + Args: + owner_type: Owner type of the role to assign. Required with role_identifier (an identifier + is unique only per owner type); must be omitted with role_id. + + role_id: ID of the role to assign. Provide exactly one of role_id or role_identifier; + owner_type must be omitted when role_id is used. + + role_identifier: Role identifier: a lowercase slug (letters and digits separated by single + hyphens or underscores), unique per owner type within a zone. Role identifiers + surface in policy evaluation, so the slug restriction keeps them unambiguous in + policy text. + + scope_id: The ID of the resource to scope the grant to. Provide together with scope_type, + or omit both for an unscoped assignment. When scope_type is `zone`, this must + reference a different zone in the same organization. + + scope_type: The kind of resource to scope the grant to (e.g. `zone`). Provide together with + scope_id, or omit both for an unscoped assignment (applies to the owning zone + itself). Only platform roles on the org zone may carry a scope. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not zone_id: + raise ValueError(f"Expected a non-empty value for `zone_id` but received {zone_id!r}") + if not application_id: + raise ValueError(f"Expected a non-empty value for `application_id` but received {application_id!r}") + return self._post( + path_template( + "/zones/{zone_id}/applications/{application_id}/roles", zone_id=zone_id, application_id=application_id + ), + body=maybe_transform( + { + "owner_type": owner_type, + "role_id": role_id, + "role_identifier": role_identifier, + "scope_id": scope_id, + "scope_type": scope_type, + }, + role_assign_params.RoleAssignParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=RoleAssignment, + ) + + def revoke( + self, + role_id: str, + *, + zone_id: str, + application_id: str, + scope_id: str | Omit = omit, + scope_type: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> None: + """Revokes a role from the application. + + Provide the same (scope_type, scope_id) + pair the grant was created with, or omit both to revoke the unscoped grant. + + Args: + scope_id: Scope target of the grant to revoke. Provide together with scope_type. + + scope_type: Scope kind of the grant to revoke. Provide together with scope_id. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not zone_id: + raise ValueError(f"Expected a non-empty value for `zone_id` but received {zone_id!r}") + if not application_id: + raise ValueError(f"Expected a non-empty value for `application_id` but received {application_id!r}") + if not role_id: + raise ValueError(f"Expected a non-empty value for `role_id` but received {role_id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._delete( + path_template( + "/zones/{zone_id}/applications/{application_id}/roles/{role_id}", + zone_id=zone_id, + application_id=application_id, + role_id=role_id, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "scope_id": scope_id, + "scope_type": scope_type, + }, + role_revoke_params.RoleRevokeParams, + ), + ), + cast_to=NoneType, + ) + + +class AsyncRolesResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncRolesResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/keycardai/keycard-python#accessing-raw-response-data-eg-headers + """ + return AsyncRolesResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncRolesResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/keycardai/keycard-python#with_streaming_response + """ + return AsyncRolesResourceWithStreamingResponse(self) + + async def list( + self, + application_id: str, + *, + zone_id: str, + after: str | Omit = omit, + before: str | Omit = omit, + expand: Union[Literal["total_count"], List[Literal["total_count"]]] | Omit = omit, + limit: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> RoleListResponse: + """Returns the roles assigned to the specified application within the zone. + + The + full result set is currently returned in a single page; the + `after`/`before`/`limit` cursor parameters are reserved and not yet enforced, + and `pagination` cursors are always null. + + Args: + after: Cursor for forward pagination + + before: Cursor for backward pagination + + limit: Maximum number of items to return + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not zone_id: + raise ValueError(f"Expected a non-empty value for `zone_id` but received {zone_id!r}") + if not application_id: + raise ValueError(f"Expected a non-empty value for `application_id` but received {application_id!r}") + return await self._get( + path_template( + "/zones/{zone_id}/applications/{application_id}/roles", zone_id=zone_id, application_id=application_id + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform( + { + "after": after, + "before": before, + "expand": expand, + "limit": limit, + }, + role_list_params.RoleListParams, + ), + ), + cast_to=RoleListResponse, + ) + + async def assign( + self, + application_id: str, + *, + zone_id: str, + owner_type: Literal["platform", "customer"] | Omit = omit, + role_id: str | Omit = omit, + role_identifier: str | Omit = omit, + scope_id: str | Omit = omit, + scope_type: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> RoleAssignment: + """Assigns a role to the application. + + Provide exactly one of role_id or + role_identifier; when role_identifier is used, owner_type is required to + disambiguate roles that share an identifier across owner types (and must be + omitted with role_id). An optional (scope_type, scope_id) pair scopes the grant; + only platform roles on the org zone may carry a scope, and a `zone` scope must + reference a different zone in the same organization. + + Args: + owner_type: Owner type of the role to assign. Required with role_identifier (an identifier + is unique only per owner type); must be omitted with role_id. + + role_id: ID of the role to assign. Provide exactly one of role_id or role_identifier; + owner_type must be omitted when role_id is used. + + role_identifier: Role identifier: a lowercase slug (letters and digits separated by single + hyphens or underscores), unique per owner type within a zone. Role identifiers + surface in policy evaluation, so the slug restriction keeps them unambiguous in + policy text. + + scope_id: The ID of the resource to scope the grant to. Provide together with scope_type, + or omit both for an unscoped assignment. When scope_type is `zone`, this must + reference a different zone in the same organization. + + scope_type: The kind of resource to scope the grant to (e.g. `zone`). Provide together with + scope_id, or omit both for an unscoped assignment (applies to the owning zone + itself). Only platform roles on the org zone may carry a scope. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not zone_id: + raise ValueError(f"Expected a non-empty value for `zone_id` but received {zone_id!r}") + if not application_id: + raise ValueError(f"Expected a non-empty value for `application_id` but received {application_id!r}") + return await self._post( + path_template( + "/zones/{zone_id}/applications/{application_id}/roles", zone_id=zone_id, application_id=application_id + ), + body=await async_maybe_transform( + { + "owner_type": owner_type, + "role_id": role_id, + "role_identifier": role_identifier, + "scope_id": scope_id, + "scope_type": scope_type, + }, + role_assign_params.RoleAssignParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=RoleAssignment, + ) + + async def revoke( + self, + role_id: str, + *, + zone_id: str, + application_id: str, + scope_id: str | Omit = omit, + scope_type: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> None: + """Revokes a role from the application. + + Provide the same (scope_type, scope_id) + pair the grant was created with, or omit both to revoke the unscoped grant. + + Args: + scope_id: Scope target of the grant to revoke. Provide together with scope_type. + + scope_type: Scope kind of the grant to revoke. Provide together with scope_id. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not zone_id: + raise ValueError(f"Expected a non-empty value for `zone_id` but received {zone_id!r}") + if not application_id: + raise ValueError(f"Expected a non-empty value for `application_id` but received {application_id!r}") + if not role_id: + raise ValueError(f"Expected a non-empty value for `role_id` but received {role_id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return await self._delete( + path_template( + "/zones/{zone_id}/applications/{application_id}/roles/{role_id}", + zone_id=zone_id, + application_id=application_id, + role_id=role_id, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform( + { + "scope_id": scope_id, + "scope_type": scope_type, + }, + role_revoke_params.RoleRevokeParams, + ), + ), + cast_to=NoneType, + ) + + +class RolesResourceWithRawResponse: + def __init__(self, roles: RolesResource) -> None: + self._roles = roles + + self.list = to_raw_response_wrapper( + roles.list, + ) + self.assign = to_raw_response_wrapper( + roles.assign, + ) + self.revoke = to_raw_response_wrapper( + roles.revoke, + ) + + +class AsyncRolesResourceWithRawResponse: + def __init__(self, roles: AsyncRolesResource) -> None: + self._roles = roles + + self.list = async_to_raw_response_wrapper( + roles.list, + ) + self.assign = async_to_raw_response_wrapper( + roles.assign, + ) + self.revoke = async_to_raw_response_wrapper( + roles.revoke, + ) + + +class RolesResourceWithStreamingResponse: + def __init__(self, roles: RolesResource) -> None: + self._roles = roles + + self.list = to_streamed_response_wrapper( + roles.list, + ) + self.assign = to_streamed_response_wrapper( + roles.assign, + ) + self.revoke = to_streamed_response_wrapper( + roles.revoke, + ) + + +class AsyncRolesResourceWithStreamingResponse: + def __init__(self, roles: AsyncRolesResource) -> None: + self._roles = roles + + self.list = async_to_streamed_response_wrapper( + roles.list, + ) + self.assign = async_to_streamed_response_wrapper( + roles.assign, + ) + self.revoke = async_to_streamed_response_wrapper( + roles.revoke, + ) diff --git a/src/keycardai_api/resources/zones/groups/__init__.py b/src/keycardai_api/resources/zones/groups/__init__.py new file mode 100644 index 0000000..95890d2 --- /dev/null +++ b/src/keycardai_api/resources/zones/groups/__init__.py @@ -0,0 +1,47 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .roles import ( + RolesResource, + AsyncRolesResource, + RolesResourceWithRawResponse, + AsyncRolesResourceWithRawResponse, + RolesResourceWithStreamingResponse, + AsyncRolesResourceWithStreamingResponse, +) +from .groups import ( + GroupsResource, + AsyncGroupsResource, + GroupsResourceWithRawResponse, + AsyncGroupsResourceWithRawResponse, + GroupsResourceWithStreamingResponse, + AsyncGroupsResourceWithStreamingResponse, +) +from .members import ( + MembersResource, + AsyncMembersResource, + MembersResourceWithRawResponse, + AsyncMembersResourceWithRawResponse, + MembersResourceWithStreamingResponse, + AsyncMembersResourceWithStreamingResponse, +) + +__all__ = [ + "MembersResource", + "AsyncMembersResource", + "MembersResourceWithRawResponse", + "AsyncMembersResourceWithRawResponse", + "MembersResourceWithStreamingResponse", + "AsyncMembersResourceWithStreamingResponse", + "RolesResource", + "AsyncRolesResource", + "RolesResourceWithRawResponse", + "AsyncRolesResourceWithRawResponse", + "RolesResourceWithStreamingResponse", + "AsyncRolesResourceWithStreamingResponse", + "GroupsResource", + "AsyncGroupsResource", + "GroupsResourceWithRawResponse", + "AsyncGroupsResourceWithRawResponse", + "GroupsResourceWithStreamingResponse", + "AsyncGroupsResourceWithStreamingResponse", +] diff --git a/src/keycardai_api/resources/zones/groups/groups.py b/src/keycardai_api/resources/zones/groups/groups.py new file mode 100644 index 0000000..7046794 --- /dev/null +++ b/src/keycardai_api/resources/zones/groups/groups.py @@ -0,0 +1,761 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List, Union +from typing_extensions import Literal + +import httpx + +from .roles import ( + RolesResource, + AsyncRolesResource, + RolesResourceWithRawResponse, + AsyncRolesResourceWithRawResponse, + RolesResourceWithStreamingResponse, + AsyncRolesResourceWithStreamingResponse, +) +from .members import ( + MembersResource, + AsyncMembersResource, + MembersResourceWithRawResponse, + AsyncMembersResourceWithRawResponse, + MembersResourceWithStreamingResponse, + AsyncMembersResourceWithStreamingResponse, +) +from ...._types import Body, Omit, Query, Headers, NoneType, NotGiven, SequenceNotStr, omit, not_given +from ...._utils import path_template, maybe_transform, async_maybe_transform +from ...._compat import cached_property +from ...._resource import SyncAPIResource, AsyncAPIResource +from ...._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ....types.zones import group_list_params, group_create_params, group_update_params, group_retrieve_params +from ...._base_client import make_request_options +from ....types.zones.group import Group +from ....types.zones.group_list_response import GroupListResponse + +__all__ = ["GroupsResource", "AsyncGroupsResource"] + + +class GroupsResource(SyncAPIResource): + @cached_property + def members(self) -> MembersResource: + return MembersResource(self._client) + + @cached_property + def roles(self) -> RolesResource: + return RolesResource(self._client) + + @cached_property + def with_raw_response(self) -> GroupsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/keycardai/keycard-python#accessing-raw-response-data-eg-headers + """ + return GroupsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> GroupsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/keycardai/keycard-python#with_streaming_response + """ + return GroupsResourceWithStreamingResponse(self) + + def create( + self, + zone_id: str, + *, + name: str, + identifier: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> Group: + """Creates a group in the zone (managed in Keycard). + + Groups synced from an external + directory are created by that directory, not here. + + Args: + name: Human-readable group name + + identifier: User-specified identifier, unique within the zone. Derived from the name when + omitted (a suffix is appended if it collides). + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not zone_id: + raise ValueError(f"Expected a non-empty value for `zone_id` but received {zone_id!r}") + return self._post( + path_template("/zones/{zone_id}/groups", zone_id=zone_id), + body=maybe_transform( + { + "name": name, + "identifier": identifier, + }, + group_create_params.GroupCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Group, + ) + + def retrieve( + self, + group_id: str, + *, + zone_id: str, + expand: Union[Literal["member_count", "roles"], List[Literal["member_count", "roles"]]] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> Group: + """Returns a group by ID. + + Pass `expand[]=member_count` for its member count and + `expand[]=roles` for the identifiers of its assigned roles. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not zone_id: + raise ValueError(f"Expected a non-empty value for `zone_id` but received {zone_id!r}") + if not group_id: + raise ValueError(f"Expected a non-empty value for `group_id` but received {group_id!r}") + return self._get( + path_template("/zones/{zone_id}/groups/{group_id}", zone_id=zone_id, group_id=group_id), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform({"expand": expand}, group_retrieve_params.GroupRetrieveParams), + ), + cast_to=Group, + ) + + def update( + self, + group_id: str, + *, + zone_id: str, + identifier: str | Omit = omit, + name: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> Group: + """Updates a group's name and/or identifier (partial update). + + A group's source is + immutable. The name of a group synced from an external directory cannot be + changed while external sync is enabled for the zone; its identifier can. + + Args: + identifier: User-specified identifier, unique within the zone. + + name: Human-readable group name + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not zone_id: + raise ValueError(f"Expected a non-empty value for `zone_id` but received {zone_id!r}") + if not group_id: + raise ValueError(f"Expected a non-empty value for `group_id` but received {group_id!r}") + return self._patch( + path_template("/zones/{zone_id}/groups/{group_id}", zone_id=zone_id, group_id=group_id), + body=maybe_transform( + { + "identifier": identifier, + "name": name, + }, + group_update_params.GroupUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Group, + ) + + def list( + self, + zone_id: str, + *, + after: str | Omit = omit, + before: str | Omit = omit, + expand: Union[ + Literal["total_count", "member_count", "roles"], List[Literal["total_count", "member_count", "roles"]] + ] + | Omit = omit, + filter_id: Union[str, SequenceNotStr[str]] | Omit = omit, + filter_identifier: Union[str, SequenceNotStr[str]] | Omit = omit, + limit: int | Omit = omit, + query: Union[str, SequenceNotStr[str]] | Omit = omit, + sort: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> GroupListResponse: + """Returns a paginated list of the groups in the specified zone. + + Use cursor + pagination via `after`/`before`. Sort: comma-separated field list; prefix with + `-` for descending (allowed: created_at, name, identifier). Pass + `expand[]=member_count` to include each group's member count, `expand[]=roles` + to include the identifiers of the roles assigned to each group, and + `expand[]=total_count` to include the matching row count. Filter by exact + identifier via `filter[identifier]` (repeatable, OR'd across values). Search via + `query[]` (case-insensitive substring match, OR'd across repeated values); it + matches the group's name and identifier. Pass `filter[id]` (repeatable, max 100) + to restrict results to a known set of groups — mutually exclusive with + `after`/`before` (returns 400 if combined). When `filter[id]` is set, `limit` is + ignored and the response contains every requested group that exists in the zone, + in a single page. IDs not in the zone are silently omitted. + + Args: + after: Cursor for forward pagination + + before: Cursor for backward pagination + + filter_id: Restrict results to groups with this ID. Repeatable, max 100. Mutually exclusive + with after/before. + + filter_identifier: Filter by exact group identifier + + limit: Maximum number of items to return + + query: Search across name and identifier (substring match) + + sort: Comma-separated sort fields. Prefix with - for descending. Allowed: created_at, + name, identifier + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not zone_id: + raise ValueError(f"Expected a non-empty value for `zone_id` but received {zone_id!r}") + return self._get( + path_template("/zones/{zone_id}/groups", zone_id=zone_id), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "after": after, + "before": before, + "expand": expand, + "filter_id": filter_id, + "filter_identifier": filter_identifier, + "limit": limit, + "query": query, + "sort": sort, + }, + group_list_params.GroupListParams, + ), + ), + cast_to=GroupListResponse, + ) + + def delete( + self, + group_id: str, + *, + zone_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> None: + """Deletes a group and its memberships and role assignments. + + Groups synced from an + external directory can only be deleted by that directory (after external sync is + disabled). + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not zone_id: + raise ValueError(f"Expected a non-empty value for `zone_id` but received {zone_id!r}") + if not group_id: + raise ValueError(f"Expected a non-empty value for `group_id` but received {group_id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._delete( + path_template("/zones/{zone_id}/groups/{group_id}", zone_id=zone_id, group_id=group_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + +class AsyncGroupsResource(AsyncAPIResource): + @cached_property + def members(self) -> AsyncMembersResource: + return AsyncMembersResource(self._client) + + @cached_property + def roles(self) -> AsyncRolesResource: + return AsyncRolesResource(self._client) + + @cached_property + def with_raw_response(self) -> AsyncGroupsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/keycardai/keycard-python#accessing-raw-response-data-eg-headers + """ + return AsyncGroupsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncGroupsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/keycardai/keycard-python#with_streaming_response + """ + return AsyncGroupsResourceWithStreamingResponse(self) + + async def create( + self, + zone_id: str, + *, + name: str, + identifier: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> Group: + """Creates a group in the zone (managed in Keycard). + + Groups synced from an external + directory are created by that directory, not here. + + Args: + name: Human-readable group name + + identifier: User-specified identifier, unique within the zone. Derived from the name when + omitted (a suffix is appended if it collides). + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not zone_id: + raise ValueError(f"Expected a non-empty value for `zone_id` but received {zone_id!r}") + return await self._post( + path_template("/zones/{zone_id}/groups", zone_id=zone_id), + body=await async_maybe_transform( + { + "name": name, + "identifier": identifier, + }, + group_create_params.GroupCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Group, + ) + + async def retrieve( + self, + group_id: str, + *, + zone_id: str, + expand: Union[Literal["member_count", "roles"], List[Literal["member_count", "roles"]]] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> Group: + """Returns a group by ID. + + Pass `expand[]=member_count` for its member count and + `expand[]=roles` for the identifiers of its assigned roles. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not zone_id: + raise ValueError(f"Expected a non-empty value for `zone_id` but received {zone_id!r}") + if not group_id: + raise ValueError(f"Expected a non-empty value for `group_id` but received {group_id!r}") + return await self._get( + path_template("/zones/{zone_id}/groups/{group_id}", zone_id=zone_id, group_id=group_id), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform({"expand": expand}, group_retrieve_params.GroupRetrieveParams), + ), + cast_to=Group, + ) + + async def update( + self, + group_id: str, + *, + zone_id: str, + identifier: str | Omit = omit, + name: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> Group: + """Updates a group's name and/or identifier (partial update). + + A group's source is + immutable. The name of a group synced from an external directory cannot be + changed while external sync is enabled for the zone; its identifier can. + + Args: + identifier: User-specified identifier, unique within the zone. + + name: Human-readable group name + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not zone_id: + raise ValueError(f"Expected a non-empty value for `zone_id` but received {zone_id!r}") + if not group_id: + raise ValueError(f"Expected a non-empty value for `group_id` but received {group_id!r}") + return await self._patch( + path_template("/zones/{zone_id}/groups/{group_id}", zone_id=zone_id, group_id=group_id), + body=await async_maybe_transform( + { + "identifier": identifier, + "name": name, + }, + group_update_params.GroupUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Group, + ) + + async def list( + self, + zone_id: str, + *, + after: str | Omit = omit, + before: str | Omit = omit, + expand: Union[ + Literal["total_count", "member_count", "roles"], List[Literal["total_count", "member_count", "roles"]] + ] + | Omit = omit, + filter_id: Union[str, SequenceNotStr[str]] | Omit = omit, + filter_identifier: Union[str, SequenceNotStr[str]] | Omit = omit, + limit: int | Omit = omit, + query: Union[str, SequenceNotStr[str]] | Omit = omit, + sort: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> GroupListResponse: + """Returns a paginated list of the groups in the specified zone. + + Use cursor + pagination via `after`/`before`. Sort: comma-separated field list; prefix with + `-` for descending (allowed: created_at, name, identifier). Pass + `expand[]=member_count` to include each group's member count, `expand[]=roles` + to include the identifiers of the roles assigned to each group, and + `expand[]=total_count` to include the matching row count. Filter by exact + identifier via `filter[identifier]` (repeatable, OR'd across values). Search via + `query[]` (case-insensitive substring match, OR'd across repeated values); it + matches the group's name and identifier. Pass `filter[id]` (repeatable, max 100) + to restrict results to a known set of groups — mutually exclusive with + `after`/`before` (returns 400 if combined). When `filter[id]` is set, `limit` is + ignored and the response contains every requested group that exists in the zone, + in a single page. IDs not in the zone are silently omitted. + + Args: + after: Cursor for forward pagination + + before: Cursor for backward pagination + + filter_id: Restrict results to groups with this ID. Repeatable, max 100. Mutually exclusive + with after/before. + + filter_identifier: Filter by exact group identifier + + limit: Maximum number of items to return + + query: Search across name and identifier (substring match) + + sort: Comma-separated sort fields. Prefix with - for descending. Allowed: created_at, + name, identifier + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not zone_id: + raise ValueError(f"Expected a non-empty value for `zone_id` but received {zone_id!r}") + return await self._get( + path_template("/zones/{zone_id}/groups", zone_id=zone_id), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform( + { + "after": after, + "before": before, + "expand": expand, + "filter_id": filter_id, + "filter_identifier": filter_identifier, + "limit": limit, + "query": query, + "sort": sort, + }, + group_list_params.GroupListParams, + ), + ), + cast_to=GroupListResponse, + ) + + async def delete( + self, + group_id: str, + *, + zone_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> None: + """Deletes a group and its memberships and role assignments. + + Groups synced from an + external directory can only be deleted by that directory (after external sync is + disabled). + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not zone_id: + raise ValueError(f"Expected a non-empty value for `zone_id` but received {zone_id!r}") + if not group_id: + raise ValueError(f"Expected a non-empty value for `group_id` but received {group_id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return await self._delete( + path_template("/zones/{zone_id}/groups/{group_id}", zone_id=zone_id, group_id=group_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + +class GroupsResourceWithRawResponse: + def __init__(self, groups: GroupsResource) -> None: + self._groups = groups + + self.create = to_raw_response_wrapper( + groups.create, + ) + self.retrieve = to_raw_response_wrapper( + groups.retrieve, + ) + self.update = to_raw_response_wrapper( + groups.update, + ) + self.list = to_raw_response_wrapper( + groups.list, + ) + self.delete = to_raw_response_wrapper( + groups.delete, + ) + + @cached_property + def members(self) -> MembersResourceWithRawResponse: + return MembersResourceWithRawResponse(self._groups.members) + + @cached_property + def roles(self) -> RolesResourceWithRawResponse: + return RolesResourceWithRawResponse(self._groups.roles) + + +class AsyncGroupsResourceWithRawResponse: + def __init__(self, groups: AsyncGroupsResource) -> None: + self._groups = groups + + self.create = async_to_raw_response_wrapper( + groups.create, + ) + self.retrieve = async_to_raw_response_wrapper( + groups.retrieve, + ) + self.update = async_to_raw_response_wrapper( + groups.update, + ) + self.list = async_to_raw_response_wrapper( + groups.list, + ) + self.delete = async_to_raw_response_wrapper( + groups.delete, + ) + + @cached_property + def members(self) -> AsyncMembersResourceWithRawResponse: + return AsyncMembersResourceWithRawResponse(self._groups.members) + + @cached_property + def roles(self) -> AsyncRolesResourceWithRawResponse: + return AsyncRolesResourceWithRawResponse(self._groups.roles) + + +class GroupsResourceWithStreamingResponse: + def __init__(self, groups: GroupsResource) -> None: + self._groups = groups + + self.create = to_streamed_response_wrapper( + groups.create, + ) + self.retrieve = to_streamed_response_wrapper( + groups.retrieve, + ) + self.update = to_streamed_response_wrapper( + groups.update, + ) + self.list = to_streamed_response_wrapper( + groups.list, + ) + self.delete = to_streamed_response_wrapper( + groups.delete, + ) + + @cached_property + def members(self) -> MembersResourceWithStreamingResponse: + return MembersResourceWithStreamingResponse(self._groups.members) + + @cached_property + def roles(self) -> RolesResourceWithStreamingResponse: + return RolesResourceWithStreamingResponse(self._groups.roles) + + +class AsyncGroupsResourceWithStreamingResponse: + def __init__(self, groups: AsyncGroupsResource) -> None: + self._groups = groups + + self.create = async_to_streamed_response_wrapper( + groups.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + groups.retrieve, + ) + self.update = async_to_streamed_response_wrapper( + groups.update, + ) + self.list = async_to_streamed_response_wrapper( + groups.list, + ) + self.delete = async_to_streamed_response_wrapper( + groups.delete, + ) + + @cached_property + def members(self) -> AsyncMembersResourceWithStreamingResponse: + return AsyncMembersResourceWithStreamingResponse(self._groups.members) + + @cached_property + def roles(self) -> AsyncRolesResourceWithStreamingResponse: + return AsyncRolesResourceWithStreamingResponse(self._groups.roles) diff --git a/src/keycardai_api/resources/zones/groups/members.py b/src/keycardai_api/resources/zones/groups/members.py new file mode 100644 index 0000000..3450e8c --- /dev/null +++ b/src/keycardai_api/resources/zones/groups/members.py @@ -0,0 +1,461 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List, Union +from typing_extensions import Literal + +import httpx + +from ...._types import Body, Omit, Query, Headers, NoneType, NotGiven, SequenceNotStr, omit, not_given +from ...._utils import path_template, maybe_transform, async_maybe_transform +from ...._compat import cached_property +from ...._resource import SyncAPIResource, AsyncAPIResource +from ...._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ...._base_client import make_request_options +from ....types.zones.groups import member_add_params, member_list_params +from ....types.zones.groups.group_member import GroupMember +from ....types.zones.groups.member_list_response import MemberListResponse + +__all__ = ["MembersResource", "AsyncMembersResource"] + + +class MembersResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> MembersResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/keycardai/keycard-python#accessing-raw-response-data-eg-headers + """ + return MembersResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> MembersResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/keycardai/keycard-python#with_streaming_response + """ + return MembersResourceWithStreamingResponse(self) + + def list( + self, + group_id: str, + *, + zone_id: str, + after: str | Omit = omit, + before: str | Omit = omit, + expand: Union[Literal["total_count", "user"], List[Literal["total_count", "user"]]] | Omit = omit, + filter_id: Union[str, SequenceNotStr[str]] | Omit = omit, + limit: int | Omit = omit, + query: Union[str, SequenceNotStr[str]] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> MemberListResponse: + """Returns a paginated list of the group's members. + + Use cursor pagination via + `after`/`before`. Pass `expand[]=user` to embed each member's full user record + and `expand[]=total_count` to include the matching row count. Pass `query[]` + (repeatable, 1-255 chars) to search members by their user's email or federated + credential subject (substring match, OR'd across repeated values). Pass + `filter[id]` (repeatable, max 100) to restrict results to a known set of members + by user ID — mutually exclusive with `after`/`before` (returns 400 if combined). + When `filter[id]` is set, `limit` is ignored and the response contains every + requested member that exists in the group, in a single page. IDs not in the + group are silently omitted. + + Args: + after: Cursor for forward pagination + + before: Cursor for backward pagination + + filter_id: Restrict results to the member with this user ID. Repeatable, max 100. Mutually + exclusive with after/before. + + limit: Maximum number of items to return + + query: Search members by their user's email or federated credential subject (substring + match) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not zone_id: + raise ValueError(f"Expected a non-empty value for `zone_id` but received {zone_id!r}") + if not group_id: + raise ValueError(f"Expected a non-empty value for `group_id` but received {group_id!r}") + return self._get( + path_template("/zones/{zone_id}/groups/{group_id}/members", zone_id=zone_id, group_id=group_id), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "after": after, + "before": before, + "expand": expand, + "filter_id": filter_id, + "limit": limit, + "query": query, + }, + member_list_params.MemberListParams, + ), + ), + cast_to=MemberListResponse, + ) + + def add( + self, + group_id: str, + *, + zone_id: str, + user_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> GroupMember: + """Adds a user to a group managed in Keycard. + + Membership of externally synced + groups is not managed manually. + + Args: + user_id: ID of the user to add to the group + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not zone_id: + raise ValueError(f"Expected a non-empty value for `zone_id` but received {zone_id!r}") + if not group_id: + raise ValueError(f"Expected a non-empty value for `group_id` but received {group_id!r}") + return self._post( + path_template("/zones/{zone_id}/groups/{group_id}/members", zone_id=zone_id, group_id=group_id), + body=maybe_transform({"user_id": user_id}, member_add_params.MemberAddParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=GroupMember, + ) + + def remove( + self, + user_id: str, + *, + zone_id: str, + group_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> None: + """Removes a user from a group managed in Keycard. + + Membership of externally synced + groups is not managed manually. A member is identified by its user's ID. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not zone_id: + raise ValueError(f"Expected a non-empty value for `zone_id` but received {zone_id!r}") + if not group_id: + raise ValueError(f"Expected a non-empty value for `group_id` but received {group_id!r}") + if not user_id: + raise ValueError(f"Expected a non-empty value for `user_id` but received {user_id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._delete( + path_template( + "/zones/{zone_id}/groups/{group_id}/members/{user_id}", + zone_id=zone_id, + group_id=group_id, + user_id=user_id, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + +class AsyncMembersResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncMembersResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/keycardai/keycard-python#accessing-raw-response-data-eg-headers + """ + return AsyncMembersResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncMembersResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/keycardai/keycard-python#with_streaming_response + """ + return AsyncMembersResourceWithStreamingResponse(self) + + async def list( + self, + group_id: str, + *, + zone_id: str, + after: str | Omit = omit, + before: str | Omit = omit, + expand: Union[Literal["total_count", "user"], List[Literal["total_count", "user"]]] | Omit = omit, + filter_id: Union[str, SequenceNotStr[str]] | Omit = omit, + limit: int | Omit = omit, + query: Union[str, SequenceNotStr[str]] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> MemberListResponse: + """Returns a paginated list of the group's members. + + Use cursor pagination via + `after`/`before`. Pass `expand[]=user` to embed each member's full user record + and `expand[]=total_count` to include the matching row count. Pass `query[]` + (repeatable, 1-255 chars) to search members by their user's email or federated + credential subject (substring match, OR'd across repeated values). Pass + `filter[id]` (repeatable, max 100) to restrict results to a known set of members + by user ID — mutually exclusive with `after`/`before` (returns 400 if combined). + When `filter[id]` is set, `limit` is ignored and the response contains every + requested member that exists in the group, in a single page. IDs not in the + group are silently omitted. + + Args: + after: Cursor for forward pagination + + before: Cursor for backward pagination + + filter_id: Restrict results to the member with this user ID. Repeatable, max 100. Mutually + exclusive with after/before. + + limit: Maximum number of items to return + + query: Search members by their user's email or federated credential subject (substring + match) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not zone_id: + raise ValueError(f"Expected a non-empty value for `zone_id` but received {zone_id!r}") + if not group_id: + raise ValueError(f"Expected a non-empty value for `group_id` but received {group_id!r}") + return await self._get( + path_template("/zones/{zone_id}/groups/{group_id}/members", zone_id=zone_id, group_id=group_id), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform( + { + "after": after, + "before": before, + "expand": expand, + "filter_id": filter_id, + "limit": limit, + "query": query, + }, + member_list_params.MemberListParams, + ), + ), + cast_to=MemberListResponse, + ) + + async def add( + self, + group_id: str, + *, + zone_id: str, + user_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> GroupMember: + """Adds a user to a group managed in Keycard. + + Membership of externally synced + groups is not managed manually. + + Args: + user_id: ID of the user to add to the group + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not zone_id: + raise ValueError(f"Expected a non-empty value for `zone_id` but received {zone_id!r}") + if not group_id: + raise ValueError(f"Expected a non-empty value for `group_id` but received {group_id!r}") + return await self._post( + path_template("/zones/{zone_id}/groups/{group_id}/members", zone_id=zone_id, group_id=group_id), + body=await async_maybe_transform({"user_id": user_id}, member_add_params.MemberAddParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=GroupMember, + ) + + async def remove( + self, + user_id: str, + *, + zone_id: str, + group_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> None: + """Removes a user from a group managed in Keycard. + + Membership of externally synced + groups is not managed manually. A member is identified by its user's ID. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not zone_id: + raise ValueError(f"Expected a non-empty value for `zone_id` but received {zone_id!r}") + if not group_id: + raise ValueError(f"Expected a non-empty value for `group_id` but received {group_id!r}") + if not user_id: + raise ValueError(f"Expected a non-empty value for `user_id` but received {user_id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return await self._delete( + path_template( + "/zones/{zone_id}/groups/{group_id}/members/{user_id}", + zone_id=zone_id, + group_id=group_id, + user_id=user_id, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + +class MembersResourceWithRawResponse: + def __init__(self, members: MembersResource) -> None: + self._members = members + + self.list = to_raw_response_wrapper( + members.list, + ) + self.add = to_raw_response_wrapper( + members.add, + ) + self.remove = to_raw_response_wrapper( + members.remove, + ) + + +class AsyncMembersResourceWithRawResponse: + def __init__(self, members: AsyncMembersResource) -> None: + self._members = members + + self.list = async_to_raw_response_wrapper( + members.list, + ) + self.add = async_to_raw_response_wrapper( + members.add, + ) + self.remove = async_to_raw_response_wrapper( + members.remove, + ) + + +class MembersResourceWithStreamingResponse: + def __init__(self, members: MembersResource) -> None: + self._members = members + + self.list = to_streamed_response_wrapper( + members.list, + ) + self.add = to_streamed_response_wrapper( + members.add, + ) + self.remove = to_streamed_response_wrapper( + members.remove, + ) + + +class AsyncMembersResourceWithStreamingResponse: + def __init__(self, members: AsyncMembersResource) -> None: + self._members = members + + self.list = async_to_streamed_response_wrapper( + members.list, + ) + self.add = async_to_streamed_response_wrapper( + members.add, + ) + self.remove = async_to_streamed_response_wrapper( + members.remove, + ) diff --git a/src/keycardai_api/resources/zones/groups/roles.py b/src/keycardai_api/resources/zones/groups/roles.py new file mode 100644 index 0000000..b2c4aab --- /dev/null +++ b/src/keycardai_api/resources/zones/groups/roles.py @@ -0,0 +1,541 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List, Union +from typing_extensions import Literal + +import httpx + +from ...._types import Body, Omit, Query, Headers, NoneType, NotGiven, SequenceNotStr, omit, not_given +from ...._utils import path_template, maybe_transform, async_maybe_transform +from ...._compat import cached_property +from ...._resource import SyncAPIResource, AsyncAPIResource +from ...._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ...._base_client import make_request_options +from ....types.zones.groups import role_add_params, role_list_params, role_remove_params +from ....types.zones.users.role_assignment import RoleAssignment +from ....types.zones.groups.role_list_response import RoleListResponse + +__all__ = ["RolesResource", "AsyncRolesResource"] + + +class RolesResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> RolesResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/keycardai/keycard-python#accessing-raw-response-data-eg-headers + """ + return RolesResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> RolesResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/keycardai/keycard-python#with_streaming_response + """ + return RolesResourceWithStreamingResponse(self) + + def list( + self, + group_id: str, + *, + zone_id: str, + after: str | Omit = omit, + before: str | Omit = omit, + expand: Union[Literal["total_count"], List[Literal["total_count"]]] | Omit = omit, + filter_id: Union[str, SequenceNotStr[str]] | Omit = omit, + limit: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> RoleListResponse: + """Returns the roles assigned to the group. + + Members inherit these roles. Returns + the shared role-assignment shape with `principal_type` set to `group`. Use + cursor pagination via `after`/`before`; pass `expand[]=total_count` to include + the matching row count. Pass `filter[id]` (repeatable, max 100) to restrict + results to a known set of role assignments, mutually exclusive with + `after`/`before` (returns 400 if combined). When `filter[id]` is set, `limit` is + ignored and the response contains every requested assignment that exists on the + group, in a single page. IDs not on the group are silently omitted. + + Args: + after: Cursor for forward pagination + + before: Cursor for backward pagination + + filter_id: Restrict results to the role assignment with this ID. Repeatable, max 100. + Mutually exclusive with after/before. + + limit: Maximum number of items to return + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not zone_id: + raise ValueError(f"Expected a non-empty value for `zone_id` but received {zone_id!r}") + if not group_id: + raise ValueError(f"Expected a non-empty value for `group_id` but received {group_id!r}") + return self._get( + path_template("/zones/{zone_id}/groups/{group_id}/roles", zone_id=zone_id, group_id=group_id), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "after": after, + "before": before, + "expand": expand, + "filter_id": filter_id, + "limit": limit, + }, + role_list_params.RoleListParams, + ), + ), + cast_to=RoleListResponse, + ) + + def add( + self, + group_id: str, + *, + zone_id: str, + owner_type: Literal["platform", "customer"] | Omit = omit, + role_id: str | Omit = omit, + role_identifier: str | Omit = omit, + scope_id: str | Omit = omit, + scope_type: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> RoleAssignment: + """Assigns a role to the group; members inherit it. + + Provide role_id, or + role_identifier with owner_type. Returns the shared role-assignment shape with + `principal_type` set to `group`. + + Args: + owner_type: Owner type of the role to assign. Required with role_identifier (an identifier + is unique only per owner type); must be omitted with role_id. + + role_id: ID of the role to assign. Provide exactly one of role_id or role_identifier; + owner_type must be omitted when role_id is used. + + role_identifier: Role identifier: a lowercase slug (letters and digits separated by single + hyphens or underscores), unique per owner type within a zone. Role identifiers + surface in policy evaluation, so the slug restriction keeps them unambiguous in + policy text. + + scope_id: The ID of the resource to scope the grant to. Provide together with scope_type, + or omit both for an unscoped assignment. When scope_type is `zone`, this must + reference a different zone in the same organization. + + scope_type: The kind of resource to scope the grant to (e.g. `zone`). Provide together with + scope_id, or omit both for an unscoped assignment (applies to the owning zone + itself). Only platform roles on the org zone may carry a scope. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not zone_id: + raise ValueError(f"Expected a non-empty value for `zone_id` but received {zone_id!r}") + if not group_id: + raise ValueError(f"Expected a non-empty value for `group_id` but received {group_id!r}") + return self._post( + path_template("/zones/{zone_id}/groups/{group_id}/roles", zone_id=zone_id, group_id=group_id), + body=maybe_transform( + { + "owner_type": owner_type, + "role_id": role_id, + "role_identifier": role_identifier, + "scope_id": scope_id, + "scope_type": scope_type, + }, + role_add_params.RoleAddParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=RoleAssignment, + ) + + def remove( + self, + role_id: str, + *, + zone_id: str, + group_id: str, + scope_id: str | Omit = omit, + scope_type: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> None: + """Revokes a role from the group. + + Provide the same (scope_type, scope_id) pair the + grant was created with, or omit both to revoke the unscoped grant. + + Args: + scope_id: Scope target of the grant to revoke. Provide together with scope_type. + + scope_type: Scope kind of the grant to revoke. Provide together with scope_id. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not zone_id: + raise ValueError(f"Expected a non-empty value for `zone_id` but received {zone_id!r}") + if not group_id: + raise ValueError(f"Expected a non-empty value for `group_id` but received {group_id!r}") + if not role_id: + raise ValueError(f"Expected a non-empty value for `role_id` but received {role_id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._delete( + path_template( + "/zones/{zone_id}/groups/{group_id}/roles/{role_id}", + zone_id=zone_id, + group_id=group_id, + role_id=role_id, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "scope_id": scope_id, + "scope_type": scope_type, + }, + role_remove_params.RoleRemoveParams, + ), + ), + cast_to=NoneType, + ) + + +class AsyncRolesResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncRolesResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/keycardai/keycard-python#accessing-raw-response-data-eg-headers + """ + return AsyncRolesResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncRolesResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/keycardai/keycard-python#with_streaming_response + """ + return AsyncRolesResourceWithStreamingResponse(self) + + async def list( + self, + group_id: str, + *, + zone_id: str, + after: str | Omit = omit, + before: str | Omit = omit, + expand: Union[Literal["total_count"], List[Literal["total_count"]]] | Omit = omit, + filter_id: Union[str, SequenceNotStr[str]] | Omit = omit, + limit: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> RoleListResponse: + """Returns the roles assigned to the group. + + Members inherit these roles. Returns + the shared role-assignment shape with `principal_type` set to `group`. Use + cursor pagination via `after`/`before`; pass `expand[]=total_count` to include + the matching row count. Pass `filter[id]` (repeatable, max 100) to restrict + results to a known set of role assignments, mutually exclusive with + `after`/`before` (returns 400 if combined). When `filter[id]` is set, `limit` is + ignored and the response contains every requested assignment that exists on the + group, in a single page. IDs not on the group are silently omitted. + + Args: + after: Cursor for forward pagination + + before: Cursor for backward pagination + + filter_id: Restrict results to the role assignment with this ID. Repeatable, max 100. + Mutually exclusive with after/before. + + limit: Maximum number of items to return + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not zone_id: + raise ValueError(f"Expected a non-empty value for `zone_id` but received {zone_id!r}") + if not group_id: + raise ValueError(f"Expected a non-empty value for `group_id` but received {group_id!r}") + return await self._get( + path_template("/zones/{zone_id}/groups/{group_id}/roles", zone_id=zone_id, group_id=group_id), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform( + { + "after": after, + "before": before, + "expand": expand, + "filter_id": filter_id, + "limit": limit, + }, + role_list_params.RoleListParams, + ), + ), + cast_to=RoleListResponse, + ) + + async def add( + self, + group_id: str, + *, + zone_id: str, + owner_type: Literal["platform", "customer"] | Omit = omit, + role_id: str | Omit = omit, + role_identifier: str | Omit = omit, + scope_id: str | Omit = omit, + scope_type: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> RoleAssignment: + """Assigns a role to the group; members inherit it. + + Provide role_id, or + role_identifier with owner_type. Returns the shared role-assignment shape with + `principal_type` set to `group`. + + Args: + owner_type: Owner type of the role to assign. Required with role_identifier (an identifier + is unique only per owner type); must be omitted with role_id. + + role_id: ID of the role to assign. Provide exactly one of role_id or role_identifier; + owner_type must be omitted when role_id is used. + + role_identifier: Role identifier: a lowercase slug (letters and digits separated by single + hyphens or underscores), unique per owner type within a zone. Role identifiers + surface in policy evaluation, so the slug restriction keeps them unambiguous in + policy text. + + scope_id: The ID of the resource to scope the grant to. Provide together with scope_type, + or omit both for an unscoped assignment. When scope_type is `zone`, this must + reference a different zone in the same organization. + + scope_type: The kind of resource to scope the grant to (e.g. `zone`). Provide together with + scope_id, or omit both for an unscoped assignment (applies to the owning zone + itself). Only platform roles on the org zone may carry a scope. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not zone_id: + raise ValueError(f"Expected a non-empty value for `zone_id` but received {zone_id!r}") + if not group_id: + raise ValueError(f"Expected a non-empty value for `group_id` but received {group_id!r}") + return await self._post( + path_template("/zones/{zone_id}/groups/{group_id}/roles", zone_id=zone_id, group_id=group_id), + body=await async_maybe_transform( + { + "owner_type": owner_type, + "role_id": role_id, + "role_identifier": role_identifier, + "scope_id": scope_id, + "scope_type": scope_type, + }, + role_add_params.RoleAddParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=RoleAssignment, + ) + + async def remove( + self, + role_id: str, + *, + zone_id: str, + group_id: str, + scope_id: str | Omit = omit, + scope_type: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> None: + """Revokes a role from the group. + + Provide the same (scope_type, scope_id) pair the + grant was created with, or omit both to revoke the unscoped grant. + + Args: + scope_id: Scope target of the grant to revoke. Provide together with scope_type. + + scope_type: Scope kind of the grant to revoke. Provide together with scope_id. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not zone_id: + raise ValueError(f"Expected a non-empty value for `zone_id` but received {zone_id!r}") + if not group_id: + raise ValueError(f"Expected a non-empty value for `group_id` but received {group_id!r}") + if not role_id: + raise ValueError(f"Expected a non-empty value for `role_id` but received {role_id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return await self._delete( + path_template( + "/zones/{zone_id}/groups/{group_id}/roles/{role_id}", + zone_id=zone_id, + group_id=group_id, + role_id=role_id, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform( + { + "scope_id": scope_id, + "scope_type": scope_type, + }, + role_remove_params.RoleRemoveParams, + ), + ), + cast_to=NoneType, + ) + + +class RolesResourceWithRawResponse: + def __init__(self, roles: RolesResource) -> None: + self._roles = roles + + self.list = to_raw_response_wrapper( + roles.list, + ) + self.add = to_raw_response_wrapper( + roles.add, + ) + self.remove = to_raw_response_wrapper( + roles.remove, + ) + + +class AsyncRolesResourceWithRawResponse: + def __init__(self, roles: AsyncRolesResource) -> None: + self._roles = roles + + self.list = async_to_raw_response_wrapper( + roles.list, + ) + self.add = async_to_raw_response_wrapper( + roles.add, + ) + self.remove = async_to_raw_response_wrapper( + roles.remove, + ) + + +class RolesResourceWithStreamingResponse: + def __init__(self, roles: RolesResource) -> None: + self._roles = roles + + self.list = to_streamed_response_wrapper( + roles.list, + ) + self.add = to_streamed_response_wrapper( + roles.add, + ) + self.remove = to_streamed_response_wrapper( + roles.remove, + ) + + +class AsyncRolesResourceWithStreamingResponse: + def __init__(self, roles: AsyncRolesResource) -> None: + self._roles = roles + + self.list = async_to_streamed_response_wrapper( + roles.list, + ) + self.add = async_to_streamed_response_wrapper( + roles.add, + ) + self.remove = async_to_streamed_response_wrapper( + roles.remove, + ) diff --git a/src/keycardai_api/resources/zones/policy_schemas.py b/src/keycardai_api/resources/zones/policy_schemas.py index eb5b9fd..a5a4f5b 100644 --- a/src/keycardai_api/resources/zones/policy_schemas.py +++ b/src/keycardai_api/resources/zones/policy_schemas.py @@ -36,7 +36,7 @@ class PolicySchemasResource(SyncAPIResource): - **Keycard::RegistrationMethod** — enum entity: `"managed"`, `"dcr"` - **Keycard::CredentialType** — enum entity: `"token"`, `"password"`, `"public-key"`, `"url"`, `"public"` - **Keycard::Resource** — `id` (String), `name` (String), `scopes` (Set of String) - - **Keycard::Claims** — `email` (String), `groups` (Set of String), plus arbitrary additional fields + - **Keycard::Claims** — `email` (String), `groups` (Set of String), `issuer_claims` (issuer-specific claims record), plus arbitrary additional fields Enum-like attributes use Cedar enum entity types (schema version `2026-03-16`+). In policies, reference values as `RegistrationMethod::"managed"` or `CredentialType::"token"`. @@ -284,7 +284,7 @@ class AsyncPolicySchemasResource(AsyncAPIResource): - **Keycard::RegistrationMethod** — enum entity: `"managed"`, `"dcr"` - **Keycard::CredentialType** — enum entity: `"token"`, `"password"`, `"public-key"`, `"url"`, `"public"` - **Keycard::Resource** — `id` (String), `name` (String), `scopes` (Set of String) - - **Keycard::Claims** — `email` (String), `groups` (Set of String), plus arbitrary additional fields + - **Keycard::Claims** — `email` (String), `groups` (Set of String), `issuer_claims` (issuer-specific claims record), plus arbitrary additional fields Enum-like attributes use Cedar enum entity types (schema version `2026-03-16`+). In policies, reference values as `RegistrationMethod::"managed"` or `CredentialType::"token"`. diff --git a/src/keycardai_api/resources/zones/providers.py b/src/keycardai_api/resources/zones/providers.py index d4ed74f..d558692 100644 --- a/src/keycardai_api/resources/zones/providers.py +++ b/src/keycardai_api/resources/zones/providers.py @@ -7,7 +7,7 @@ import httpx -from ..._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given +from ..._types import Body, Omit, Query, Headers, NoneType, NotGiven, SequenceNotStr, omit, not_given from ..._utils import path_template, maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource @@ -20,6 +20,7 @@ from ...types.zones import provider_list_params, provider_create_params, provider_update_params from ..._base_client import make_request_options from ...types.zones.provider import Provider +from ...types.zones.validation_result import ValidationResult from ...types.zones.provider_list_response import ProviderListResponse __all__ = ["ProvidersResource", "AsyncProvidersResource"] @@ -54,7 +55,7 @@ def create( client_id: str | Omit = omit, client_secret: str | Omit = omit, description: Optional[str] | Omit = omit, - metadata: object | Omit = omit, + metadata: provider_create_params.Metadata | Omit = omit, protocols: provider_create_params.Protocols | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -160,7 +161,7 @@ def update( client_secret: Optional[str] | Omit = omit, description: Optional[str] | Omit = omit, identifier: str | Omit = omit, - metadata: Optional[object] | Omit = omit, + metadata: Optional[provider_update_params.Metadata] | Omit = omit, name: str | Omit = omit, protocols: Optional[provider_update_params.Protocols] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -232,6 +233,7 @@ def list( before: str | Omit = omit, cursor: str | Omit = omit, expand: Union[Literal["total_count"], List[Literal["total_count"]]] | Omit = omit, + filter_id: Union[str, SequenceNotStr[str]] | Omit = omit, identifier: str | Omit = omit, limit: int | Omit = omit, slug: str | Omit = omit, @@ -243,14 +245,19 @@ def list( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ProviderListResponse: - """ - Returns a list of providers in the specified zone + """Returns a list of providers in the specified zone. + + Pass `filter[id]` + (repeatable, max 100) to restrict results to a known set of provider IDs; + unknown or malformed IDs are silently omitted. Args: after: Cursor for forward pagination before: Cursor for backward pagination + filter_id: Restrict results to providers with this ID. Repeatable, max 100. + limit: Maximum number of items to return extra_headers: Send extra headers @@ -276,6 +283,7 @@ def list( "before": before, "cursor": cursor, "expand": expand, + "filter_id": filter_id, "identifier": identifier, "limit": limit, "slug": slug, @@ -324,6 +332,45 @@ def delete( cast_to=NoneType, ) + def validate( + self, + id: str, + *, + zone_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ValidationResult: + """ + Runs on-demand OIDC connection checks (issuer reachability, metadata retrieval, + endpoint consistency, authorization endpoint reachability, and a demonstration + client_credentials exchange) against the provider and returns a per-check + result. Results are not persisted. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not zone_id: + raise ValueError(f"Expected a non-empty value for `zone_id` but received {zone_id!r}") + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._post( + path_template("/zones/{zone_id}/providers/{id}/validate", zone_id=zone_id, id=id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ValidationResult, + ) + class AsyncProvidersResource(AsyncAPIResource): @cached_property @@ -354,7 +401,7 @@ async def create( client_id: str | Omit = omit, client_secret: str | Omit = omit, description: Optional[str] | Omit = omit, - metadata: object | Omit = omit, + metadata: provider_create_params.Metadata | Omit = omit, protocols: provider_create_params.Protocols | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -460,7 +507,7 @@ async def update( client_secret: Optional[str] | Omit = omit, description: Optional[str] | Omit = omit, identifier: str | Omit = omit, - metadata: Optional[object] | Omit = omit, + metadata: Optional[provider_update_params.Metadata] | Omit = omit, name: str | Omit = omit, protocols: Optional[provider_update_params.Protocols] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -532,6 +579,7 @@ async def list( before: str | Omit = omit, cursor: str | Omit = omit, expand: Union[Literal["total_count"], List[Literal["total_count"]]] | Omit = omit, + filter_id: Union[str, SequenceNotStr[str]] | Omit = omit, identifier: str | Omit = omit, limit: int | Omit = omit, slug: str | Omit = omit, @@ -543,14 +591,19 @@ async def list( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ProviderListResponse: - """ - Returns a list of providers in the specified zone + """Returns a list of providers in the specified zone. + + Pass `filter[id]` + (repeatable, max 100) to restrict results to a known set of provider IDs; + unknown or malformed IDs are silently omitted. Args: after: Cursor for forward pagination before: Cursor for backward pagination + filter_id: Restrict results to providers with this ID. Repeatable, max 100. + limit: Maximum number of items to return extra_headers: Send extra headers @@ -576,6 +629,7 @@ async def list( "before": before, "cursor": cursor, "expand": expand, + "filter_id": filter_id, "identifier": identifier, "limit": limit, "slug": slug, @@ -624,6 +678,45 @@ async def delete( cast_to=NoneType, ) + async def validate( + self, + id: str, + *, + zone_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ValidationResult: + """ + Runs on-demand OIDC connection checks (issuer reachability, metadata retrieval, + endpoint consistency, authorization endpoint reachability, and a demonstration + client_credentials exchange) against the provider and returns a per-check + result. Results are not persisted. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not zone_id: + raise ValueError(f"Expected a non-empty value for `zone_id` but received {zone_id!r}") + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._post( + path_template("/zones/{zone_id}/providers/{id}/validate", zone_id=zone_id, id=id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ValidationResult, + ) + class ProvidersResourceWithRawResponse: def __init__(self, providers: ProvidersResource) -> None: @@ -644,6 +737,9 @@ def __init__(self, providers: ProvidersResource) -> None: self.delete = to_raw_response_wrapper( providers.delete, ) + self.validate = to_raw_response_wrapper( + providers.validate, + ) class AsyncProvidersResourceWithRawResponse: @@ -665,6 +761,9 @@ def __init__(self, providers: AsyncProvidersResource) -> None: self.delete = async_to_raw_response_wrapper( providers.delete, ) + self.validate = async_to_raw_response_wrapper( + providers.validate, + ) class ProvidersResourceWithStreamingResponse: @@ -686,6 +785,9 @@ def __init__(self, providers: ProvidersResource) -> None: self.delete = to_streamed_response_wrapper( providers.delete, ) + self.validate = to_streamed_response_wrapper( + providers.validate, + ) class AsyncProvidersResourceWithStreamingResponse: @@ -707,3 +809,6 @@ def __init__(self, providers: AsyncProvidersResource) -> None: self.delete = async_to_streamed_response_wrapper( providers.delete, ) + self.validate = async_to_streamed_response_wrapper( + providers.validate, + ) diff --git a/src/keycardai_api/resources/zones/resources.py b/src/keycardai_api/resources/zones/resources.py index 2941cfe..0c24e0c 100644 --- a/src/keycardai_api/resources/zones/resources.py +++ b/src/keycardai_api/resources/zones/resources.py @@ -271,6 +271,9 @@ def list( before: str | Omit = omit, credential_provider_id: str | Omit = omit, expand: Union[Literal["total_count"], List[Literal["total_count"]]] | Omit = omit, + filter_identifier: Union[str, SequenceNotStr[str]] | Omit = omit, + filter_owner_type: Literal["platform", "customer"] | Omit = omit, + filter_traits: Union[str, SequenceNotStr[str]] | Omit = omit, identifier: str | Omit = omit, limit: int | Omit = omit, slug: str | Omit = omit, @@ -281,8 +284,15 @@ def list( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ResourceListResponse: - """ - Returns a list of resources in the specified zone + """Returns a paginated list of resources in the specified zone. + + Use cursor + pagination via `after`/`before`, and `expand[]=total_count` to include the + matching row count. Filter by exact identifier via `filter[identifier]`. Filter + by trait via `filter[traits]`: comma-separated values are AND'd, repeated params + are OR'd. The scalar `identifier` query parameter is a backward-compatible alias + for `filter[identifier]`: exact match on a single value, folded into the same + exact-match identifier filter. Args: after: Cursor for forward pagination @@ -291,7 +301,15 @@ def list( credential_provider_id: Filter resources by credential provider ID - identifier: Filter resources by identifier + filter_identifier: Filter by exact resource identifier + + filter_owner_type: Filter by owner type: `platform` (Keycard-managed) or `customer` (org-created). + + filter_traits: Filter by trait. Comma-separated values (`a,b`) are AND'd; repeated params are + OR'd. + + identifier: Backward-compatible alias for `filter[identifier]`: exact match on a single + resource identifier. limit: Maximum number of items to return @@ -318,6 +336,9 @@ def list( "before": before, "credential_provider_id": credential_provider_id, "expand": expand, + "filter_identifier": filter_identifier, + "filter_owner_type": filter_owner_type, + "filter_traits": filter_traits, "identifier": identifier, "limit": limit, "slug": slug, @@ -606,6 +627,9 @@ async def list( before: str | Omit = omit, credential_provider_id: str | Omit = omit, expand: Union[Literal["total_count"], List[Literal["total_count"]]] | Omit = omit, + filter_identifier: Union[str, SequenceNotStr[str]] | Omit = omit, + filter_owner_type: Literal["platform", "customer"] | Omit = omit, + filter_traits: Union[str, SequenceNotStr[str]] | Omit = omit, identifier: str | Omit = omit, limit: int | Omit = omit, slug: str | Omit = omit, @@ -616,8 +640,15 @@ async def list( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ResourceListResponse: - """ - Returns a list of resources in the specified zone + """Returns a paginated list of resources in the specified zone. + + Use cursor + pagination via `after`/`before`, and `expand[]=total_count` to include the + matching row count. Filter by exact identifier via `filter[identifier]`. Filter + by trait via `filter[traits]`: comma-separated values are AND'd, repeated params + are OR'd. The scalar `identifier` query parameter is a backward-compatible alias + for `filter[identifier]`: exact match on a single value, folded into the same + exact-match identifier filter. Args: after: Cursor for forward pagination @@ -626,7 +657,15 @@ async def list( credential_provider_id: Filter resources by credential provider ID - identifier: Filter resources by identifier + filter_identifier: Filter by exact resource identifier + + filter_owner_type: Filter by owner type: `platform` (Keycard-managed) or `customer` (org-created). + + filter_traits: Filter by trait. Comma-separated values (`a,b`) are AND'd; repeated params are + OR'd. + + identifier: Backward-compatible alias for `filter[identifier]`: exact match on a single + resource identifier. limit: Maximum number of items to return @@ -653,6 +692,9 @@ async def list( "before": before, "credential_provider_id": credential_provider_id, "expand": expand, + "filter_identifier": filter_identifier, + "filter_owner_type": filter_owner_type, + "filter_traits": filter_traits, "identifier": identifier, "limit": limit, "slug": slug, diff --git a/src/keycardai_api/resources/zones/roles.py b/src/keycardai_api/resources/zones/roles.py new file mode 100644 index 0000000..36a2379 --- /dev/null +++ b/src/keycardai_api/resources/zones/roles.py @@ -0,0 +1,613 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List, Union, Optional +from typing_extensions import Literal + +import httpx + +from ..._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given +from ..._utils import path_template, maybe_transform, async_maybe_transform +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ...types.zones import role_list_params, role_create_params, role_update_params +from ..._base_client import make_request_options +from ...types.zones.role import Role +from ...types.zones.role_list_response import RoleListResponse + +__all__ = ["RolesResource", "AsyncRolesResource"] + + +class RolesResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> RolesResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/keycardai/keycard-python#accessing-raw-response-data-eg-headers + """ + return RolesResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> RolesResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/keycardai/keycard-python#with_streaming_response + """ + return RolesResourceWithStreamingResponse(self) + + def create( + self, + zone_id: str, + *, + identifier: str, + description: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> Role: + """Creates a new customer-owned role in the specified zone. + + The owner_type is + always customer; platform roles are managed by Keycard. + + Args: + identifier: Role identifier: a lowercase slug (letters and digits separated by single + hyphens or underscores), unique per owner type within a zone. Role identifiers + surface in policy evaluation, so the slug restriction keeps them unambiguous in + policy text. + + description: Human-readable description + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not zone_id: + raise ValueError(f"Expected a non-empty value for `zone_id` but received {zone_id!r}") + return self._post( + path_template("/zones/{zone_id}/roles", zone_id=zone_id), + body=maybe_transform( + { + "identifier": identifier, + "description": description, + }, + role_create_params.RoleCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Role, + ) + + def retrieve( + self, + role_id: str, + *, + zone_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> Role: + """ + Returns details of a specific role by ID + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not zone_id: + raise ValueError(f"Expected a non-empty value for `zone_id` but received {zone_id!r}") + if not role_id: + raise ValueError(f"Expected a non-empty value for `role_id` but received {role_id!r}") + return self._get( + path_template("/zones/{zone_id}/roles/{role_id}", zone_id=zone_id, role_id=role_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Role, + ) + + def update( + self, + role_id: str, + *, + zone_id: str, + description: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> Role: + """Updates a customer-owned role's description. + + The identifier is immutable, and + platform-owned roles cannot be modified. + + Args: + description: Human-readable description (set to null to unset) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not zone_id: + raise ValueError(f"Expected a non-empty value for `zone_id` but received {zone_id!r}") + if not role_id: + raise ValueError(f"Expected a non-empty value for `role_id` but received {role_id!r}") + return self._patch( + path_template("/zones/{zone_id}/roles/{role_id}", zone_id=zone_id, role_id=role_id), + body=maybe_transform({"description": description}, role_update_params.RoleUpdateParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Role, + ) + + def list( + self, + zone_id: str, + *, + after: str | Omit = omit, + before: str | Omit = omit, + expand: Union[Literal["total_count"], List[Literal["total_count"]]] | Omit = omit, + identifier: str | Omit = omit, + limit: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> RoleListResponse: + """Returns the roles defined in the specified zone. + + The full result set is + currently returned in a single page; the `after`/`before`/`limit` cursor + parameters are reserved and not yet enforced, and `pagination` cursors are + always null. + + Args: + after: Cursor for forward pagination + + before: Cursor for backward pagination + + identifier: Filter roles by identifier + + limit: Maximum number of items to return + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not zone_id: + raise ValueError(f"Expected a non-empty value for `zone_id` but received {zone_id!r}") + return self._get( + path_template("/zones/{zone_id}/roles", zone_id=zone_id), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "after": after, + "before": before, + "expand": expand, + "identifier": identifier, + "limit": limit, + }, + role_list_params.RoleListParams, + ), + ), + cast_to=RoleListResponse, + ) + + def delete( + self, + role_id: str, + *, + zone_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> None: + """Permanently deletes a customer-owned role. + + Platform-owned roles cannot be + deleted, and a role with existing assignments returns 409. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not zone_id: + raise ValueError(f"Expected a non-empty value for `zone_id` but received {zone_id!r}") + if not role_id: + raise ValueError(f"Expected a non-empty value for `role_id` but received {role_id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._delete( + path_template("/zones/{zone_id}/roles/{role_id}", zone_id=zone_id, role_id=role_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + +class AsyncRolesResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncRolesResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/keycardai/keycard-python#accessing-raw-response-data-eg-headers + """ + return AsyncRolesResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncRolesResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/keycardai/keycard-python#with_streaming_response + """ + return AsyncRolesResourceWithStreamingResponse(self) + + async def create( + self, + zone_id: str, + *, + identifier: str, + description: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> Role: + """Creates a new customer-owned role in the specified zone. + + The owner_type is + always customer; platform roles are managed by Keycard. + + Args: + identifier: Role identifier: a lowercase slug (letters and digits separated by single + hyphens or underscores), unique per owner type within a zone. Role identifiers + surface in policy evaluation, so the slug restriction keeps them unambiguous in + policy text. + + description: Human-readable description + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not zone_id: + raise ValueError(f"Expected a non-empty value for `zone_id` but received {zone_id!r}") + return await self._post( + path_template("/zones/{zone_id}/roles", zone_id=zone_id), + body=await async_maybe_transform( + { + "identifier": identifier, + "description": description, + }, + role_create_params.RoleCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Role, + ) + + async def retrieve( + self, + role_id: str, + *, + zone_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> Role: + """ + Returns details of a specific role by ID + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not zone_id: + raise ValueError(f"Expected a non-empty value for `zone_id` but received {zone_id!r}") + if not role_id: + raise ValueError(f"Expected a non-empty value for `role_id` but received {role_id!r}") + return await self._get( + path_template("/zones/{zone_id}/roles/{role_id}", zone_id=zone_id, role_id=role_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Role, + ) + + async def update( + self, + role_id: str, + *, + zone_id: str, + description: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> Role: + """Updates a customer-owned role's description. + + The identifier is immutable, and + platform-owned roles cannot be modified. + + Args: + description: Human-readable description (set to null to unset) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not zone_id: + raise ValueError(f"Expected a non-empty value for `zone_id` but received {zone_id!r}") + if not role_id: + raise ValueError(f"Expected a non-empty value for `role_id` but received {role_id!r}") + return await self._patch( + path_template("/zones/{zone_id}/roles/{role_id}", zone_id=zone_id, role_id=role_id), + body=await async_maybe_transform({"description": description}, role_update_params.RoleUpdateParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Role, + ) + + async def list( + self, + zone_id: str, + *, + after: str | Omit = omit, + before: str | Omit = omit, + expand: Union[Literal["total_count"], List[Literal["total_count"]]] | Omit = omit, + identifier: str | Omit = omit, + limit: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> RoleListResponse: + """Returns the roles defined in the specified zone. + + The full result set is + currently returned in a single page; the `after`/`before`/`limit` cursor + parameters are reserved and not yet enforced, and `pagination` cursors are + always null. + + Args: + after: Cursor for forward pagination + + before: Cursor for backward pagination + + identifier: Filter roles by identifier + + limit: Maximum number of items to return + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not zone_id: + raise ValueError(f"Expected a non-empty value for `zone_id` but received {zone_id!r}") + return await self._get( + path_template("/zones/{zone_id}/roles", zone_id=zone_id), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform( + { + "after": after, + "before": before, + "expand": expand, + "identifier": identifier, + "limit": limit, + }, + role_list_params.RoleListParams, + ), + ), + cast_to=RoleListResponse, + ) + + async def delete( + self, + role_id: str, + *, + zone_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> None: + """Permanently deletes a customer-owned role. + + Platform-owned roles cannot be + deleted, and a role with existing assignments returns 409. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not zone_id: + raise ValueError(f"Expected a non-empty value for `zone_id` but received {zone_id!r}") + if not role_id: + raise ValueError(f"Expected a non-empty value for `role_id` but received {role_id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return await self._delete( + path_template("/zones/{zone_id}/roles/{role_id}", zone_id=zone_id, role_id=role_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + +class RolesResourceWithRawResponse: + def __init__(self, roles: RolesResource) -> None: + self._roles = roles + + self.create = to_raw_response_wrapper( + roles.create, + ) + self.retrieve = to_raw_response_wrapper( + roles.retrieve, + ) + self.update = to_raw_response_wrapper( + roles.update, + ) + self.list = to_raw_response_wrapper( + roles.list, + ) + self.delete = to_raw_response_wrapper( + roles.delete, + ) + + +class AsyncRolesResourceWithRawResponse: + def __init__(self, roles: AsyncRolesResource) -> None: + self._roles = roles + + self.create = async_to_raw_response_wrapper( + roles.create, + ) + self.retrieve = async_to_raw_response_wrapper( + roles.retrieve, + ) + self.update = async_to_raw_response_wrapper( + roles.update, + ) + self.list = async_to_raw_response_wrapper( + roles.list, + ) + self.delete = async_to_raw_response_wrapper( + roles.delete, + ) + + +class RolesResourceWithStreamingResponse: + def __init__(self, roles: RolesResource) -> None: + self._roles = roles + + self.create = to_streamed_response_wrapper( + roles.create, + ) + self.retrieve = to_streamed_response_wrapper( + roles.retrieve, + ) + self.update = to_streamed_response_wrapper( + roles.update, + ) + self.list = to_streamed_response_wrapper( + roles.list, + ) + self.delete = to_streamed_response_wrapper( + roles.delete, + ) + + +class AsyncRolesResourceWithStreamingResponse: + def __init__(self, roles: AsyncRolesResource) -> None: + self._roles = roles + + self.create = async_to_streamed_response_wrapper( + roles.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + roles.retrieve, + ) + self.update = async_to_streamed_response_wrapper( + roles.update, + ) + self.list = async_to_streamed_response_wrapper( + roles.list, + ) + self.delete = async_to_streamed_response_wrapper( + roles.delete, + ) diff --git a/src/keycardai_api/resources/zones/secrets.py b/src/keycardai_api/resources/zones/secrets.py index bce28cd..0a87ed5 100644 --- a/src/keycardai_api/resources/zones/secrets.py +++ b/src/keycardai_api/resources/zones/secrets.py @@ -75,9 +75,7 @@ def create( metadata: A JSON object containing arbitrary metadata. Metadata will not be encrypted. - body_zone_id: Optional zone ID. This field is provided for API compatibility but is ignored - during processing. The zone ID is derived from the path parameter - (/zones/{zone_id}/secrets) and takes precedence. + body_zone_id: A globally unique opaque identifier extra_headers: Send extra headers @@ -345,9 +343,7 @@ async def create( metadata: A JSON object containing arbitrary metadata. Metadata will not be encrypted. - body_zone_id: Optional zone ID. This field is provided for API compatibility but is ignored - during processing. The zone ID is derived from the path parameter - (/zones/{zone_id}/secrets) and takes precedence. + body_zone_id: A globally unique opaque identifier extra_headers: Send extra headers diff --git a/src/keycardai_api/resources/zones/users.py b/src/keycardai_api/resources/zones/users.py deleted file mode 100644 index 81cd5a1..0000000 --- a/src/keycardai_api/resources/zones/users.py +++ /dev/null @@ -1,401 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing import List, Union -from typing_extensions import Literal - -import httpx - -from ..._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given -from ..._utils import path_template, maybe_transform, async_maybe_transform -from ..._compat import cached_property -from ..._resource import SyncAPIResource, AsyncAPIResource -from ..._response import ( - to_raw_response_wrapper, - to_streamed_response_wrapper, - async_to_raw_response_wrapper, - async_to_streamed_response_wrapper, -) -from ...types.zones import user_list_params -from ..._base_client import make_request_options -from ...types.zones.user import User -from ...types.zones.user_list_response import UserListResponse - -__all__ = ["UsersResource", "AsyncUsersResource"] - - -class UsersResource(SyncAPIResource): - @cached_property - def with_raw_response(self) -> UsersResourceWithRawResponse: - """ - This property can be used as a prefix for any HTTP method call to return - the raw response object instead of the parsed content. - - For more information, see https://www.github.com/keycardai/keycard-python#accessing-raw-response-data-eg-headers - """ - return UsersResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> UsersResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/keycardai/keycard-python#with_streaming_response - """ - return UsersResourceWithStreamingResponse(self) - - def retrieve( - self, - id: str, - *, - zone_id: str, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> User: - """ - Returns details of a specific user by user ID - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not zone_id: - raise ValueError(f"Expected a non-empty value for `zone_id` but received {zone_id!r}") - if not id: - raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") - return self._get( - path_template("/zones/{zone_id}/users/{id}", zone_id=zone_id, id=id), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=User, - ) - - def list( - self, - zone_id: str, - *, - after: str | Omit = omit, - before: str | Omit = omit, - expand: Union[ - Literal["total_count", "session_count", "grant_count", "role-assignments"], - List[Literal["total_count", "session_count", "grant_count", "role-assignments"]], - ] - | Omit = omit, - filter_email: Union[str, SequenceNotStr[str]] | Omit = omit, - filter_id: Union[str, SequenceNotStr[str]] | Omit = omit, - limit: int | Omit = omit, - query: Union[str, SequenceNotStr[str]] | Omit = omit, - query_email: Union[str, SequenceNotStr[str]] | Omit = omit, - query_subject: Union[str, SequenceNotStr[str]] | Omit = omit, - sort: str | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> UserListResponse: - """ - Returns a list of users in the specified zone. - - **Rollout note:** the paginated/searchable/sortable behavior described below is - gated behind the `user-pagination` feature flag and is currently disabled for - most zones. While the flag is off, the response returns every user in the zone - (capped at 100) in `items` and a fixed pagination envelope where `after_cursor` - and `before_cursor` are `null` and `total_count` is `0`. The query parameters - below are accepted but ignored. The flag is rolled out per-zone in Datadog and - will become the default once Console adopts the paginated contract. - - Use cursor pagination via `after`/`before`. Sort: comma-separated field list; - prefix with `-` for descending. Use `expand[]=total_count` to include the - matching row count, `expand[]=session_count` to include per-user session counts, - `expand[]=grant_count` to include per-user delegated-grant counts, and - `expand[]=role-assignments` to include each user's structured role grants. - Filter by exact email via `filter[email]`; search via `query[email]` / - `query[subject]` / `query[]` (substring match, OR'd across repeated values). - `query[]` matches against email and federation credential subject. Pass - `filter[id]` (repeatable, max 100) to restrict results to a known set of users — - mutually exclusive with `after`/`before` (returns 400 if combined). When - `filter[id]` is set, `limit` is ignored and the response contains every - requested user that exists in the zone, in a single page. IDs not in the zone - are silently omitted. - - Args: - after: Cursor for forward pagination - - before: Cursor for backward pagination - - filter_email: Filter by exact email address - - filter_id: Restrict results to users with this publicId. Repeatable, max 100. Mutually - exclusive with after/before. - - limit: Maximum number of items to return - - query: Search across email and credential subject (substring match) - - query_email: Search by email (substring match) - - query_subject: Search by federated credential subject (substring match) - - sort: Comma-separated sort fields. Prefix with - for descending. Allowed: created_at, - email, authenticated_at - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not zone_id: - raise ValueError(f"Expected a non-empty value for `zone_id` but received {zone_id!r}") - return self._get( - path_template("/zones/{zone_id}/users", zone_id=zone_id), - options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - query=maybe_transform( - { - "after": after, - "before": before, - "expand": expand, - "filter_email": filter_email, - "filter_id": filter_id, - "limit": limit, - "query": query, - "query_email": query_email, - "query_subject": query_subject, - "sort": sort, - }, - user_list_params.UserListParams, - ), - ), - cast_to=UserListResponse, - ) - - -class AsyncUsersResource(AsyncAPIResource): - @cached_property - def with_raw_response(self) -> AsyncUsersResourceWithRawResponse: - """ - This property can be used as a prefix for any HTTP method call to return - the raw response object instead of the parsed content. - - For more information, see https://www.github.com/keycardai/keycard-python#accessing-raw-response-data-eg-headers - """ - return AsyncUsersResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> AsyncUsersResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/keycardai/keycard-python#with_streaming_response - """ - return AsyncUsersResourceWithStreamingResponse(self) - - async def retrieve( - self, - id: str, - *, - zone_id: str, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> User: - """ - Returns details of a specific user by user ID - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not zone_id: - raise ValueError(f"Expected a non-empty value for `zone_id` but received {zone_id!r}") - if not id: - raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") - return await self._get( - path_template("/zones/{zone_id}/users/{id}", zone_id=zone_id, id=id), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=User, - ) - - async def list( - self, - zone_id: str, - *, - after: str | Omit = omit, - before: str | Omit = omit, - expand: Union[ - Literal["total_count", "session_count", "grant_count", "role-assignments"], - List[Literal["total_count", "session_count", "grant_count", "role-assignments"]], - ] - | Omit = omit, - filter_email: Union[str, SequenceNotStr[str]] | Omit = omit, - filter_id: Union[str, SequenceNotStr[str]] | Omit = omit, - limit: int | Omit = omit, - query: Union[str, SequenceNotStr[str]] | Omit = omit, - query_email: Union[str, SequenceNotStr[str]] | Omit = omit, - query_subject: Union[str, SequenceNotStr[str]] | Omit = omit, - sort: str | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> UserListResponse: - """ - Returns a list of users in the specified zone. - - **Rollout note:** the paginated/searchable/sortable behavior described below is - gated behind the `user-pagination` feature flag and is currently disabled for - most zones. While the flag is off, the response returns every user in the zone - (capped at 100) in `items` and a fixed pagination envelope where `after_cursor` - and `before_cursor` are `null` and `total_count` is `0`. The query parameters - below are accepted but ignored. The flag is rolled out per-zone in Datadog and - will become the default once Console adopts the paginated contract. - - Use cursor pagination via `after`/`before`. Sort: comma-separated field list; - prefix with `-` for descending. Use `expand[]=total_count` to include the - matching row count, `expand[]=session_count` to include per-user session counts, - `expand[]=grant_count` to include per-user delegated-grant counts, and - `expand[]=role-assignments` to include each user's structured role grants. - Filter by exact email via `filter[email]`; search via `query[email]` / - `query[subject]` / `query[]` (substring match, OR'd across repeated values). - `query[]` matches against email and federation credential subject. Pass - `filter[id]` (repeatable, max 100) to restrict results to a known set of users — - mutually exclusive with `after`/`before` (returns 400 if combined). When - `filter[id]` is set, `limit` is ignored and the response contains every - requested user that exists in the zone, in a single page. IDs not in the zone - are silently omitted. - - Args: - after: Cursor for forward pagination - - before: Cursor for backward pagination - - filter_email: Filter by exact email address - - filter_id: Restrict results to users with this publicId. Repeatable, max 100. Mutually - exclusive with after/before. - - limit: Maximum number of items to return - - query: Search across email and credential subject (substring match) - - query_email: Search by email (substring match) - - query_subject: Search by federated credential subject (substring match) - - sort: Comma-separated sort fields. Prefix with - for descending. Allowed: created_at, - email, authenticated_at - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not zone_id: - raise ValueError(f"Expected a non-empty value for `zone_id` but received {zone_id!r}") - return await self._get( - path_template("/zones/{zone_id}/users", zone_id=zone_id), - options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - query=await async_maybe_transform( - { - "after": after, - "before": before, - "expand": expand, - "filter_email": filter_email, - "filter_id": filter_id, - "limit": limit, - "query": query, - "query_email": query_email, - "query_subject": query_subject, - "sort": sort, - }, - user_list_params.UserListParams, - ), - ), - cast_to=UserListResponse, - ) - - -class UsersResourceWithRawResponse: - def __init__(self, users: UsersResource) -> None: - self._users = users - - self.retrieve = to_raw_response_wrapper( - users.retrieve, - ) - self.list = to_raw_response_wrapper( - users.list, - ) - - -class AsyncUsersResourceWithRawResponse: - def __init__(self, users: AsyncUsersResource) -> None: - self._users = users - - self.retrieve = async_to_raw_response_wrapper( - users.retrieve, - ) - self.list = async_to_raw_response_wrapper( - users.list, - ) - - -class UsersResourceWithStreamingResponse: - def __init__(self, users: UsersResource) -> None: - self._users = users - - self.retrieve = to_streamed_response_wrapper( - users.retrieve, - ) - self.list = to_streamed_response_wrapper( - users.list, - ) - - -class AsyncUsersResourceWithStreamingResponse: - def __init__(self, users: AsyncUsersResource) -> None: - self._users = users - - self.retrieve = async_to_streamed_response_wrapper( - users.retrieve, - ) - self.list = async_to_streamed_response_wrapper( - users.list, - ) diff --git a/src/keycardai_api/resources/zones/users/__init__.py b/src/keycardai_api/resources/zones/users/__init__.py new file mode 100644 index 0000000..0348960 --- /dev/null +++ b/src/keycardai_api/resources/zones/users/__init__.py @@ -0,0 +1,33 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .roles import ( + RolesResource, + AsyncRolesResource, + RolesResourceWithRawResponse, + AsyncRolesResourceWithRawResponse, + RolesResourceWithStreamingResponse, + AsyncRolesResourceWithStreamingResponse, +) +from .users import ( + UsersResource, + AsyncUsersResource, + UsersResourceWithRawResponse, + AsyncUsersResourceWithRawResponse, + UsersResourceWithStreamingResponse, + AsyncUsersResourceWithStreamingResponse, +) + +__all__ = [ + "RolesResource", + "AsyncRolesResource", + "RolesResourceWithRawResponse", + "AsyncRolesResourceWithRawResponse", + "RolesResourceWithStreamingResponse", + "AsyncRolesResourceWithStreamingResponse", + "UsersResource", + "AsyncUsersResource", + "UsersResourceWithRawResponse", + "AsyncUsersResourceWithRawResponse", + "UsersResourceWithStreamingResponse", + "AsyncUsersResourceWithStreamingResponse", +] diff --git a/src/keycardai_api/resources/zones/users/roles.py b/src/keycardai_api/resources/zones/users/roles.py new file mode 100644 index 0000000..9789cf2 --- /dev/null +++ b/src/keycardai_api/resources/zones/users/roles.py @@ -0,0 +1,523 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List, Union +from typing_extensions import Literal + +import httpx + +from ...._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given +from ...._utils import path_template, maybe_transform, async_maybe_transform +from ...._compat import cached_property +from ...._resource import SyncAPIResource, AsyncAPIResource +from ...._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ...._base_client import make_request_options +from ....types.zones.users import role_list_params, role_assign_params, role_revoke_params +from ....types.zones.users.role_assignment import RoleAssignment +from ....types.zones.users.role_list_response import RoleListResponse + +__all__ = ["RolesResource", "AsyncRolesResource"] + + +class RolesResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> RolesResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/keycardai/keycard-python#accessing-raw-response-data-eg-headers + """ + return RolesResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> RolesResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/keycardai/keycard-python#with_streaming_response + """ + return RolesResourceWithStreamingResponse(self) + + def list( + self, + user_id: str, + *, + zone_id: str, + after: str | Omit = omit, + before: str | Omit = omit, + expand: Union[Literal["total_count"], List[Literal["total_count"]]] | Omit = omit, + limit: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> RoleListResponse: + """Returns the roles assigned to the specified user within the zone. + + The full + result set is currently returned in a single page; the `after`/`before`/`limit` + cursor parameters are reserved and not yet enforced, and `pagination` cursors + are always null. + + Args: + after: Cursor for forward pagination + + before: Cursor for backward pagination + + limit: Maximum number of items to return + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not zone_id: + raise ValueError(f"Expected a non-empty value for `zone_id` but received {zone_id!r}") + if not user_id: + raise ValueError(f"Expected a non-empty value for `user_id` but received {user_id!r}") + return self._get( + path_template("/zones/{zone_id}/users/{user_id}/roles", zone_id=zone_id, user_id=user_id), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "after": after, + "before": before, + "expand": expand, + "limit": limit, + }, + role_list_params.RoleListParams, + ), + ), + cast_to=RoleListResponse, + ) + + def assign( + self, + user_id: str, + *, + zone_id: str, + owner_type: Literal["platform", "customer"] | Omit = omit, + role_id: str | Omit = omit, + role_identifier: str | Omit = omit, + scope_id: str | Omit = omit, + scope_type: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> RoleAssignment: + """Assigns a role to the user. + + Provide exactly one of role_id or role_identifier; + when role_identifier is used, owner_type is required to disambiguate roles that + share an identifier across owner types (and must be omitted with role_id). An + optional (scope_type, scope_id) pair scopes the grant; only platform roles on + the org zone may carry a scope, and a `zone` scope must reference a different + zone in the same organization. + + Args: + owner_type: Owner type of the role to assign. Required with role_identifier (an identifier + is unique only per owner type); must be omitted with role_id. + + role_id: ID of the role to assign. Provide exactly one of role_id or role_identifier; + owner_type must be omitted when role_id is used. + + role_identifier: Role identifier: a lowercase slug (letters and digits separated by single + hyphens or underscores), unique per owner type within a zone. Role identifiers + surface in policy evaluation, so the slug restriction keeps them unambiguous in + policy text. + + scope_id: The ID of the resource to scope the grant to. Provide together with scope_type, + or omit both for an unscoped assignment. When scope_type is `zone`, this must + reference a different zone in the same organization. + + scope_type: The kind of resource to scope the grant to (e.g. `zone`). Provide together with + scope_id, or omit both for an unscoped assignment (applies to the owning zone + itself). Only platform roles on the org zone may carry a scope. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not zone_id: + raise ValueError(f"Expected a non-empty value for `zone_id` but received {zone_id!r}") + if not user_id: + raise ValueError(f"Expected a non-empty value for `user_id` but received {user_id!r}") + return self._post( + path_template("/zones/{zone_id}/users/{user_id}/roles", zone_id=zone_id, user_id=user_id), + body=maybe_transform( + { + "owner_type": owner_type, + "role_id": role_id, + "role_identifier": role_identifier, + "scope_id": scope_id, + "scope_type": scope_type, + }, + role_assign_params.RoleAssignParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=RoleAssignment, + ) + + def revoke( + self, + role_id: str, + *, + zone_id: str, + user_id: str, + scope_id: str | Omit = omit, + scope_type: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> None: + """Revokes a role from the user. + + Provide the same (scope_type, scope_id) pair the + grant was created with, or omit both to revoke the unscoped grant. + + Args: + scope_id: Scope target of the grant to revoke. Provide together with scope_type. + + scope_type: Scope kind of the grant to revoke. Provide together with scope_id. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not zone_id: + raise ValueError(f"Expected a non-empty value for `zone_id` but received {zone_id!r}") + if not user_id: + raise ValueError(f"Expected a non-empty value for `user_id` but received {user_id!r}") + if not role_id: + raise ValueError(f"Expected a non-empty value for `role_id` but received {role_id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._delete( + path_template( + "/zones/{zone_id}/users/{user_id}/roles/{role_id}", zone_id=zone_id, user_id=user_id, role_id=role_id + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "scope_id": scope_id, + "scope_type": scope_type, + }, + role_revoke_params.RoleRevokeParams, + ), + ), + cast_to=NoneType, + ) + + +class AsyncRolesResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncRolesResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/keycardai/keycard-python#accessing-raw-response-data-eg-headers + """ + return AsyncRolesResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncRolesResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/keycardai/keycard-python#with_streaming_response + """ + return AsyncRolesResourceWithStreamingResponse(self) + + async def list( + self, + user_id: str, + *, + zone_id: str, + after: str | Omit = omit, + before: str | Omit = omit, + expand: Union[Literal["total_count"], List[Literal["total_count"]]] | Omit = omit, + limit: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> RoleListResponse: + """Returns the roles assigned to the specified user within the zone. + + The full + result set is currently returned in a single page; the `after`/`before`/`limit` + cursor parameters are reserved and not yet enforced, and `pagination` cursors + are always null. + + Args: + after: Cursor for forward pagination + + before: Cursor for backward pagination + + limit: Maximum number of items to return + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not zone_id: + raise ValueError(f"Expected a non-empty value for `zone_id` but received {zone_id!r}") + if not user_id: + raise ValueError(f"Expected a non-empty value for `user_id` but received {user_id!r}") + return await self._get( + path_template("/zones/{zone_id}/users/{user_id}/roles", zone_id=zone_id, user_id=user_id), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform( + { + "after": after, + "before": before, + "expand": expand, + "limit": limit, + }, + role_list_params.RoleListParams, + ), + ), + cast_to=RoleListResponse, + ) + + async def assign( + self, + user_id: str, + *, + zone_id: str, + owner_type: Literal["platform", "customer"] | Omit = omit, + role_id: str | Omit = omit, + role_identifier: str | Omit = omit, + scope_id: str | Omit = omit, + scope_type: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> RoleAssignment: + """Assigns a role to the user. + + Provide exactly one of role_id or role_identifier; + when role_identifier is used, owner_type is required to disambiguate roles that + share an identifier across owner types (and must be omitted with role_id). An + optional (scope_type, scope_id) pair scopes the grant; only platform roles on + the org zone may carry a scope, and a `zone` scope must reference a different + zone in the same organization. + + Args: + owner_type: Owner type of the role to assign. Required with role_identifier (an identifier + is unique only per owner type); must be omitted with role_id. + + role_id: ID of the role to assign. Provide exactly one of role_id or role_identifier; + owner_type must be omitted when role_id is used. + + role_identifier: Role identifier: a lowercase slug (letters and digits separated by single + hyphens or underscores), unique per owner type within a zone. Role identifiers + surface in policy evaluation, so the slug restriction keeps them unambiguous in + policy text. + + scope_id: The ID of the resource to scope the grant to. Provide together with scope_type, + or omit both for an unscoped assignment. When scope_type is `zone`, this must + reference a different zone in the same organization. + + scope_type: The kind of resource to scope the grant to (e.g. `zone`). Provide together with + scope_id, or omit both for an unscoped assignment (applies to the owning zone + itself). Only platform roles on the org zone may carry a scope. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not zone_id: + raise ValueError(f"Expected a non-empty value for `zone_id` but received {zone_id!r}") + if not user_id: + raise ValueError(f"Expected a non-empty value for `user_id` but received {user_id!r}") + return await self._post( + path_template("/zones/{zone_id}/users/{user_id}/roles", zone_id=zone_id, user_id=user_id), + body=await async_maybe_transform( + { + "owner_type": owner_type, + "role_id": role_id, + "role_identifier": role_identifier, + "scope_id": scope_id, + "scope_type": scope_type, + }, + role_assign_params.RoleAssignParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=RoleAssignment, + ) + + async def revoke( + self, + role_id: str, + *, + zone_id: str, + user_id: str, + scope_id: str | Omit = omit, + scope_type: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> None: + """Revokes a role from the user. + + Provide the same (scope_type, scope_id) pair the + grant was created with, or omit both to revoke the unscoped grant. + + Args: + scope_id: Scope target of the grant to revoke. Provide together with scope_type. + + scope_type: Scope kind of the grant to revoke. Provide together with scope_id. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not zone_id: + raise ValueError(f"Expected a non-empty value for `zone_id` but received {zone_id!r}") + if not user_id: + raise ValueError(f"Expected a non-empty value for `user_id` but received {user_id!r}") + if not role_id: + raise ValueError(f"Expected a non-empty value for `role_id` but received {role_id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return await self._delete( + path_template( + "/zones/{zone_id}/users/{user_id}/roles/{role_id}", zone_id=zone_id, user_id=user_id, role_id=role_id + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform( + { + "scope_id": scope_id, + "scope_type": scope_type, + }, + role_revoke_params.RoleRevokeParams, + ), + ), + cast_to=NoneType, + ) + + +class RolesResourceWithRawResponse: + def __init__(self, roles: RolesResource) -> None: + self._roles = roles + + self.list = to_raw_response_wrapper( + roles.list, + ) + self.assign = to_raw_response_wrapper( + roles.assign, + ) + self.revoke = to_raw_response_wrapper( + roles.revoke, + ) + + +class AsyncRolesResourceWithRawResponse: + def __init__(self, roles: AsyncRolesResource) -> None: + self._roles = roles + + self.list = async_to_raw_response_wrapper( + roles.list, + ) + self.assign = async_to_raw_response_wrapper( + roles.assign, + ) + self.revoke = async_to_raw_response_wrapper( + roles.revoke, + ) + + +class RolesResourceWithStreamingResponse: + def __init__(self, roles: RolesResource) -> None: + self._roles = roles + + self.list = to_streamed_response_wrapper( + roles.list, + ) + self.assign = to_streamed_response_wrapper( + roles.assign, + ) + self.revoke = to_streamed_response_wrapper( + roles.revoke, + ) + + +class AsyncRolesResourceWithStreamingResponse: + def __init__(self, roles: AsyncRolesResource) -> None: + self._roles = roles + + self.list = async_to_streamed_response_wrapper( + roles.list, + ) + self.assign = async_to_streamed_response_wrapper( + roles.assign, + ) + self.revoke = async_to_streamed_response_wrapper( + roles.revoke, + ) diff --git a/src/keycardai_api/resources/zones/users/users.py b/src/keycardai_api/resources/zones/users/users.py new file mode 100644 index 0000000..43f06a2 --- /dev/null +++ b/src/keycardai_api/resources/zones/users/users.py @@ -0,0 +1,745 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List, Union +from typing_extensions import Literal + +import httpx + +from .roles import ( + RolesResource, + AsyncRolesResource, + RolesResourceWithRawResponse, + AsyncRolesResourceWithRawResponse, + RolesResourceWithStreamingResponse, + AsyncRolesResourceWithStreamingResponse, +) +from ...._types import Body, Omit, Query, Headers, NoneType, NotGiven, SequenceNotStr, omit, not_given +from ...._utils import path_template, maybe_transform, async_maybe_transform +from ...._compat import cached_property +from ...._resource import SyncAPIResource, AsyncAPIResource +from ...._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ....types.zones import user_list_params, user_update_params, user_retrieve_params +from ...._base_client import make_request_options +from ....types.zones.user import User +from ....types.zones.user_list_response import UserListResponse + +__all__ = ["UsersResource", "AsyncUsersResource"] + + +class UsersResource(SyncAPIResource): + @cached_property + def roles(self) -> RolesResource: + return RolesResource(self._client) + + @cached_property + def with_raw_response(self) -> UsersResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/keycardai/keycard-python#accessing-raw-response-data-eg-headers + """ + return UsersResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> UsersResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/keycardai/keycard-python#with_streaming_response + """ + return UsersResourceWithStreamingResponse(self) + + def retrieve( + self, + id: str, + *, + zone_id: str, + expand: Union[Literal["role-assignments", "groups"], List[Literal["role-assignments", "groups"]]] | Omit = omit, + role_source: Literal["user", "group", "all"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> User: + """Returns details of a specific user by user ID. + + Use `expand[]=role-assignments` + for the user's structured role grants and `expand[]=groups` for the user's group + memberships. Role grants are direct only by default, each tagged with `source`; + use `role_source=all` to also include group-inherited. + + Args: + role_source: Selects which grants `expand[]=role-assignments` returns, tagging each with + `source`: `user` (direct only, the default), `group` (group-inherited only), or + `all` (both direct and group-inherited). Requires `expand[]=role-assignments`. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not zone_id: + raise ValueError(f"Expected a non-empty value for `zone_id` but received {zone_id!r}") + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._get( + path_template("/zones/{zone_id}/users/{id}", zone_id=zone_id, id=id), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "expand": expand, + "role_source": role_source, + }, + user_retrieve_params.UserRetrieveParams, + ), + ), + cast_to=User, + ) + + def update( + self, + id: str, + *, + zone_id: str, + identifier: str | Omit = omit, + status: Literal["active", "disabled"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> User: + """ + Update a user + + Args: + identifier: Zone-scoped user identifier + + status: Status of the user. Set to `disabled` to prevent the user from authenticating + and revoke their active sessions, or `active` to re-enable. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not zone_id: + raise ValueError(f"Expected a non-empty value for `zone_id` but received {zone_id!r}") + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._patch( + path_template("/zones/{zone_id}/users/{id}", zone_id=zone_id, id=id), + body=maybe_transform( + { + "identifier": identifier, + "status": status, + }, + user_update_params.UserUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=User, + ) + + def list( + self, + zone_id: str, + *, + after: str | Omit = omit, + before: str | Omit = omit, + expand: Union[ + Literal[ + "total_count", + "session_count", + "grant_count", + "role-assignments", + "groups", + "credentials", + "credentials.provider", + ], + List[ + Literal[ + "total_count", + "session_count", + "grant_count", + "role-assignments", + "groups", + "credentials", + "credentials.provider", + ] + ], + ] + | Omit = omit, + filter_email: Union[str, SequenceNotStr[str]] | Omit = omit, + filter_groups: Union[str, SequenceNotStr[str]] | Omit = omit, + filter_id: Union[str, SequenceNotStr[str]] | Omit = omit, + filter_identifier: Union[str, SequenceNotStr[str]] | Omit = omit, + limit: int | Omit = omit, + query: Union[str, SequenceNotStr[str]] | Omit = omit, + query_email: Union[str, SequenceNotStr[str]] | Omit = omit, + query_subject: Union[str, SequenceNotStr[str]] | Omit = omit, + role_source: Literal["user", "group", "all"] | Omit = omit, + sort: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> UserListResponse: + """ + Returns a list of users in the specified zone. + + Note: cursor pagination, search, and sort are not yet enabled for all zones. + Where they are not enabled, the response returns all users in the zone (capped + at 100) in `items`, with `after_cursor` and `before_cursor` set to `null` and + `total_count` of `0`; `filter[email]` and `filter[identifier]` are still + applied, while the pagination, search, and sort parameters below are accepted + but ignored. + + Use cursor pagination via `after`/`before`. Sort: comma-separated field list; + prefix with `-` for descending. Use `expand[]=total_count` to include the + matching row count, `expand[]=session_count` to include per-user session counts, + `expand[]=grant_count` to include per-user delegated-grant counts, + `expand[]=role-assignments` to include each user's structured role grants + (direct grants only by default, each tagged with `source`; use `role_source=all` + to also include group-inherited), `expand[]=groups` to include each user's group + memberships, `expand[]=credentials` to include each user's authentication + credentials (each with its `provider_id`), and `expand[]=credentials.provider` + to additionally inline the full identity provider on each federation credential. + Filter by exact email via `filter[email]` and by exact identifier via + `filter[identifier]`; restrict to members of a group via `filter[groups]` + (repeatable, OR'd across values); search via `query[email]` / `query[subject]` / + `query[]` (substring match, OR'd across repeated values). `query[]` matches + against email and federation credential subject. Pass `filter[id]` (repeatable, + max 100) to restrict results to a known set of users — mutually exclusive with + `after`/`before` (returns 400 if combined). When `filter[id]` is set, `limit` is + ignored and the response contains every requested user that exists in the zone, + in a single page. IDs not in the zone are silently omitted. + + Args: + after: Cursor for forward pagination + + before: Cursor for backward pagination + + filter_email: Filter by exact email address + + filter_groups: Restrict to members of this group (by group ID). Repeatable; OR'd across values. + + filter_id: Restrict results to users with this publicId. Repeatable, max 100. Mutually + exclusive with after/before. + + filter_identifier: Filter by exact user identifier + + limit: Maximum number of items to return + + query: Search across email and credential subject (substring match) + + query_email: Search by email (substring match) + + query_subject: Search by federated credential subject (substring match) + + role_source: Selects which grants `expand[]=role-assignments` returns, tagging each with + `source`: `user` (direct only, the default), `group` (group-inherited only), or + `all` (both direct and group-inherited). Requires `expand[]=role-assignments`. + + sort: Comma-separated sort fields. Prefix with - for descending. Allowed: created_at, + email, authenticated_at + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not zone_id: + raise ValueError(f"Expected a non-empty value for `zone_id` but received {zone_id!r}") + return self._get( + path_template("/zones/{zone_id}/users", zone_id=zone_id), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "after": after, + "before": before, + "expand": expand, + "filter_email": filter_email, + "filter_groups": filter_groups, + "filter_id": filter_id, + "filter_identifier": filter_identifier, + "limit": limit, + "query": query, + "query_email": query_email, + "query_subject": query_subject, + "role_source": role_source, + "sort": sort, + }, + user_list_params.UserListParams, + ), + ), + cast_to=UserListResponse, + ) + + def delete( + self, + id: str, + *, + zone_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> None: + """ + Delete a user + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not zone_id: + raise ValueError(f"Expected a non-empty value for `zone_id` but received {zone_id!r}") + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._delete( + path_template("/zones/{zone_id}/users/{id}", zone_id=zone_id, id=id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + +class AsyncUsersResource(AsyncAPIResource): + @cached_property + def roles(self) -> AsyncRolesResource: + return AsyncRolesResource(self._client) + + @cached_property + def with_raw_response(self) -> AsyncUsersResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/keycardai/keycard-python#accessing-raw-response-data-eg-headers + """ + return AsyncUsersResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncUsersResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/keycardai/keycard-python#with_streaming_response + """ + return AsyncUsersResourceWithStreamingResponse(self) + + async def retrieve( + self, + id: str, + *, + zone_id: str, + expand: Union[Literal["role-assignments", "groups"], List[Literal["role-assignments", "groups"]]] | Omit = omit, + role_source: Literal["user", "group", "all"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> User: + """Returns details of a specific user by user ID. + + Use `expand[]=role-assignments` + for the user's structured role grants and `expand[]=groups` for the user's group + memberships. Role grants are direct only by default, each tagged with `source`; + use `role_source=all` to also include group-inherited. + + Args: + role_source: Selects which grants `expand[]=role-assignments` returns, tagging each with + `source`: `user` (direct only, the default), `group` (group-inherited only), or + `all` (both direct and group-inherited). Requires `expand[]=role-assignments`. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not zone_id: + raise ValueError(f"Expected a non-empty value for `zone_id` but received {zone_id!r}") + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._get( + path_template("/zones/{zone_id}/users/{id}", zone_id=zone_id, id=id), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform( + { + "expand": expand, + "role_source": role_source, + }, + user_retrieve_params.UserRetrieveParams, + ), + ), + cast_to=User, + ) + + async def update( + self, + id: str, + *, + zone_id: str, + identifier: str | Omit = omit, + status: Literal["active", "disabled"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> User: + """ + Update a user + + Args: + identifier: Zone-scoped user identifier + + status: Status of the user. Set to `disabled` to prevent the user from authenticating + and revoke their active sessions, or `active` to re-enable. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not zone_id: + raise ValueError(f"Expected a non-empty value for `zone_id` but received {zone_id!r}") + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._patch( + path_template("/zones/{zone_id}/users/{id}", zone_id=zone_id, id=id), + body=await async_maybe_transform( + { + "identifier": identifier, + "status": status, + }, + user_update_params.UserUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=User, + ) + + async def list( + self, + zone_id: str, + *, + after: str | Omit = omit, + before: str | Omit = omit, + expand: Union[ + Literal[ + "total_count", + "session_count", + "grant_count", + "role-assignments", + "groups", + "credentials", + "credentials.provider", + ], + List[ + Literal[ + "total_count", + "session_count", + "grant_count", + "role-assignments", + "groups", + "credentials", + "credentials.provider", + ] + ], + ] + | Omit = omit, + filter_email: Union[str, SequenceNotStr[str]] | Omit = omit, + filter_groups: Union[str, SequenceNotStr[str]] | Omit = omit, + filter_id: Union[str, SequenceNotStr[str]] | Omit = omit, + filter_identifier: Union[str, SequenceNotStr[str]] | Omit = omit, + limit: int | Omit = omit, + query: Union[str, SequenceNotStr[str]] | Omit = omit, + query_email: Union[str, SequenceNotStr[str]] | Omit = omit, + query_subject: Union[str, SequenceNotStr[str]] | Omit = omit, + role_source: Literal["user", "group", "all"] | Omit = omit, + sort: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> UserListResponse: + """ + Returns a list of users in the specified zone. + + Note: cursor pagination, search, and sort are not yet enabled for all zones. + Where they are not enabled, the response returns all users in the zone (capped + at 100) in `items`, with `after_cursor` and `before_cursor` set to `null` and + `total_count` of `0`; `filter[email]` and `filter[identifier]` are still + applied, while the pagination, search, and sort parameters below are accepted + but ignored. + + Use cursor pagination via `after`/`before`. Sort: comma-separated field list; + prefix with `-` for descending. Use `expand[]=total_count` to include the + matching row count, `expand[]=session_count` to include per-user session counts, + `expand[]=grant_count` to include per-user delegated-grant counts, + `expand[]=role-assignments` to include each user's structured role grants + (direct grants only by default, each tagged with `source`; use `role_source=all` + to also include group-inherited), `expand[]=groups` to include each user's group + memberships, `expand[]=credentials` to include each user's authentication + credentials (each with its `provider_id`), and `expand[]=credentials.provider` + to additionally inline the full identity provider on each federation credential. + Filter by exact email via `filter[email]` and by exact identifier via + `filter[identifier]`; restrict to members of a group via `filter[groups]` + (repeatable, OR'd across values); search via `query[email]` / `query[subject]` / + `query[]` (substring match, OR'd across repeated values). `query[]` matches + against email and federation credential subject. Pass `filter[id]` (repeatable, + max 100) to restrict results to a known set of users — mutually exclusive with + `after`/`before` (returns 400 if combined). When `filter[id]` is set, `limit` is + ignored and the response contains every requested user that exists in the zone, + in a single page. IDs not in the zone are silently omitted. + + Args: + after: Cursor for forward pagination + + before: Cursor for backward pagination + + filter_email: Filter by exact email address + + filter_groups: Restrict to members of this group (by group ID). Repeatable; OR'd across values. + + filter_id: Restrict results to users with this publicId. Repeatable, max 100. Mutually + exclusive with after/before. + + filter_identifier: Filter by exact user identifier + + limit: Maximum number of items to return + + query: Search across email and credential subject (substring match) + + query_email: Search by email (substring match) + + query_subject: Search by federated credential subject (substring match) + + role_source: Selects which grants `expand[]=role-assignments` returns, tagging each with + `source`: `user` (direct only, the default), `group` (group-inherited only), or + `all` (both direct and group-inherited). Requires `expand[]=role-assignments`. + + sort: Comma-separated sort fields. Prefix with - for descending. Allowed: created_at, + email, authenticated_at + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not zone_id: + raise ValueError(f"Expected a non-empty value for `zone_id` but received {zone_id!r}") + return await self._get( + path_template("/zones/{zone_id}/users", zone_id=zone_id), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform( + { + "after": after, + "before": before, + "expand": expand, + "filter_email": filter_email, + "filter_groups": filter_groups, + "filter_id": filter_id, + "filter_identifier": filter_identifier, + "limit": limit, + "query": query, + "query_email": query_email, + "query_subject": query_subject, + "role_source": role_source, + "sort": sort, + }, + user_list_params.UserListParams, + ), + ), + cast_to=UserListResponse, + ) + + async def delete( + self, + id: str, + *, + zone_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> None: + """ + Delete a user + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not zone_id: + raise ValueError(f"Expected a non-empty value for `zone_id` but received {zone_id!r}") + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return await self._delete( + path_template("/zones/{zone_id}/users/{id}", zone_id=zone_id, id=id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + +class UsersResourceWithRawResponse: + def __init__(self, users: UsersResource) -> None: + self._users = users + + self.retrieve = to_raw_response_wrapper( + users.retrieve, + ) + self.update = to_raw_response_wrapper( + users.update, + ) + self.list = to_raw_response_wrapper( + users.list, + ) + self.delete = to_raw_response_wrapper( + users.delete, + ) + + @cached_property + def roles(self) -> RolesResourceWithRawResponse: + return RolesResourceWithRawResponse(self._users.roles) + + +class AsyncUsersResourceWithRawResponse: + def __init__(self, users: AsyncUsersResource) -> None: + self._users = users + + self.retrieve = async_to_raw_response_wrapper( + users.retrieve, + ) + self.update = async_to_raw_response_wrapper( + users.update, + ) + self.list = async_to_raw_response_wrapper( + users.list, + ) + self.delete = async_to_raw_response_wrapper( + users.delete, + ) + + @cached_property + def roles(self) -> AsyncRolesResourceWithRawResponse: + return AsyncRolesResourceWithRawResponse(self._users.roles) + + +class UsersResourceWithStreamingResponse: + def __init__(self, users: UsersResource) -> None: + self._users = users + + self.retrieve = to_streamed_response_wrapper( + users.retrieve, + ) + self.update = to_streamed_response_wrapper( + users.update, + ) + self.list = to_streamed_response_wrapper( + users.list, + ) + self.delete = to_streamed_response_wrapper( + users.delete, + ) + + @cached_property + def roles(self) -> RolesResourceWithStreamingResponse: + return RolesResourceWithStreamingResponse(self._users.roles) + + +class AsyncUsersResourceWithStreamingResponse: + def __init__(self, users: AsyncUsersResource) -> None: + self._users = users + + self.retrieve = async_to_streamed_response_wrapper( + users.retrieve, + ) + self.update = async_to_streamed_response_wrapper( + users.update, + ) + self.list = async_to_streamed_response_wrapper( + users.list, + ) + self.delete = async_to_streamed_response_wrapper( + users.delete, + ) + + @cached_property + def roles(self) -> AsyncRolesResourceWithStreamingResponse: + return AsyncRolesResourceWithStreamingResponse(self._users.roles) diff --git a/src/keycardai_api/resources/zones/zones.py b/src/keycardai_api/resources/zones/zones.py index fcbdb8a..ed64c52 100644 --- a/src/keycardai_api/resources/zones/zones.py +++ b/src/keycardai_api/resources/zones/zones.py @@ -7,13 +7,13 @@ import httpx -from .users import ( - UsersResource, - AsyncUsersResource, - UsersResourceWithRawResponse, - AsyncUsersResourceWithRawResponse, - UsersResourceWithStreamingResponse, - AsyncUsersResourceWithStreamingResponse, +from .roles import ( + RolesResource, + AsyncRolesResource, + RolesResourceWithRawResponse, + AsyncRolesResourceWithRawResponse, + RolesResourceWithStreamingResponse, + AsyncRolesResourceWithStreamingResponse, ) from ...types import ( zone_list_params, @@ -79,7 +79,23 @@ UserAgentsResourceWithStreamingResponse, AsyncUserAgentsResourceWithStreamingResponse, ) +from .users.users import ( + UsersResource, + AsyncUsersResource, + UsersResourceWithRawResponse, + AsyncUsersResourceWithRawResponse, + UsersResourceWithStreamingResponse, + AsyncUsersResourceWithStreamingResponse, +) from ...types.zone import Zone +from .groups.groups import ( + GroupsResource, + AsyncGroupsResource, + GroupsResourceWithRawResponse, + AsyncGroupsResourceWithRawResponse, + GroupsResourceWithStreamingResponse, + AsyncGroupsResourceWithStreamingResponse, +) from ..._base_client import make_request_options from .policy_schemas import ( PolicySchemasResource, @@ -172,6 +188,14 @@ def users(self) -> UsersResource: def members(self) -> MembersResource: return MembersResource(self._client) + @cached_property + def roles(self) -> RolesResource: + return RolesResource(self._client) + + @cached_property + def groups(self) -> GroupsResource: + return GroupsResource(self._client) + @cached_property def secrets(self) -> SecretsResource: return SecretsResource(self._client) @@ -188,7 +212,7 @@ def policy_schemas(self) -> PolicySchemasResource: - **Keycard::RegistrationMethod** — enum entity: `"managed"`, `"dcr"` - **Keycard::CredentialType** — enum entity: `"token"`, `"password"`, `"public-key"`, `"url"`, `"public"` - **Keycard::Resource** — `id` (String), `name` (String), `scopes` (Set of String) - - **Keycard::Claims** — `email` (String), `groups` (Set of String), plus arbitrary additional fields + - **Keycard::Claims** — `email` (String), `groups` (Set of String), `issuer_claims` (issuer-specific claims record), plus arbitrary additional fields Enum-like attributes use Cedar enum entity types (schema version `2026-03-16`+). In policies, reference values as `RegistrationMethod::"managed"` or `CredentialType::"token"`. @@ -540,6 +564,14 @@ def users(self) -> AsyncUsersResource: def members(self) -> AsyncMembersResource: return AsyncMembersResource(self._client) + @cached_property + def roles(self) -> AsyncRolesResource: + return AsyncRolesResource(self._client) + + @cached_property + def groups(self) -> AsyncGroupsResource: + return AsyncGroupsResource(self._client) + @cached_property def secrets(self) -> AsyncSecretsResource: return AsyncSecretsResource(self._client) @@ -556,7 +588,7 @@ def policy_schemas(self) -> AsyncPolicySchemasResource: - **Keycard::RegistrationMethod** — enum entity: `"managed"`, `"dcr"` - **Keycard::CredentialType** — enum entity: `"token"`, `"password"`, `"public-key"`, `"url"`, `"public"` - **Keycard::Resource** — `id` (String), `name` (String), `scopes` (Set of String) - - **Keycard::Claims** — `email` (String), `groups` (Set of String), plus arbitrary additional fields + - **Keycard::Claims** — `email` (String), `groups` (Set of String), `issuer_claims` (issuer-specific claims record), plus arbitrary additional fields Enum-like attributes use Cedar enum entity types (schema version `2026-03-16`+). In policies, reference values as `RegistrationMethod::"managed"` or `CredentialType::"token"`. @@ -927,6 +959,14 @@ def users(self) -> UsersResourceWithRawResponse: def members(self) -> MembersResourceWithRawResponse: return MembersResourceWithRawResponse(self._zones.members) + @cached_property + def roles(self) -> RolesResourceWithRawResponse: + return RolesResourceWithRawResponse(self._zones.roles) + + @cached_property + def groups(self) -> GroupsResourceWithRawResponse: + return GroupsResourceWithRawResponse(self._zones.groups) + @cached_property def secrets(self) -> SecretsResourceWithRawResponse: return SecretsResourceWithRawResponse(self._zones.secrets) @@ -943,7 +983,7 @@ def policy_schemas(self) -> PolicySchemasResourceWithRawResponse: - **Keycard::RegistrationMethod** — enum entity: `"managed"`, `"dcr"` - **Keycard::CredentialType** — enum entity: `"token"`, `"password"`, `"public-key"`, `"url"`, `"public"` - **Keycard::Resource** — `id` (String), `name` (String), `scopes` (Set of String) - - **Keycard::Claims** — `email` (String), `groups` (Set of String), plus arbitrary additional fields + - **Keycard::Claims** — `email` (String), `groups` (Set of String), `issuer_claims` (issuer-specific claims record), plus arbitrary additional fields Enum-like attributes use Cedar enum entity types (schema version `2026-03-16`+). In policies, reference values as `RegistrationMethod::"managed"` or `CredentialType::"token"`. @@ -1018,6 +1058,14 @@ def users(self) -> AsyncUsersResourceWithRawResponse: def members(self) -> AsyncMembersResourceWithRawResponse: return AsyncMembersResourceWithRawResponse(self._zones.members) + @cached_property + def roles(self) -> AsyncRolesResourceWithRawResponse: + return AsyncRolesResourceWithRawResponse(self._zones.roles) + + @cached_property + def groups(self) -> AsyncGroupsResourceWithRawResponse: + return AsyncGroupsResourceWithRawResponse(self._zones.groups) + @cached_property def secrets(self) -> AsyncSecretsResourceWithRawResponse: return AsyncSecretsResourceWithRawResponse(self._zones.secrets) @@ -1034,7 +1082,7 @@ def policy_schemas(self) -> AsyncPolicySchemasResourceWithRawResponse: - **Keycard::RegistrationMethod** — enum entity: `"managed"`, `"dcr"` - **Keycard::CredentialType** — enum entity: `"token"`, `"password"`, `"public-key"`, `"url"`, `"public"` - **Keycard::Resource** — `id` (String), `name` (String), `scopes` (Set of String) - - **Keycard::Claims** — `email` (String), `groups` (Set of String), plus arbitrary additional fields + - **Keycard::Claims** — `email` (String), `groups` (Set of String), `issuer_claims` (issuer-specific claims record), plus arbitrary additional fields Enum-like attributes use Cedar enum entity types (schema version `2026-03-16`+). In policies, reference values as `RegistrationMethod::"managed"` or `CredentialType::"token"`. @@ -1109,6 +1157,14 @@ def users(self) -> UsersResourceWithStreamingResponse: def members(self) -> MembersResourceWithStreamingResponse: return MembersResourceWithStreamingResponse(self._zones.members) + @cached_property + def roles(self) -> RolesResourceWithStreamingResponse: + return RolesResourceWithStreamingResponse(self._zones.roles) + + @cached_property + def groups(self) -> GroupsResourceWithStreamingResponse: + return GroupsResourceWithStreamingResponse(self._zones.groups) + @cached_property def secrets(self) -> SecretsResourceWithStreamingResponse: return SecretsResourceWithStreamingResponse(self._zones.secrets) @@ -1125,7 +1181,7 @@ def policy_schemas(self) -> PolicySchemasResourceWithStreamingResponse: - **Keycard::RegistrationMethod** — enum entity: `"managed"`, `"dcr"` - **Keycard::CredentialType** — enum entity: `"token"`, `"password"`, `"public-key"`, `"url"`, `"public"` - **Keycard::Resource** — `id` (String), `name` (String), `scopes` (Set of String) - - **Keycard::Claims** — `email` (String), `groups` (Set of String), plus arbitrary additional fields + - **Keycard::Claims** — `email` (String), `groups` (Set of String), `issuer_claims` (issuer-specific claims record), plus arbitrary additional fields Enum-like attributes use Cedar enum entity types (schema version `2026-03-16`+). In policies, reference values as `RegistrationMethod::"managed"` or `CredentialType::"token"`. @@ -1200,6 +1256,14 @@ def users(self) -> AsyncUsersResourceWithStreamingResponse: def members(self) -> AsyncMembersResourceWithStreamingResponse: return AsyncMembersResourceWithStreamingResponse(self._zones.members) + @cached_property + def roles(self) -> AsyncRolesResourceWithStreamingResponse: + return AsyncRolesResourceWithStreamingResponse(self._zones.roles) + + @cached_property + def groups(self) -> AsyncGroupsResourceWithStreamingResponse: + return AsyncGroupsResourceWithStreamingResponse(self._zones.groups) + @cached_property def secrets(self) -> AsyncSecretsResourceWithStreamingResponse: return AsyncSecretsResourceWithStreamingResponse(self._zones.secrets) @@ -1216,7 +1280,7 @@ def policy_schemas(self) -> AsyncPolicySchemasResourceWithStreamingResponse: - **Keycard::RegistrationMethod** — enum entity: `"managed"`, `"dcr"` - **Keycard::CredentialType** — enum entity: `"token"`, `"password"`, `"public-key"`, `"url"`, `"public"` - **Keycard::Resource** — `id` (String), `name` (String), `scopes` (Set of String) - - **Keycard::Claims** — `email` (String), `groups` (Set of String), plus arbitrary additional fields + - **Keycard::Claims** — `email` (String), `groups` (Set of String), `issuer_claims` (issuer-specific claims record), plus arbitrary additional fields Enum-like attributes use Cedar enum entity types (schema version `2026-03-16`+). In policies, reference values as `RegistrationMethod::"managed"` or `CredentialType::"token"`. diff --git a/src/keycardai_api/types/__init__.py b/src/keycardai_api/types/__init__.py index d89e8f9..99dcdfa 100644 --- a/src/keycardai_api/types/__init__.py +++ b/src/keycardai_api/types/__init__.py @@ -5,7 +5,6 @@ from .zone import Zone as Zone from .role_scope import RoleScope as RoleScope from .organization import Organization as Organization -from .token_response import TokenResponse as TokenResponse from .page_info_cursor import PageInfoCursor as PageInfoCursor from .zone_list_params import ZoneListParams as ZoneListParams from .zone_create_params import ZoneCreateParams as ZoneCreateParams @@ -18,6 +17,7 @@ from .organization_create_params import OrganizationCreateParams as OrganizationCreateParams from .organization_list_response import OrganizationListResponse as OrganizationListResponse from .organization_update_params import OrganizationUpdateParams as OrganizationUpdateParams +from .policy_bundle_update_params import PolicyBundleUpdateParams as PolicyBundleUpdateParams from .invitation_retrieve_response import InvitationRetrieveResponse as InvitationRetrieveResponse from .organization_retrieve_params import OrganizationRetrieveParams as OrganizationRetrieveParams from .encryption_key_aws_kms_config import EncryptionKeyAwsKmsConfig as EncryptionKeyAwsKmsConfig diff --git a/src/keycardai_api/types/organizations/sso_connection_protocol.py b/src/keycardai_api/types/organizations/sso_connection_protocol.py index 5767f80..42ae9b7 100644 --- a/src/keycardai_api/types/organizations/sso_connection_protocol.py +++ b/src/keycardai_api/types/organizations/sso_connection_protocol.py @@ -1,6 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import List, Optional +from typing import Dict, List, Optional from ..._models import BaseModel @@ -13,6 +13,13 @@ class Oauth2(BaseModel): authorization_endpoint: Optional[str] = None """OAuth 2.0 authorization endpoint""" + authorization_parameters: Optional[Dict[str, str]] = None + """Custom query parameters appended to authorization redirect URLs. + + Use for non-standard providers (e.g. Google prompt=consent, + access_type=offline). + """ + code_challenge_methods_supported: Optional[List[str]] = None """Supported PKCE code challenge methods""" @@ -32,6 +39,18 @@ class Oauth2(BaseModel): class Openid(BaseModel): """OpenID Connect protocol configuration for SSO connection""" + scopes: Optional[List[str]] = None + """Additional OIDC scopes to request from this provider during authentication (e.g. + + "groups"). Merged with the default scopes (openid, profile, email). + """ + + user_identifier_claim: Optional[str] = None + """ + Name of a top-level string claim in the provider's ID Token to use as the user + identifier on user creation. When not set, the user's Keycard ID is used. + """ + userinfo_endpoint: Optional[str] = None """OpenID Connect UserInfo endpoint""" diff --git a/src/keycardai_api/types/organizations/sso_connection_protocol_param.py b/src/keycardai_api/types/organizations/sso_connection_protocol_param.py index 84d6074..2183279 100644 --- a/src/keycardai_api/types/organizations/sso_connection_protocol_param.py +++ b/src/keycardai_api/types/organizations/sso_connection_protocol_param.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Optional +from typing import Dict, Optional from typing_extensions import TypedDict from ..._types import SequenceNotStr @@ -16,6 +16,13 @@ class Oauth2(TypedDict, total=False): authorization_endpoint: Optional[str] """OAuth 2.0 authorization endpoint""" + authorization_parameters: Optional[Dict[str, str]] + """Custom query parameters appended to authorization redirect URLs. + + Use for non-standard providers (e.g. Google prompt=consent, + access_type=offline). + """ + code_challenge_methods_supported: Optional[SequenceNotStr[str]] """Supported PKCE code challenge methods""" @@ -35,6 +42,18 @@ class Oauth2(TypedDict, total=False): class Openid(TypedDict, total=False): """OpenID Connect protocol configuration for SSO connection""" + scopes: Optional[SequenceNotStr[str]] + """Additional OIDC scopes to request from this provider during authentication (e.g. + + "groups"). Merged with the default scopes (openid, profile, email). + """ + + user_identifier_claim: Optional[str] + """ + Name of a top-level string claim in the provider's ID Token to use as the user + identifier on user creation. When not set, the user's Keycard ID is used. + """ + userinfo_endpoint: Optional[str] """OpenID Connect UserInfo endpoint""" diff --git a/src/keycardai_api/types/organizations/sso_connection_update_params.py b/src/keycardai_api/types/organizations/sso_connection_update_params.py index e8d852e..93c8454 100644 --- a/src/keycardai_api/types/organizations/sso_connection_update_params.py +++ b/src/keycardai_api/types/organizations/sso_connection_update_params.py @@ -2,13 +2,13 @@ from __future__ import annotations -from typing import Optional +from typing import Dict, Optional from typing_extensions import Annotated, TypedDict +from ..._types import SequenceNotStr from ..._utils import PropertyInfo -from .sso_connection_protocol_param import SSOConnectionProtocolParam -__all__ = ["SSOConnectionUpdateParams"] +__all__ = ["SSOConnectionUpdateParams", "Protocols", "ProtocolsOauth2", "ProtocolsOpenid"] class SSOConnectionUpdateParams(TypedDict, total=False): @@ -21,7 +21,86 @@ class SSOConnectionUpdateParams(TypedDict, total=False): identifier: str """SSO provider identifier (e.g., issuer URL)""" - protocols: Optional[SSOConnectionProtocolParam] - """Protocol configuration for SSO connection""" + protocols: Optional[Protocols] + """Protocol configuration for an SSO connection update. + + Omit a protocol to leave it unchanged. + """ x_client_request_id: Annotated[str, PropertyInfo(alias="X-Client-Request-ID")] + + +class ProtocolsOauth2(TypedDict, total=False): + """OAuth 2.0 protocol configuration for an SSO connection update. + + Each field is tri-state, omit to leave unchanged, send null to clear, send a value to set. + """ + + authorization_endpoint: Optional[str] + """OAuth 2.0 authorization endpoint. Set to null to clear.""" + + authorization_parameters: Optional[Dict[str, str]] + """Custom query parameters appended to authorization redirect URLs. + + Use for non-standard providers (e.g. Google prompt=consent, + access_type=offline). Set to null to clear. + """ + + code_challenge_methods_supported: Optional[SequenceNotStr[str]] + """Supported PKCE code challenge methods. Set to null to clear.""" + + jwks_uri: Optional[str] + """JSON Web Key Set endpoint. Set to null to clear.""" + + registration_endpoint: Optional[str] + """OAuth 2.0 registration endpoint. Set to null to clear.""" + + scopes_supported: Optional[SequenceNotStr[str]] + """Supported OAuth 2.0 scopes. Set to null to clear.""" + + token_endpoint: Optional[str] + """OAuth 2.0 token endpoint. Set to null to clear.""" + + +class ProtocolsOpenid(TypedDict, total=False): + """OpenID Connect protocol configuration for an SSO connection update. + + Each field is tri-state, omit to leave unchanged, send null to clear, send a value to set. + """ + + scopes: Optional[SequenceNotStr[str]] + """Additional OIDC scopes to request from this provider during authentication (e.g. + + "groups"). Merged with the default scopes (openid, profile, email). Set to null + to clear. + """ + + user_identifier_claim: Optional[str] + """ + Name of a top-level string claim in the provider's ID Token to use as the user + identifier on user creation. Set to null to clear. + """ + + userinfo_endpoint: Optional[str] + """OpenID Connect UserInfo endpoint. Set to null to clear.""" + + +class Protocols(TypedDict, total=False): + """Protocol configuration for an SSO connection update. + + Omit a protocol to leave it unchanged. + """ + + oauth2: Optional[ProtocolsOauth2] + """OAuth 2.0 protocol configuration for an SSO connection update. + + Each field is tri-state, omit to leave unchanged, send null to clear, send a + value to set. + """ + + openid: Optional[ProtocolsOpenid] + """OpenID Connect protocol configuration for an SSO connection update. + + Each field is tri-state, omit to leave unchanged, send null to clear, send a + value to set. + """ diff --git a/src/keycardai_api/types/policy_bundle_update_params.py b/src/keycardai_api/types/policy_bundle_update_params.py new file mode 100644 index 0000000..86dd8e2 --- /dev/null +++ b/src/keycardai_api/types/policy_bundle_update_params.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Annotated, TypedDict + +from .._utils import PropertyInfo + +__all__ = ["PolicyBundleUpdateParams"] + + +class PolicyBundleUpdateParams(TypedDict, total=False): + if_match: Annotated[str, PropertyInfo(alias="If-Match")] + + x_client_request_id: Annotated[str, PropertyInfo(alias="X-Client-Request-ID")] diff --git a/src/keycardai_api/types/token_response.py b/src/keycardai_api/types/token_response.py deleted file mode 100644 index a25c1ec..0000000 --- a/src/keycardai_api/types/token_response.py +++ /dev/null @@ -1,20 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Optional - -from .._models import BaseModel - -__all__ = ["TokenResponse"] - - -class TokenResponse(BaseModel): - """OAuth2-style token response for M2M tokens""" - - access_token: str - """The M2M access token""" - - token_type: str - """Token type (always "Bearer")""" - - expires_in: Optional[int] = None - """Token expiration time in seconds""" diff --git a/src/keycardai_api/types/zones/__init__.py b/src/keycardai_api/types/zones/__init__.py index 03942fa..e449c40 100644 --- a/src/keycardai_api/types/zones/__init__.py +++ b/src/keycardai_api/types/zones/__init__.py @@ -3,8 +3,10 @@ from __future__ import annotations from .url import URL as URL +from .role import Role as Role from .user import User as User from .grant import Grant as Grant +from .group import Group as Group from .token import Token as Token from .policy import Policy as Policy from .public import Public as Public @@ -20,19 +22,26 @@ from .user_agent import UserAgent as UserAgent from .application import Application as Application from .base_fields import BaseFields as BaseFields -from .task_status import TaskStatus as TaskStatus from .zone_member import ZoneMember as ZoneMember -from .install_status import InstallStatus as InstallStatus from .metadata_param import MetadataParam as MetadataParam from .schema_version import SchemaVersion as SchemaVersion -from .task_operation import TaskOperation as TaskOperation +from .role_list_params import RoleListParams as RoleListParams from .user_list_params import UserListParams as UserListParams from .application_trait import ApplicationTrait as ApplicationTrait +from .group_list_params import GroupListParams as GroupListParams from .member_add_params import MemberAddParams as MemberAddParams +from .validation_result import ValidationResult as ValidationResult from .member_list_params import MemberListParams as MemberListParams from .policy_list_params import PolicyListParams as PolicyListParams +from .role_create_params import RoleCreateParams as RoleCreateParams +from .role_list_response import RoleListResponse as RoleListResponse +from .role_update_params import RoleUpdateParams as RoleUpdateParams from .secret_list_params import SecretListParams as SecretListParams from .user_list_response import UserListResponse as UserListResponse +from .user_update_params import UserUpdateParams as UserUpdateParams +from .group_create_params import GroupCreateParams as GroupCreateParams +from .group_list_response import GroupListResponse as GroupListResponse +from .group_update_params import GroupUpdateParams as GroupUpdateParams from .policy_set_manifest import PolicySetManifest as PolicySetManifest from .secret_token_fields import SecretTokenFields as SecretTokenFields from .session_list_params import SessionListParams as SessionListParams @@ -46,7 +55,9 @@ from .secret_create_params import SecretCreateParams as SecretCreateParams from .secret_list_response import SecretListResponse as SecretListResponse from .secret_update_params import SecretUpdateParams as SecretUpdateParams +from .user_retrieve_params import UserRetrieveParams as UserRetrieveParams from .attestation_statement import AttestationStatement as AttestationStatement +from .group_retrieve_params import GroupRetrieveParams as GroupRetrieveParams from .metadata_update_param import MetadataUpdateParam as MetadataUpdateParam from .session_list_response import SessionListResponse as SessionListResponse from .session_update_params import SessionUpdateParams as SessionUpdateParams diff --git a/src/keycardai_api/types/zones/application_list_params.py b/src/keycardai_api/types/zones/application_list_params.py index fee478b..22631ca 100644 --- a/src/keycardai_api/types/zones/application_list_params.py +++ b/src/keycardai_api/types/zones/application_list_params.py @@ -7,7 +7,6 @@ from ..._types import SequenceNotStr from ..._utils import PropertyInfo -from .application_trait import ApplicationTrait __all__ = ["ApplicationListParams"] @@ -54,15 +53,3 @@ class ApplicationListParams(TypedDict, total=False): Prefix with - for descending. Allowed: created_at, name, identifier """ - - traits: List[ApplicationTrait] - """ - Filter by traits (OR matching - returns applications with any of the specified - traits) - """ - - traits_all: Annotated[List[ApplicationTrait], PropertyInfo(alias="traits[all]")] - """ - Filter by traits (AND matching - returns applications with all of the specified - traits) - """ diff --git a/src/keycardai_api/types/zones/application_list_resources_response.py b/src/keycardai_api/types/zones/application_list_resources_response.py index b7d3283..f932d79 100644 --- a/src/keycardai_api/types/zones/application_list_resources_response.py +++ b/src/keycardai_api/types/zones/application_list_resources_response.py @@ -28,8 +28,8 @@ class Pagination(BaseModel): class ApplicationListResourcesResponse(BaseModel): items: List[Resource] - page_info: PageInfoPagination - """Pagination information""" - pagination: Pagination """Cursor-based pagination metadata""" + + page_info: Optional[PageInfoPagination] = None + """Pagination information""" diff --git a/src/keycardai_api/types/zones/application_trait.py b/src/keycardai_api/types/zones/application_trait.py index 293bb0f..c266be7 100644 --- a/src/keycardai_api/types/zones/application_trait.py +++ b/src/keycardai_api/types/zones/application_trait.py @@ -4,4 +4,4 @@ __all__ = ["ApplicationTrait"] -ApplicationTrait: TypeAlias = Literal["gateway", "mcp-provider"] +ApplicationTrait: TypeAlias = Literal["gateway", "mcp-provider", "unified-gateway", "mcp-server"] diff --git a/src/keycardai_api/types/zones/applications/__init__.py b/src/keycardai_api/types/zones/applications/__init__.py index 809ee9c..ceaba20 100644 --- a/src/keycardai_api/types/zones/applications/__init__.py +++ b/src/keycardai_api/types/zones/applications/__init__.py @@ -3,6 +3,10 @@ from __future__ import annotations from .resource import Resource as Resource +from .role_list_params import RoleListParams as RoleListParams +from .role_assign_params import RoleAssignParams as RoleAssignParams +from .role_list_response import RoleListResponse as RoleListResponse +from .role_revoke_params import RoleRevokeParams as RoleRevokeParams from .dependency_add_params import DependencyAddParams as DependencyAddParams from .dependency_list_params import DependencyListParams as DependencyListParams from .dependency_list_response import DependencyListResponse as DependencyListResponse diff --git a/src/keycardai_api/types/zones/applications/dependency_list_response.py b/src/keycardai_api/types/zones/applications/dependency_list_response.py index 7e34742..0cfc93f 100644 --- a/src/keycardai_api/types/zones/applications/dependency_list_response.py +++ b/src/keycardai_api/types/zones/applications/dependency_list_response.py @@ -28,8 +28,8 @@ class Pagination(BaseModel): class DependencyListResponse(BaseModel): items: List[Resource] - page_info: PageInfoPagination - """Pagination information""" - pagination: Pagination """Cursor-based pagination metadata""" + + page_info: Optional[PageInfoPagination] = None + """Pagination information""" diff --git a/src/keycardai_api/types/zones/applications/role_assign_params.py b/src/keycardai_api/types/zones/applications/role_assign_params.py new file mode 100644 index 0000000..1f4e8dc --- /dev/null +++ b/src/keycardai_api/types/zones/applications/role_assign_params.py @@ -0,0 +1,51 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, Required, Annotated, TypedDict + +from ...._utils import PropertyInfo + +__all__ = ["RoleAssignParams"] + + +class RoleAssignParams(TypedDict, total=False): + zone_id: Required[Annotated[str, PropertyInfo(alias="zoneId")]] + + owner_type: Literal["platform", "customer"] + """Owner type of the role to assign. + + Required with role_identifier (an identifier is unique only per owner type); + must be omitted with role_id. + """ + + role_id: str + """ID of the role to assign. + + Provide exactly one of role_id or role_identifier; owner_type must be omitted + when role_id is used. + """ + + role_identifier: str + """ + Role identifier: a lowercase slug (letters and digits separated by single + hyphens or underscores), unique per owner type within a zone. Role identifiers + surface in policy evaluation, so the slug restriction keeps them unambiguous in + policy text. + """ + + scope_id: str + """The ID of the resource to scope the grant to. + + Provide together with scope_type, or omit both for an unscoped assignment. When + scope_type is `zone`, this must reference a different zone in the same + organization. + """ + + scope_type: str + """The kind of resource to scope the grant to (e.g. + + `zone`). Provide together with scope_id, or omit both for an unscoped assignment + (applies to the owning zone itself). Only platform roles on the org zone may + carry a scope. + """ diff --git a/src/keycardai_api/types/zones/applications/role_list_params.py b/src/keycardai_api/types/zones/applications/role_list_params.py new file mode 100644 index 0000000..c6f9f41 --- /dev/null +++ b/src/keycardai_api/types/zones/applications/role_list_params.py @@ -0,0 +1,25 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List, Union +from typing_extensions import Literal, Required, Annotated, TypedDict + +from ...._utils import PropertyInfo + +__all__ = ["RoleListParams"] + + +class RoleListParams(TypedDict, total=False): + zone_id: Required[Annotated[str, PropertyInfo(alias="zoneId")]] + + after: str + """Cursor for forward pagination""" + + before: str + """Cursor for backward pagination""" + + expand: Annotated[Union[Literal["total_count"], List[Literal["total_count"]]], PropertyInfo(alias="expand[]")] + + limit: int + """Maximum number of items to return""" diff --git a/src/keycardai_api/types/zones/applications/role_list_response.py b/src/keycardai_api/types/zones/applications/role_list_response.py new file mode 100644 index 0000000..abf227c --- /dev/null +++ b/src/keycardai_api/types/zones/applications/role_list_response.py @@ -0,0 +1,31 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from ...._models import BaseModel +from ..users.role_assignment import RoleAssignment + +__all__ = ["RoleListResponse", "Pagination"] + + +class Pagination(BaseModel): + """Cursor-based pagination metadata""" + + after_cursor: Optional[str] = None + """An opaque cursor used for paginating through a list of results""" + + before_cursor: Optional[str] = None + """An opaque cursor used for paginating through a list of results""" + + total_count: Optional[int] = None + """Total number of items matching the query. + + Only included when expand[]=total_count is requested. + """ + + +class RoleListResponse(BaseModel): + items: List[RoleAssignment] + + pagination: Pagination + """Cursor-based pagination metadata""" diff --git a/src/keycardai_api/types/zones/applications/role_revoke_params.py b/src/keycardai_api/types/zones/applications/role_revoke_params.py new file mode 100644 index 0000000..e8c67af --- /dev/null +++ b/src/keycardai_api/types/zones/applications/role_revoke_params.py @@ -0,0 +1,21 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, Annotated, TypedDict + +from ...._utils import PropertyInfo + +__all__ = ["RoleRevokeParams"] + + +class RoleRevokeParams(TypedDict, total=False): + zone_id: Required[Annotated[str, PropertyInfo(alias="zoneId")]] + + application_id: Required[Annotated[str, PropertyInfo(alias="applicationId")]] + + scope_id: str + """Scope target of the grant to revoke. Provide together with scope_type.""" + + scope_type: str + """Scope kind of the grant to revoke. Provide together with scope_id.""" diff --git a/src/keycardai_api/types/zones/group.py b/src/keycardai_api/types/zones/group.py new file mode 100644 index 0000000..dd5c0c7 --- /dev/null +++ b/src/keycardai_api/types/zones/group.py @@ -0,0 +1,60 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from datetime import datetime + +from ..._models import BaseModel + +__all__ = ["Group"] + + +class Group(BaseModel): + """A zone-scoped group of users, assignable to roles and usable in policies. + + Roles assigned to a group are inherited by its members. `external` is false for groups managed in Keycard and true for groups synced from an external directory. + """ + + id: str + """Unique identifier of the group""" + + created_at: datetime + """Entity creation timestamp""" + + external: bool + """Whether the group is synced from an external directory. + + When true the group is directory-owned and its membership is read-only; when + false it is managed in Keycard. Read-only: set by external sync, never by the + caller. + """ + + identifier: str + """User-specified identifier, unique within the zone. + + Automatically assigned for groups from an external directory. + """ + + name: str + """Human-readable group name""" + + organization_id: str + """Organization this group belongs to""" + + updated_at: datetime + """Entity update timestamp""" + + zone_id: str + """Zone this group belongs to""" + + member_count: Optional[int] = None + """Number of users in the group. + + Included only when requested via `expand[]=member_count` (group get or list). + """ + + roles: Optional[List[str]] = None + """Identifiers of the roles assigned to the group; members inherit them. + + Deduped across scopes. Included only when requested via `expand[]=roles` (group + get or list). + """ diff --git a/src/keycardai_api/types/zones/group_create_params.py b/src/keycardai_api/types/zones/group_create_params.py new file mode 100644 index 0000000..6ecc3c3 --- /dev/null +++ b/src/keycardai_api/types/zones/group_create_params.py @@ -0,0 +1,18 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["GroupCreateParams"] + + +class GroupCreateParams(TypedDict, total=False): + name: Required[str] + """Human-readable group name""" + + identifier: str + """User-specified identifier, unique within the zone. + + Derived from the name when omitted (a suffix is appended if it collides). + """ diff --git a/src/keycardai_api/types/zones/group_list_params.py b/src/keycardai_api/types/zones/group_list_params.py new file mode 100644 index 0000000..84363d7 --- /dev/null +++ b/src/keycardai_api/types/zones/group_list_params.py @@ -0,0 +1,45 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List, Union +from typing_extensions import Literal, Annotated, TypedDict + +from ..._types import SequenceNotStr +from ..._utils import PropertyInfo + +__all__ = ["GroupListParams"] + + +class GroupListParams(TypedDict, total=False): + after: str + """Cursor for forward pagination""" + + before: str + """Cursor for backward pagination""" + + expand: Annotated[ + Union[Literal["total_count", "member_count", "roles"], List[Literal["total_count", "member_count", "roles"]]], + PropertyInfo(alias="expand[]"), + ] + + filter_id: Annotated[Union[str, SequenceNotStr[str]], PropertyInfo(alias="filter[id]")] + """Restrict results to groups with this ID. + + Repeatable, max 100. Mutually exclusive with after/before. + """ + + filter_identifier: Annotated[Union[str, SequenceNotStr[str]], PropertyInfo(alias="filter[identifier]")] + """Filter by exact group identifier""" + + limit: int + """Maximum number of items to return""" + + query: Annotated[Union[str, SequenceNotStr[str]], PropertyInfo(alias="query[]")] + """Search across name and identifier (substring match)""" + + sort: str + """Comma-separated sort fields. + + Prefix with - for descending. Allowed: created_at, name, identifier + """ diff --git a/src/keycardai_api/types/zones/group_list_response.py b/src/keycardai_api/types/zones/group_list_response.py new file mode 100644 index 0000000..afbfec8 --- /dev/null +++ b/src/keycardai_api/types/zones/group_list_response.py @@ -0,0 +1,31 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from .group import Group +from ..._models import BaseModel + +__all__ = ["GroupListResponse", "Pagination"] + + +class Pagination(BaseModel): + """Cursor-based pagination metadata""" + + after_cursor: Optional[str] = None + """An opaque cursor used for paginating through a list of results""" + + before_cursor: Optional[str] = None + """An opaque cursor used for paginating through a list of results""" + + total_count: Optional[int] = None + """Total number of items matching the query. + + Only included when expand[]=total_count is requested. + """ + + +class GroupListResponse(BaseModel): + items: List[Group] + + pagination: Pagination + """Cursor-based pagination metadata""" diff --git a/src/keycardai_api/types/zones/group_retrieve_params.py b/src/keycardai_api/types/zones/group_retrieve_params.py new file mode 100644 index 0000000..8fd52e5 --- /dev/null +++ b/src/keycardai_api/types/zones/group_retrieve_params.py @@ -0,0 +1,18 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List, Union +from typing_extensions import Literal, Required, Annotated, TypedDict + +from ..._utils import PropertyInfo + +__all__ = ["GroupRetrieveParams"] + + +class GroupRetrieveParams(TypedDict, total=False): + zone_id: Required[Annotated[str, PropertyInfo(alias="zoneId")]] + + expand: Annotated[ + Union[Literal["member_count", "roles"], List[Literal["member_count", "roles"]]], PropertyInfo(alias="expand[]") + ] diff --git a/src/keycardai_api/types/zones/group_update_params.py b/src/keycardai_api/types/zones/group_update_params.py new file mode 100644 index 0000000..eddfc37 --- /dev/null +++ b/src/keycardai_api/types/zones/group_update_params.py @@ -0,0 +1,19 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, Annotated, TypedDict + +from ..._utils import PropertyInfo + +__all__ = ["GroupUpdateParams"] + + +class GroupUpdateParams(TypedDict, total=False): + zone_id: Required[Annotated[str, PropertyInfo(alias="zoneId")]] + + identifier: str + """User-specified identifier, unique within the zone.""" + + name: str + """Human-readable group name""" diff --git a/src/keycardai_api/types/zones/groups/__init__.py b/src/keycardai_api/types/zones/groups/__init__.py new file mode 100644 index 0000000..6984600 --- /dev/null +++ b/src/keycardai_api/types/zones/groups/__init__.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from .group_member import GroupMember as GroupMember +from .role_add_params import RoleAddParams as RoleAddParams +from .role_list_params import RoleListParams as RoleListParams +from .member_add_params import MemberAddParams as MemberAddParams +from .member_list_params import MemberListParams as MemberListParams +from .role_list_response import RoleListResponse as RoleListResponse +from .role_remove_params import RoleRemoveParams as RoleRemoveParams +from .member_list_response import MemberListResponse as MemberListResponse diff --git a/src/keycardai_api/types/zones/groups/group_member.py b/src/keycardai_api/types/zones/groups/group_member.py new file mode 100644 index 0000000..2b22370 --- /dev/null +++ b/src/keycardai_api/types/zones/groups/group_member.py @@ -0,0 +1,22 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from datetime import datetime + +from ..user import User +from ...._models import BaseModel + +__all__ = ["GroupMember"] + + +class GroupMember(BaseModel): + """A user's membership in a group""" + + created_at: datetime + """Entity creation timestamp""" + + user_id: str + """ID of the user""" + + user: Optional[User] = None + """An authenticated user entity""" diff --git a/src/keycardai_api/types/zones/groups/member_add_params.py b/src/keycardai_api/types/zones/groups/member_add_params.py new file mode 100644 index 0000000..2a1fc47 --- /dev/null +++ b/src/keycardai_api/types/zones/groups/member_add_params.py @@ -0,0 +1,16 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, Annotated, TypedDict + +from ...._utils import PropertyInfo + +__all__ = ["MemberAddParams"] + + +class MemberAddParams(TypedDict, total=False): + zone_id: Required[Annotated[str, PropertyInfo(alias="zoneId")]] + + user_id: Required[str] + """ID of the user to add to the group""" diff --git a/src/keycardai_api/types/zones/groups/member_list_params.py b/src/keycardai_api/types/zones/groups/member_list_params.py new file mode 100644 index 0000000..10acab0 --- /dev/null +++ b/src/keycardai_api/types/zones/groups/member_list_params.py @@ -0,0 +1,40 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List, Union +from typing_extensions import Literal, Required, Annotated, TypedDict + +from ...._types import SequenceNotStr +from ...._utils import PropertyInfo + +__all__ = ["MemberListParams"] + + +class MemberListParams(TypedDict, total=False): + zone_id: Required[Annotated[str, PropertyInfo(alias="zoneId")]] + + after: str + """Cursor for forward pagination""" + + before: str + """Cursor for backward pagination""" + + expand: Annotated[ + Union[Literal["total_count", "user"], List[Literal["total_count", "user"]]], PropertyInfo(alias="expand[]") + ] + + filter_id: Annotated[Union[str, SequenceNotStr[str]], PropertyInfo(alias="filter[id]")] + """Restrict results to the member with this user ID. + + Repeatable, max 100. Mutually exclusive with after/before. + """ + + limit: int + """Maximum number of items to return""" + + query: Annotated[Union[str, SequenceNotStr[str]], PropertyInfo(alias="query[]")] + """ + Search members by their user's email or federated credential subject (substring + match) + """ diff --git a/src/keycardai_api/types/zones/groups/member_list_response.py b/src/keycardai_api/types/zones/groups/member_list_response.py new file mode 100644 index 0000000..ae80762 --- /dev/null +++ b/src/keycardai_api/types/zones/groups/member_list_response.py @@ -0,0 +1,31 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from ...._models import BaseModel +from .group_member import GroupMember + +__all__ = ["MemberListResponse", "Pagination"] + + +class Pagination(BaseModel): + """Cursor-based pagination metadata""" + + after_cursor: Optional[str] = None + """An opaque cursor used for paginating through a list of results""" + + before_cursor: Optional[str] = None + """An opaque cursor used for paginating through a list of results""" + + total_count: Optional[int] = None + """Total number of items matching the query. + + Only included when expand[]=total_count is requested. + """ + + +class MemberListResponse(BaseModel): + items: List[GroupMember] + + pagination: Pagination + """Cursor-based pagination metadata""" diff --git a/src/keycardai_api/types/zones/groups/role_add_params.py b/src/keycardai_api/types/zones/groups/role_add_params.py new file mode 100644 index 0000000..81a8c46 --- /dev/null +++ b/src/keycardai_api/types/zones/groups/role_add_params.py @@ -0,0 +1,51 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, Required, Annotated, TypedDict + +from ...._utils import PropertyInfo + +__all__ = ["RoleAddParams"] + + +class RoleAddParams(TypedDict, total=False): + zone_id: Required[Annotated[str, PropertyInfo(alias="zoneId")]] + + owner_type: Literal["platform", "customer"] + """Owner type of the role to assign. + + Required with role_identifier (an identifier is unique only per owner type); + must be omitted with role_id. + """ + + role_id: str + """ID of the role to assign. + + Provide exactly one of role_id or role_identifier; owner_type must be omitted + when role_id is used. + """ + + role_identifier: str + """ + Role identifier: a lowercase slug (letters and digits separated by single + hyphens or underscores), unique per owner type within a zone. Role identifiers + surface in policy evaluation, so the slug restriction keeps them unambiguous in + policy text. + """ + + scope_id: str + """The ID of the resource to scope the grant to. + + Provide together with scope_type, or omit both for an unscoped assignment. When + scope_type is `zone`, this must reference a different zone in the same + organization. + """ + + scope_type: str + """The kind of resource to scope the grant to (e.g. + + `zone`). Provide together with scope_id, or omit both for an unscoped assignment + (applies to the owning zone itself). Only platform roles on the org zone may + carry a scope. + """ diff --git a/src/keycardai_api/types/zones/groups/role_list_params.py b/src/keycardai_api/types/zones/groups/role_list_params.py new file mode 100644 index 0000000..b5b088d --- /dev/null +++ b/src/keycardai_api/types/zones/groups/role_list_params.py @@ -0,0 +1,32 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List, Union +from typing_extensions import Literal, Required, Annotated, TypedDict + +from ...._types import SequenceNotStr +from ...._utils import PropertyInfo + +__all__ = ["RoleListParams"] + + +class RoleListParams(TypedDict, total=False): + zone_id: Required[Annotated[str, PropertyInfo(alias="zoneId")]] + + after: str + """Cursor for forward pagination""" + + before: str + """Cursor for backward pagination""" + + expand: Annotated[Union[Literal["total_count"], List[Literal["total_count"]]], PropertyInfo(alias="expand[]")] + + filter_id: Annotated[Union[str, SequenceNotStr[str]], PropertyInfo(alias="filter[id]")] + """Restrict results to the role assignment with this ID. + + Repeatable, max 100. Mutually exclusive with after/before. + """ + + limit: int + """Maximum number of items to return""" diff --git a/src/keycardai_api/types/zones/groups/role_list_response.py b/src/keycardai_api/types/zones/groups/role_list_response.py new file mode 100644 index 0000000..abf227c --- /dev/null +++ b/src/keycardai_api/types/zones/groups/role_list_response.py @@ -0,0 +1,31 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from ...._models import BaseModel +from ..users.role_assignment import RoleAssignment + +__all__ = ["RoleListResponse", "Pagination"] + + +class Pagination(BaseModel): + """Cursor-based pagination metadata""" + + after_cursor: Optional[str] = None + """An opaque cursor used for paginating through a list of results""" + + before_cursor: Optional[str] = None + """An opaque cursor used for paginating through a list of results""" + + total_count: Optional[int] = None + """Total number of items matching the query. + + Only included when expand[]=total_count is requested. + """ + + +class RoleListResponse(BaseModel): + items: List[RoleAssignment] + + pagination: Pagination + """Cursor-based pagination metadata""" diff --git a/src/keycardai_api/types/zones/groups/role_remove_params.py b/src/keycardai_api/types/zones/groups/role_remove_params.py new file mode 100644 index 0000000..23a30ff --- /dev/null +++ b/src/keycardai_api/types/zones/groups/role_remove_params.py @@ -0,0 +1,21 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, Annotated, TypedDict + +from ...._utils import PropertyInfo + +__all__ = ["RoleRemoveParams"] + + +class RoleRemoveParams(TypedDict, total=False): + zone_id: Required[Annotated[str, PropertyInfo(alias="zoneId")]] + + group_id: Required[Annotated[str, PropertyInfo(alias="groupId")]] + + scope_id: str + """Scope target of the grant to revoke. Provide together with scope_type.""" + + scope_type: str + """Scope kind of the grant to revoke. Provide together with scope_id.""" diff --git a/src/keycardai_api/types/zones/install_status.py b/src/keycardai_api/types/zones/install_status.py deleted file mode 100644 index 835b898..0000000 --- a/src/keycardai_api/types/zones/install_status.py +++ /dev/null @@ -1,7 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing_extensions import Literal, TypeAlias - -__all__ = ["InstallStatus"] - -InstallStatus: TypeAlias = Literal["pending", "active", "deleting", "failed", "deleted"] diff --git a/src/keycardai_api/types/zones/policies/policy_version.py b/src/keycardai_api/types/zones/policies/policy_version.py index f9982be..d8683af 100644 --- a/src/keycardai_api/types/zones/policies/policy_version.py +++ b/src/keycardai_api/types/zones/policies/policy_version.py @@ -43,7 +43,9 @@ class PolicyVersion(BaseModel): """Cedar policy in JSON representation. Populated by default and when `format=json` is passed; null when `format=cedar` - narrows the response to the text representation only. + narrows the response to the text representation only. Serialized verbatim from + the stored Cedar so the order of `staticPolicies` matches the source policy + order (ACC-613). """ cedar_raw: Optional[str] = None diff --git a/src/keycardai_api/types/zones/policy_sets/policy_set_version.py b/src/keycardai_api/types/zones/policy_sets/policy_set_version.py index 5b89efc..996c5e2 100644 --- a/src/keycardai_api/types/zones/policy_sets/policy_set_version.py +++ b/src/keycardai_api/types/zones/policy_sets/policy_set_version.py @@ -41,11 +41,23 @@ class PolicySetVersion(BaseModel): version: int active: Optional[bool] = None - """Whether this policy set version is currently bound with mode='active'""" + """Whether this policy set version is currently bound with mode='active'. + + Always populated in responses; clients must treat absence as unknown rather than + inferring 'not bound'. + """ archived_at: Optional[datetime] = None + """Timestamp when the version was archived. + + Non-null only for archived versions; null or absent means not archived. + """ archived_by: Optional[str] = None + """Identifier of the actor that archived the version. + + Null or absent means not archived. + """ attestation: Optional[AttestationStatement] = None """Decoded content of an Attestation JWS payload. diff --git a/src/keycardai_api/types/zones/provider.py b/src/keycardai_api/types/zones/provider.py index 48f9a9d..4bab373 100644 --- a/src/keycardai_api/types/zones/provider.py +++ b/src/keycardai_api/types/zones/provider.py @@ -6,7 +6,14 @@ from ..._models import BaseModel -__all__ = ["Provider", "Protocols", "ProtocolsOauth2", "ProtocolsOpenid"] +__all__ = ["Provider", "Metadata", "Protocols", "ProtocolsOauth2", "ProtocolsOpenid"] + + +class Metadata(BaseModel): + """Provider metadata""" + + icon_url: Optional[str] = None + """Icon URL""" class ProtocolsOauth2(BaseModel): @@ -65,6 +72,13 @@ class ProtocolsOauth2(BaseModel): class ProtocolsOpenid(BaseModel): """OpenID Connect protocol configuration""" + external_id_claim: Optional[str] = None + """ + Name of the OIDC claim carrying the stable external id used to correlate logins + with externally provisioned (SCIM) users. Defaults to "sub". Set to "oid" for + Entra, whose pairwise "sub" differs from the SCIM externalId. + """ + scopes: Optional[List[str]] = None """Additional OIDC scopes to request from this provider during authentication (e.g. @@ -137,7 +151,7 @@ class Provider(BaseModel): description: Optional[str] = None """Human-readable description""" - metadata: Optional[object] = None + metadata: Optional[Metadata] = None """Provider metadata""" protocols: Optional[Protocols] = None diff --git a/src/keycardai_api/types/zones/provider_create_params.py b/src/keycardai_api/types/zones/provider_create_params.py index ebc66b7..0a341ae 100644 --- a/src/keycardai_api/types/zones/provider_create_params.py +++ b/src/keycardai_api/types/zones/provider_create_params.py @@ -7,7 +7,7 @@ from ..._types import SequenceNotStr -__all__ = ["ProviderCreateParams", "Protocols", "ProtocolsOauth2", "ProtocolsOpenid"] +__all__ = ["ProviderCreateParams", "Metadata", "Protocols", "ProtocolsOauth2", "ProtocolsOpenid"] class ProviderCreateParams(TypedDict, total=False): @@ -35,13 +35,20 @@ class ProviderCreateParams(TypedDict, total=False): Must not contain HTML tags (e.g. `