querri 1.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- querri/__init__.py +51 -0
- querri/_auth.py +818 -0
- querri/_base_client.py +379 -0
- querri/_client.py +395 -0
- querri/_config.py +140 -0
- querri/_convenience.py +538 -0
- querri/_exceptions.py +180 -0
- querri/_pagination.py +278 -0
- querri/_streaming.py +663 -0
- querri/_user_client.py +193 -0
- querri/_version.py +1 -0
- querri/cli/__init__.py +21 -0
- querri/cli/_app.py +246 -0
- querri/cli/_context.py +216 -0
- querri/cli/_image.py +234 -0
- querri/cli/_output.py +293 -0
- querri/cli/audit.py +68 -0
- querri/cli/auth.py +556 -0
- querri/cli/chat.py +1255 -0
- querri/cli/chats.py +358 -0
- querri/cli/dashboards.py +294 -0
- querri/cli/embed.py +220 -0
- querri/cli/files.py +214 -0
- querri/cli/keys.py +304 -0
- querri/cli/policies.py +424 -0
- querri/cli/projects.py +916 -0
- querri/cli/sharing.py +403 -0
- querri/cli/sources.py +532 -0
- querri/cli/steps.py +123 -0
- querri/cli/usage.py +99 -0
- querri/cli/users.py +223 -0
- querri/cli/views.py +449 -0
- querri/cli/whoami.py +78 -0
- querri/py.typed +0 -0
- querri/resources/__init__.py +1 -0
- querri/resources/audit.py +119 -0
- querri/resources/dashboards.py +295 -0
- querri/resources/embed.py +268 -0
- querri/resources/files.py +145 -0
- querri/resources/keys.py +191 -0
- querri/resources/policies.py +492 -0
- querri/resources/projects.py +808 -0
- querri/resources/sharing.py +330 -0
- querri/resources/sources.py +511 -0
- querri/resources/usage.py +106 -0
- querri/resources/users.py +303 -0
- querri/resources/views.py +347 -0
- querri/types/__init__.py +104 -0
- querri/types/audit.py +21 -0
- querri/types/chat.py +51 -0
- querri/types/dashboard.py +51 -0
- querri/types/data.py +61 -0
- querri/types/embed.py +38 -0
- querri/types/file.py +18 -0
- querri/types/key.py +36 -0
- querri/types/policy.py +105 -0
- querri/types/project.py +158 -0
- querri/types/sharing.py +19 -0
- querri/types/usage.py +39 -0
- querri/types/user.py +33 -0
- querri-1.0.2.dist-info/METADATA +310 -0
- querri-1.0.2.dist-info/RECORD +64 -0
- querri-1.0.2.dist-info/WHEEL +4 -0
- querri-1.0.2.dist-info/entry_points.txt +2 -0
querri/resources/keys.py
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""API key management resource."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import builtins
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from .._base_client import AsyncHTTPClient, SyncHTTPClient
|
|
9
|
+
from ..types.key import ApiKey, ApiKeyCreated
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Keys:
|
|
13
|
+
"""Synchronous API key management resource.
|
|
14
|
+
|
|
15
|
+
Usage::
|
|
16
|
+
|
|
17
|
+
new_key = client.keys.create(name="CI Pipeline", scopes=["data:read"])
|
|
18
|
+
keys = client.keys.list()
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, http: SyncHTTPClient) -> None:
|
|
22
|
+
self._http = http
|
|
23
|
+
|
|
24
|
+
def create(
|
|
25
|
+
self,
|
|
26
|
+
*,
|
|
27
|
+
name: str,
|
|
28
|
+
scopes: builtins.list[str],
|
|
29
|
+
expires_in_days: int | None = None,
|
|
30
|
+
source_scope: dict[str, Any] | None = None,
|
|
31
|
+
access_policy_ids: builtins.list[str] | None = None,
|
|
32
|
+
bound_user_id: str | None = None,
|
|
33
|
+
rate_limit_per_minute: int | None = None,
|
|
34
|
+
ip_allowlist: builtins.list[str] | None = None,
|
|
35
|
+
) -> ApiKeyCreated:
|
|
36
|
+
"""Create a new API key. Returns the plaintext secret once.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
name: Display name for the key.
|
|
40
|
+
scopes: List of permission scopes.
|
|
41
|
+
expires_in_days: Days until expiration.
|
|
42
|
+
source_scope: Source access scope configuration.
|
|
43
|
+
access_policy_ids: Bound access policy UUIDs.
|
|
44
|
+
bound_user_id: User ID to bind RLS to.
|
|
45
|
+
rate_limit_per_minute: Rate limit override.
|
|
46
|
+
ip_allowlist: Allowed IP addresses.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
ApiKeyCreated with id, name, key_prefix, secret, scopes, status, etc.
|
|
50
|
+
The ``secret`` field is only returned on creation.
|
|
51
|
+
"""
|
|
52
|
+
payload: dict[str, Any] = {"name": name, "scopes": scopes}
|
|
53
|
+
if expires_in_days is not None:
|
|
54
|
+
payload["expires_in_days"] = expires_in_days
|
|
55
|
+
if source_scope is not None:
|
|
56
|
+
payload["source_scope"] = source_scope
|
|
57
|
+
if access_policy_ids is not None:
|
|
58
|
+
payload["access_policy_ids"] = access_policy_ids
|
|
59
|
+
if bound_user_id is not None:
|
|
60
|
+
payload["bound_user_id"] = bound_user_id
|
|
61
|
+
if rate_limit_per_minute is not None:
|
|
62
|
+
payload["rate_limit_per_minute"] = rate_limit_per_minute
|
|
63
|
+
if ip_allowlist is not None:
|
|
64
|
+
payload["ip_allowlist"] = ip_allowlist
|
|
65
|
+
resp = self._http.post("/keys", json=payload)
|
|
66
|
+
return ApiKeyCreated.model_validate(resp.json())
|
|
67
|
+
|
|
68
|
+
def get(self, key_id: str) -> ApiKey:
|
|
69
|
+
"""Get API key details (never returns the secret).
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
key_id: The key UUID.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
ApiKey object.
|
|
76
|
+
"""
|
|
77
|
+
resp = self._http.get(f"/keys/{key_id}")
|
|
78
|
+
return ApiKey.model_validate(resp.json())
|
|
79
|
+
|
|
80
|
+
def list(self) -> builtins.list[ApiKey]:
|
|
81
|
+
"""List API keys for the organization.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
List of ApiKey objects (secrets are never included).
|
|
85
|
+
"""
|
|
86
|
+
resp = self._http.get("/keys")
|
|
87
|
+
body = resp.json()
|
|
88
|
+
return [ApiKey.model_validate(k) for k in body.get("data", [])]
|
|
89
|
+
|
|
90
|
+
def delete(self, key_id: str) -> dict[str, Any]:
|
|
91
|
+
"""Revoke an API key.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
key_id: The key UUID.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Dict with id and status ("revoked").
|
|
98
|
+
"""
|
|
99
|
+
resp = self._http.delete(f"/keys/{key_id}")
|
|
100
|
+
return resp.json() # type: ignore[no-any-return]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class AsyncKeys:
|
|
104
|
+
"""Asynchronous API key management resource.
|
|
105
|
+
|
|
106
|
+
Usage::
|
|
107
|
+
|
|
108
|
+
new_key = await client.keys.create(name="CI Pipeline", scopes=["data:read"])
|
|
109
|
+
keys = await client.keys.list()
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
def __init__(self, http: AsyncHTTPClient) -> None:
|
|
113
|
+
self._http = http
|
|
114
|
+
|
|
115
|
+
async def create(
|
|
116
|
+
self,
|
|
117
|
+
*,
|
|
118
|
+
name: str,
|
|
119
|
+
scopes: builtins.list[str],
|
|
120
|
+
expires_in_days: int | None = None,
|
|
121
|
+
source_scope: dict[str, Any] | None = None,
|
|
122
|
+
access_policy_ids: builtins.list[str] | None = None,
|
|
123
|
+
bound_user_id: str | None = None,
|
|
124
|
+
rate_limit_per_minute: int | None = None,
|
|
125
|
+
ip_allowlist: builtins.list[str] | None = None,
|
|
126
|
+
) -> ApiKeyCreated:
|
|
127
|
+
"""Create a new API key. Returns the plaintext secret once.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
name: Display name for the key.
|
|
131
|
+
scopes: List of permission scopes.
|
|
132
|
+
expires_in_days: Days until expiration.
|
|
133
|
+
source_scope: Source access scope configuration.
|
|
134
|
+
access_policy_ids: Bound access policy UUIDs.
|
|
135
|
+
bound_user_id: User ID to bind RLS to.
|
|
136
|
+
rate_limit_per_minute: Rate limit override.
|
|
137
|
+
ip_allowlist: Allowed IP addresses.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
ApiKeyCreated with id, name, key_prefix, secret, scopes, status, etc.
|
|
141
|
+
The ``secret`` field is only returned on creation.
|
|
142
|
+
"""
|
|
143
|
+
payload: dict[str, Any] = {"name": name, "scopes": scopes}
|
|
144
|
+
if expires_in_days is not None:
|
|
145
|
+
payload["expires_in_days"] = expires_in_days
|
|
146
|
+
if source_scope is not None:
|
|
147
|
+
payload["source_scope"] = source_scope
|
|
148
|
+
if access_policy_ids is not None:
|
|
149
|
+
payload["access_policy_ids"] = access_policy_ids
|
|
150
|
+
if bound_user_id is not None:
|
|
151
|
+
payload["bound_user_id"] = bound_user_id
|
|
152
|
+
if rate_limit_per_minute is not None:
|
|
153
|
+
payload["rate_limit_per_minute"] = rate_limit_per_minute
|
|
154
|
+
if ip_allowlist is not None:
|
|
155
|
+
payload["ip_allowlist"] = ip_allowlist
|
|
156
|
+
resp = await self._http.post("/keys", json=payload)
|
|
157
|
+
return ApiKeyCreated.model_validate(resp.json())
|
|
158
|
+
|
|
159
|
+
async def get(self, key_id: str) -> ApiKey:
|
|
160
|
+
"""Get API key details (never returns the secret).
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
key_id: The key UUID.
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
ApiKey object.
|
|
167
|
+
"""
|
|
168
|
+
resp = await self._http.get(f"/keys/{key_id}")
|
|
169
|
+
return ApiKey.model_validate(resp.json())
|
|
170
|
+
|
|
171
|
+
async def list(self) -> builtins.list[ApiKey]:
|
|
172
|
+
"""List API keys for the organization.
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
List of ApiKey objects (secrets are never included).
|
|
176
|
+
"""
|
|
177
|
+
resp = await self._http.get("/keys")
|
|
178
|
+
body = resp.json()
|
|
179
|
+
return [ApiKey.model_validate(k) for k in body.get("data", [])]
|
|
180
|
+
|
|
181
|
+
async def delete(self, key_id: str) -> dict[str, Any]:
|
|
182
|
+
"""Revoke an API key.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
key_id: The key UUID.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Dict with id and status ("revoked").
|
|
189
|
+
"""
|
|
190
|
+
resp = await self._http.delete(f"/keys/{key_id}")
|
|
191
|
+
return resp.json() # type: ignore[no-any-return]
|
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
"""Access policy resources — maps to /access endpoints."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import builtins
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from .._base_client import AsyncHTTPClient, SyncHTTPClient
|
|
9
|
+
from .._convenience import async_setup_policy, sync_setup_policy
|
|
10
|
+
from .._pagination import AsyncCursorPage, SyncCursorPage
|
|
11
|
+
from ..types.policy import (
|
|
12
|
+
Policy,
|
|
13
|
+
PolicyAssignResponse,
|
|
14
|
+
PolicyDeleteResponse,
|
|
15
|
+
PolicyRemoveUserResponse,
|
|
16
|
+
PolicyReplaceResponse,
|
|
17
|
+
PolicyUpdateResponse,
|
|
18
|
+
ResolvedAccess,
|
|
19
|
+
SourceColumns,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Policies:
|
|
24
|
+
"""Synchronous access policy management.
|
|
25
|
+
|
|
26
|
+
Usage::
|
|
27
|
+
|
|
28
|
+
policy = client.policies.create(
|
|
29
|
+
name="Sales Team",
|
|
30
|
+
source_ids=["src_..."],
|
|
31
|
+
row_filters=[{"column": "region", "values": ["US"]}],
|
|
32
|
+
)
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self, http: SyncHTTPClient) -> None:
|
|
36
|
+
self._http = http
|
|
37
|
+
|
|
38
|
+
def create(
|
|
39
|
+
self,
|
|
40
|
+
*,
|
|
41
|
+
name: str,
|
|
42
|
+
description: str | None = None,
|
|
43
|
+
source_ids: builtins.list[str] | None = None,
|
|
44
|
+
row_filters: builtins.list[dict[str, Any]] | None = None,
|
|
45
|
+
) -> Policy:
|
|
46
|
+
"""Create an access policy.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
name: Policy name.
|
|
50
|
+
description: Optional description.
|
|
51
|
+
source_ids: List of source IDs this policy applies to.
|
|
52
|
+
row_filters: List of row filter dicts with ``column`` and ``values`` keys.
|
|
53
|
+
"""
|
|
54
|
+
body: dict[str, Any] = {"name": name}
|
|
55
|
+
if description is not None:
|
|
56
|
+
body["description"] = description
|
|
57
|
+
if source_ids is not None:
|
|
58
|
+
body["source_ids"] = source_ids
|
|
59
|
+
if row_filters is not None:
|
|
60
|
+
body["row_filters"] = row_filters
|
|
61
|
+
resp = self._http.post("/access/policies", json=body)
|
|
62
|
+
return Policy.model_validate(resp.json())
|
|
63
|
+
|
|
64
|
+
def get(self, policy_id: str) -> Policy:
|
|
65
|
+
"""Get policy details including assigned user IDs.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
policy_id: The policy UUID.
|
|
69
|
+
"""
|
|
70
|
+
resp = self._http.get(f"/access/policies/{policy_id}")
|
|
71
|
+
return Policy.model_validate(resp.json())
|
|
72
|
+
|
|
73
|
+
def list(
|
|
74
|
+
self,
|
|
75
|
+
*,
|
|
76
|
+
name: str | None = None,
|
|
77
|
+
limit: int = 25,
|
|
78
|
+
after: str | None = None,
|
|
79
|
+
) -> SyncCursorPage[Policy]:
|
|
80
|
+
"""List access policies for the organization with cursor pagination.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
name: Filter by exact policy name.
|
|
84
|
+
limit: Maximum number of policies per page (1-200).
|
|
85
|
+
after: Cursor for the next page.
|
|
86
|
+
"""
|
|
87
|
+
params: dict[str, Any] = {"limit": limit}
|
|
88
|
+
if name is not None:
|
|
89
|
+
params["name"] = name
|
|
90
|
+
if after is not None:
|
|
91
|
+
params["after"] = after
|
|
92
|
+
return SyncCursorPage(self._http, "/access/policies", Policy, params=params)
|
|
93
|
+
|
|
94
|
+
def update(
|
|
95
|
+
self,
|
|
96
|
+
policy_id: str,
|
|
97
|
+
*,
|
|
98
|
+
name: str | None = None,
|
|
99
|
+
description: str | None = None,
|
|
100
|
+
source_ids: builtins.list[str] | None = None,
|
|
101
|
+
row_filters: builtins.list[dict[str, Any]] | None = None,
|
|
102
|
+
) -> PolicyUpdateResponse:
|
|
103
|
+
"""Update an access policy.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
policy_id: The policy UUID.
|
|
107
|
+
name: New name.
|
|
108
|
+
description: New description.
|
|
109
|
+
source_ids: New source IDs.
|
|
110
|
+
row_filters: New row filters.
|
|
111
|
+
"""
|
|
112
|
+
body: dict[str, Any] = {}
|
|
113
|
+
if name is not None:
|
|
114
|
+
body["name"] = name
|
|
115
|
+
if description is not None:
|
|
116
|
+
body["description"] = description
|
|
117
|
+
if source_ids is not None:
|
|
118
|
+
body["source_ids"] = source_ids
|
|
119
|
+
if row_filters is not None:
|
|
120
|
+
body["row_filters"] = row_filters
|
|
121
|
+
resp = self._http.patch(f"/access/policies/{policy_id}", json=body)
|
|
122
|
+
return PolicyUpdateResponse.model_validate(resp.json())
|
|
123
|
+
|
|
124
|
+
def delete(self, policy_id: str) -> PolicyDeleteResponse:
|
|
125
|
+
"""Delete an access policy and all its user assignments.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
policy_id: The policy UUID.
|
|
129
|
+
"""
|
|
130
|
+
resp = self._http.delete(f"/access/policies/{policy_id}")
|
|
131
|
+
return PolicyDeleteResponse.model_validate(resp.json())
|
|
132
|
+
|
|
133
|
+
def assign_users(
|
|
134
|
+
self,
|
|
135
|
+
policy_id: str,
|
|
136
|
+
*,
|
|
137
|
+
user_ids: builtins.list[str],
|
|
138
|
+
) -> PolicyAssignResponse:
|
|
139
|
+
"""Assign users to a policy.
|
|
140
|
+
|
|
141
|
+
Idempotent: already-assigned users are silently skipped.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
policy_id: The policy UUID.
|
|
145
|
+
user_ids: List of WorkOS user IDs to assign.
|
|
146
|
+
"""
|
|
147
|
+
resp = self._http.post(
|
|
148
|
+
f"/access/policies/{policy_id}/users",
|
|
149
|
+
json={"user_ids": user_ids},
|
|
150
|
+
)
|
|
151
|
+
return PolicyAssignResponse.model_validate(resp.json())
|
|
152
|
+
|
|
153
|
+
def remove_user(
|
|
154
|
+
self,
|
|
155
|
+
policy_id: str,
|
|
156
|
+
user_id: str,
|
|
157
|
+
) -> PolicyRemoveUserResponse:
|
|
158
|
+
"""Remove a user from a policy.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
policy_id: The policy UUID.
|
|
162
|
+
user_id: The WorkOS user ID to remove.
|
|
163
|
+
"""
|
|
164
|
+
resp = self._http.delete(f"/access/policies/{policy_id}/users/{user_id}")
|
|
165
|
+
return PolicyRemoveUserResponse.model_validate(resp.json())
|
|
166
|
+
|
|
167
|
+
def resolve(
|
|
168
|
+
self,
|
|
169
|
+
*,
|
|
170
|
+
user_id: str,
|
|
171
|
+
source_id: str,
|
|
172
|
+
) -> ResolvedAccess:
|
|
173
|
+
"""Preview resolved access for a user+source combination.
|
|
174
|
+
|
|
175
|
+
Returns the effective row filters and SQL WHERE clause that would
|
|
176
|
+
be applied when querying the given source as the given user.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
user_id: WorkOS user ID.
|
|
180
|
+
source_id: Source UUID.
|
|
181
|
+
"""
|
|
182
|
+
resp = self._http.post(
|
|
183
|
+
"/access/resolve",
|
|
184
|
+
json={"user_id": user_id, "source_id": source_id},
|
|
185
|
+
)
|
|
186
|
+
return ResolvedAccess.model_validate(resp.json())
|
|
187
|
+
|
|
188
|
+
def columns(
|
|
189
|
+
self,
|
|
190
|
+
*,
|
|
191
|
+
source_id: str | None = None,
|
|
192
|
+
) -> builtins.list[SourceColumns]:
|
|
193
|
+
"""Discover filterable columns for RLS rule building.
|
|
194
|
+
|
|
195
|
+
Returns column names and types from data sources so you can
|
|
196
|
+
construct valid ``row_filters`` for access policies.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
source_id: Optional source ID to filter to a single source.
|
|
200
|
+
"""
|
|
201
|
+
params: dict[str, Any] = {}
|
|
202
|
+
if source_id is not None:
|
|
203
|
+
params["source_id"] = source_id
|
|
204
|
+
resp = self._http.get("/access/columns", params=params)
|
|
205
|
+
body = resp.json()
|
|
206
|
+
return [SourceColumns.model_validate(s) for s in body.get("data", [])]
|
|
207
|
+
|
|
208
|
+
def replace_user_policies(
|
|
209
|
+
self, user_id: str, *, policy_ids: builtins.list[str]
|
|
210
|
+
) -> PolicyReplaceResponse:
|
|
211
|
+
"""Atomically replace all policy assignments for a user.
|
|
212
|
+
|
|
213
|
+
Removes any existing policy assignments and replaces them with the
|
|
214
|
+
provided set. This prevents policy accumulation from repeated
|
|
215
|
+
``get_session()`` calls.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
user_id: The user ID whose policies to replace.
|
|
219
|
+
policy_ids: Complete set of policy IDs to assign. Pass ``[]`` to
|
|
220
|
+
remove all policies.
|
|
221
|
+
"""
|
|
222
|
+
resp = self._http.put(
|
|
223
|
+
f"/access/users/{user_id}/policies",
|
|
224
|
+
json={"policy_ids": policy_ids},
|
|
225
|
+
)
|
|
226
|
+
return PolicyReplaceResponse.model_validate(resp.json())
|
|
227
|
+
|
|
228
|
+
def setup(
|
|
229
|
+
self,
|
|
230
|
+
*,
|
|
231
|
+
name: str,
|
|
232
|
+
sources: builtins.list[str] | None = None,
|
|
233
|
+
row_filters: dict[str, Any] | None = None,
|
|
234
|
+
users: builtins.list[str] | None = None,
|
|
235
|
+
description: str | None = None,
|
|
236
|
+
) -> Policy:
|
|
237
|
+
"""Create a policy and assign users in one call.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
name: Policy name.
|
|
241
|
+
sources: Source IDs this policy applies to.
|
|
242
|
+
row_filters: Dict of column -> values for row filtering.
|
|
243
|
+
users: User IDs to assign to the policy.
|
|
244
|
+
description: Optional policy description.
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
Created Policy object.
|
|
248
|
+
"""
|
|
249
|
+
return sync_setup_policy(
|
|
250
|
+
self._http,
|
|
251
|
+
name=name,
|
|
252
|
+
sources=sources,
|
|
253
|
+
row_filters=row_filters,
|
|
254
|
+
users=users,
|
|
255
|
+
description=description,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
class AsyncPolicies:
|
|
260
|
+
"""Asynchronous access policy management.
|
|
261
|
+
|
|
262
|
+
Usage::
|
|
263
|
+
|
|
264
|
+
policy = await client.policies.create(
|
|
265
|
+
name="Sales Team",
|
|
266
|
+
source_ids=["src_..."],
|
|
267
|
+
row_filters=[{"column": "region", "values": ["US"]}],
|
|
268
|
+
)
|
|
269
|
+
"""
|
|
270
|
+
|
|
271
|
+
def __init__(self, http: AsyncHTTPClient) -> None:
|
|
272
|
+
self._http = http
|
|
273
|
+
|
|
274
|
+
async def create(
|
|
275
|
+
self,
|
|
276
|
+
*,
|
|
277
|
+
name: str,
|
|
278
|
+
description: str | None = None,
|
|
279
|
+
source_ids: builtins.list[str] | None = None,
|
|
280
|
+
row_filters: builtins.list[dict[str, Any]] | None = None,
|
|
281
|
+
) -> Policy:
|
|
282
|
+
"""Create an access policy.
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
name: Policy name.
|
|
286
|
+
description: Optional description.
|
|
287
|
+
source_ids: List of source IDs this policy applies to.
|
|
288
|
+
row_filters: List of row filter dicts with ``column`` and ``values`` keys.
|
|
289
|
+
"""
|
|
290
|
+
body: dict[str, Any] = {"name": name}
|
|
291
|
+
if description is not None:
|
|
292
|
+
body["description"] = description
|
|
293
|
+
if source_ids is not None:
|
|
294
|
+
body["source_ids"] = source_ids
|
|
295
|
+
if row_filters is not None:
|
|
296
|
+
body["row_filters"] = row_filters
|
|
297
|
+
resp = await self._http.post("/access/policies", json=body)
|
|
298
|
+
return Policy.model_validate(resp.json())
|
|
299
|
+
|
|
300
|
+
async def get(self, policy_id: str) -> Policy:
|
|
301
|
+
"""Get policy details including assigned user IDs.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
policy_id: The policy UUID.
|
|
305
|
+
"""
|
|
306
|
+
resp = await self._http.get(f"/access/policies/{policy_id}")
|
|
307
|
+
return Policy.model_validate(resp.json())
|
|
308
|
+
|
|
309
|
+
def list(
|
|
310
|
+
self,
|
|
311
|
+
*,
|
|
312
|
+
name: str | None = None,
|
|
313
|
+
limit: int = 25,
|
|
314
|
+
after: str | None = None,
|
|
315
|
+
) -> AsyncCursorPage[Policy]:
|
|
316
|
+
"""List access policies for the organization with cursor pagination.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
name: Filter by exact policy name.
|
|
320
|
+
limit: Maximum number of policies per page (1-200).
|
|
321
|
+
after: Cursor for the next page.
|
|
322
|
+
"""
|
|
323
|
+
params: dict[str, Any] = {"limit": limit}
|
|
324
|
+
if name is not None:
|
|
325
|
+
params["name"] = name
|
|
326
|
+
if after is not None:
|
|
327
|
+
params["after"] = after
|
|
328
|
+
return AsyncCursorPage(self._http, "/access/policies", Policy, params=params)
|
|
329
|
+
|
|
330
|
+
async def update(
|
|
331
|
+
self,
|
|
332
|
+
policy_id: str,
|
|
333
|
+
*,
|
|
334
|
+
name: str | None = None,
|
|
335
|
+
description: str | None = None,
|
|
336
|
+
source_ids: builtins.list[str] | None = None,
|
|
337
|
+
row_filters: builtins.list[dict[str, Any]] | None = None,
|
|
338
|
+
) -> PolicyUpdateResponse:
|
|
339
|
+
"""Update an access policy.
|
|
340
|
+
|
|
341
|
+
Args:
|
|
342
|
+
policy_id: The policy UUID.
|
|
343
|
+
name: New name.
|
|
344
|
+
description: New description.
|
|
345
|
+
source_ids: New source IDs.
|
|
346
|
+
row_filters: New row filters.
|
|
347
|
+
"""
|
|
348
|
+
body: dict[str, Any] = {}
|
|
349
|
+
if name is not None:
|
|
350
|
+
body["name"] = name
|
|
351
|
+
if description is not None:
|
|
352
|
+
body["description"] = description
|
|
353
|
+
if source_ids is not None:
|
|
354
|
+
body["source_ids"] = source_ids
|
|
355
|
+
if row_filters is not None:
|
|
356
|
+
body["row_filters"] = row_filters
|
|
357
|
+
resp = await self._http.patch(f"/access/policies/{policy_id}", json=body)
|
|
358
|
+
return PolicyUpdateResponse.model_validate(resp.json())
|
|
359
|
+
|
|
360
|
+
async def delete(self, policy_id: str) -> PolicyDeleteResponse:
|
|
361
|
+
"""Delete an access policy and all its user assignments.
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
policy_id: The policy UUID.
|
|
365
|
+
"""
|
|
366
|
+
resp = await self._http.delete(f"/access/policies/{policy_id}")
|
|
367
|
+
return PolicyDeleteResponse.model_validate(resp.json())
|
|
368
|
+
|
|
369
|
+
async def assign_users(
|
|
370
|
+
self,
|
|
371
|
+
policy_id: str,
|
|
372
|
+
*,
|
|
373
|
+
user_ids: builtins.list[str],
|
|
374
|
+
) -> PolicyAssignResponse:
|
|
375
|
+
"""Assign users to a policy.
|
|
376
|
+
|
|
377
|
+
Idempotent: already-assigned users are silently skipped.
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
policy_id: The policy UUID.
|
|
381
|
+
user_ids: List of WorkOS user IDs to assign.
|
|
382
|
+
"""
|
|
383
|
+
resp = await self._http.post(
|
|
384
|
+
f"/access/policies/{policy_id}/users",
|
|
385
|
+
json={"user_ids": user_ids},
|
|
386
|
+
)
|
|
387
|
+
return PolicyAssignResponse.model_validate(resp.json())
|
|
388
|
+
|
|
389
|
+
async def remove_user(
|
|
390
|
+
self,
|
|
391
|
+
policy_id: str,
|
|
392
|
+
user_id: str,
|
|
393
|
+
) -> PolicyRemoveUserResponse:
|
|
394
|
+
"""Remove a user from a policy.
|
|
395
|
+
|
|
396
|
+
Args:
|
|
397
|
+
policy_id: The policy UUID.
|
|
398
|
+
user_id: The WorkOS user ID to remove.
|
|
399
|
+
"""
|
|
400
|
+
resp = await self._http.delete(f"/access/policies/{policy_id}/users/{user_id}")
|
|
401
|
+
return PolicyRemoveUserResponse.model_validate(resp.json())
|
|
402
|
+
|
|
403
|
+
async def resolve(
|
|
404
|
+
self,
|
|
405
|
+
*,
|
|
406
|
+
user_id: str,
|
|
407
|
+
source_id: str,
|
|
408
|
+
) -> ResolvedAccess:
|
|
409
|
+
"""Preview resolved access for a user+source combination.
|
|
410
|
+
|
|
411
|
+
Returns the effective row filters and SQL WHERE clause that would
|
|
412
|
+
be applied when querying the given source as the given user.
|
|
413
|
+
|
|
414
|
+
Args:
|
|
415
|
+
user_id: WorkOS user ID.
|
|
416
|
+
source_id: Source UUID.
|
|
417
|
+
"""
|
|
418
|
+
resp = await self._http.post(
|
|
419
|
+
"/access/resolve",
|
|
420
|
+
json={"user_id": user_id, "source_id": source_id},
|
|
421
|
+
)
|
|
422
|
+
return ResolvedAccess.model_validate(resp.json())
|
|
423
|
+
|
|
424
|
+
async def columns(
|
|
425
|
+
self,
|
|
426
|
+
*,
|
|
427
|
+
source_id: str | None = None,
|
|
428
|
+
) -> builtins.list[SourceColumns]:
|
|
429
|
+
"""Discover filterable columns for RLS rule building.
|
|
430
|
+
|
|
431
|
+
Returns column names and types from data sources so you can
|
|
432
|
+
construct valid ``row_filters`` for access policies.
|
|
433
|
+
|
|
434
|
+
Args:
|
|
435
|
+
source_id: Optional source ID to filter to a single source.
|
|
436
|
+
"""
|
|
437
|
+
params: dict[str, Any] = {}
|
|
438
|
+
if source_id is not None:
|
|
439
|
+
params["source_id"] = source_id
|
|
440
|
+
resp = await self._http.get("/access/columns", params=params)
|
|
441
|
+
body = resp.json()
|
|
442
|
+
return [SourceColumns.model_validate(s) for s in body.get("data", [])]
|
|
443
|
+
|
|
444
|
+
async def replace_user_policies(
|
|
445
|
+
self, user_id: str, *, policy_ids: builtins.list[str]
|
|
446
|
+
) -> PolicyReplaceResponse:
|
|
447
|
+
"""Atomically replace all policy assignments for a user.
|
|
448
|
+
|
|
449
|
+
Removes any existing policy assignments and replaces them with the
|
|
450
|
+
provided set. This prevents policy accumulation from repeated
|
|
451
|
+
``get_session()`` calls.
|
|
452
|
+
|
|
453
|
+
Args:
|
|
454
|
+
user_id: The user ID whose policies to replace.
|
|
455
|
+
policy_ids: Complete set of policy IDs to assign. Pass ``[]`` to
|
|
456
|
+
remove all policies.
|
|
457
|
+
"""
|
|
458
|
+
resp = await self._http.put(
|
|
459
|
+
f"/access/users/{user_id}/policies",
|
|
460
|
+
json={"policy_ids": policy_ids},
|
|
461
|
+
)
|
|
462
|
+
return PolicyReplaceResponse.model_validate(resp.json())
|
|
463
|
+
|
|
464
|
+
async def setup(
|
|
465
|
+
self,
|
|
466
|
+
*,
|
|
467
|
+
name: str,
|
|
468
|
+
sources: builtins.list[str] | None = None,
|
|
469
|
+
row_filters: dict[str, Any] | None = None,
|
|
470
|
+
users: builtins.list[str] | None = None,
|
|
471
|
+
description: str | None = None,
|
|
472
|
+
) -> Policy:
|
|
473
|
+
"""Create a policy and assign users in one call.
|
|
474
|
+
|
|
475
|
+
Args:
|
|
476
|
+
name: Policy name.
|
|
477
|
+
sources: Source IDs this policy applies to.
|
|
478
|
+
row_filters: Dict of column -> values for row filtering.
|
|
479
|
+
users: User IDs to assign to the policy.
|
|
480
|
+
description: Optional policy description.
|
|
481
|
+
|
|
482
|
+
Returns:
|
|
483
|
+
Created Policy object.
|
|
484
|
+
"""
|
|
485
|
+
return await async_setup_policy(
|
|
486
|
+
self._http,
|
|
487
|
+
name=name,
|
|
488
|
+
sources=sources,
|
|
489
|
+
row_filters=row_filters,
|
|
490
|
+
users=users,
|
|
491
|
+
description=description,
|
|
492
|
+
)
|