provablyfine-client 0.3.0__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.
- provablyfine_client/__init__.py +28 -0
- provablyfine_client/_base64url.py +9 -0
- provablyfine_client/account_client.py +31 -0
- provablyfine_client/aio.py +324 -0
- provablyfine_client/directory.py +88 -0
- provablyfine_client/exceptions.py +11 -0
- provablyfine_client/http_session.py +105 -0
- provablyfine_client/http_signatures.py +82 -0
- provablyfine_client/invitation_client.py +32 -0
- provablyfine_client/public_client.py +61 -0
- provablyfine_client/py.typed +0 -0
- provablyfine_client/schemas.py +628 -0
- provablyfine_client/session_client.py +619 -0
- provablyfine_client/signer.py +43 -0
- provablyfine_client-0.3.0.dist-info/METADATA +25 -0
- provablyfine_client-0.3.0.dist-info/RECORD +18 -0
- provablyfine_client-0.3.0.dist-info/WHEEL +4 -0
- provablyfine_client-0.3.0.dist-info/licenses/LICENSE +20 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from . import exceptions, schemas
|
|
2
|
+
from .account_client import AccountClient
|
|
3
|
+
from .aio import AsyncAccountClient, AsyncInvitationClient, AsyncPublicClient, AsyncSessionClient
|
|
4
|
+
from .directory import Directory
|
|
5
|
+
from .http_session import HttpSession
|
|
6
|
+
from .http_signatures import Auth
|
|
7
|
+
from .invitation_client import InvitationClient
|
|
8
|
+
from .public_client import PublicClient
|
|
9
|
+
from .session_client import SessionClient
|
|
10
|
+
from .signer import HmacSigner, Signer
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"AccountClient",
|
|
14
|
+
"AsyncAccountClient",
|
|
15
|
+
"AsyncInvitationClient",
|
|
16
|
+
"AsyncPublicClient",
|
|
17
|
+
"AsyncSessionClient",
|
|
18
|
+
"Auth",
|
|
19
|
+
"Directory",
|
|
20
|
+
"HmacSigner",
|
|
21
|
+
"HttpSession",
|
|
22
|
+
"InvitationClient",
|
|
23
|
+
"PublicClient",
|
|
24
|
+
"SessionClient",
|
|
25
|
+
"Signer",
|
|
26
|
+
"exceptions",
|
|
27
|
+
"schemas",
|
|
28
|
+
]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typing
|
|
4
|
+
|
|
5
|
+
from . import directory, exceptions, http_session, http_signatures, signer
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AccountClient:
|
|
9
|
+
"""API methods that require account + session authentication."""
|
|
10
|
+
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
session: http_session.HttpSession,
|
|
14
|
+
_directory: directory.Directory,
|
|
15
|
+
account_signer: signer.Signer,
|
|
16
|
+
session_signer: signer.Signer,
|
|
17
|
+
) -> None:
|
|
18
|
+
self._session = session
|
|
19
|
+
self._directory = _directory
|
|
20
|
+
self._account_signer = account_signer
|
|
21
|
+
self._session_signer = session_signer
|
|
22
|
+
|
|
23
|
+
def login_http_sig(self, session_public_key: dict[str, typing.Any]) -> None:
|
|
24
|
+
auth = http_signatures.Auth([self._account_signer, self._session_signer])
|
|
25
|
+
response = self._session.post(
|
|
26
|
+
self._directory.login,
|
|
27
|
+
auth=auth,
|
|
28
|
+
json={"session_public_key": session_public_key},
|
|
29
|
+
)
|
|
30
|
+
if response.status_code != 204:
|
|
31
|
+
raise exceptions.UI(f"Unable to login successfully: {response.text}")
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import typing
|
|
5
|
+
|
|
6
|
+
from . import account_client, invitation_client, public_client, schemas, session_client, signer
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AsyncPublicClient:
|
|
10
|
+
def __init__(self, inner: public_client.PublicClient) -> None:
|
|
11
|
+
self._inner = inner
|
|
12
|
+
self._lock = asyncio.Lock()
|
|
13
|
+
|
|
14
|
+
async def _run(self, fn: typing.Callable[[], typing.Any]) -> typing.Any:
|
|
15
|
+
async with self._lock:
|
|
16
|
+
return await asyncio.to_thread(fn)
|
|
17
|
+
|
|
18
|
+
async def get_user_trusted_keys_public(self) -> str:
|
|
19
|
+
return await self._run(self._inner.get_user_trusted_keys_public)
|
|
20
|
+
|
|
21
|
+
async def get_public_auth(self, auth_name: str) -> schemas.AuthPublic:
|
|
22
|
+
return await self._run(lambda: self._inner.get_public_auth(auth_name))
|
|
23
|
+
|
|
24
|
+
async def list_public_auths(self) -> list[schemas.AuthPublicSummary]:
|
|
25
|
+
return await self._run(self._inner.list_public_auths)
|
|
26
|
+
|
|
27
|
+
async def initialize(self, account_signer: signer.Signer, account_public_key: dict[str, typing.Any]) -> None:
|
|
28
|
+
return await self._run(lambda: self._inner.initialize(account_signer, account_public_key))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class AsyncSessionClient:
|
|
32
|
+
def __init__(self, inner: session_client.SessionClient) -> None:
|
|
33
|
+
self._inner = inner
|
|
34
|
+
self._lock = asyncio.Lock()
|
|
35
|
+
|
|
36
|
+
async def _run(self, fn: typing.Callable[[], typing.Any]) -> typing.Any:
|
|
37
|
+
async with self._lock:
|
|
38
|
+
return await asyncio.to_thread(fn)
|
|
39
|
+
|
|
40
|
+
async def list_ssh_hosts(self) -> schemas.SshHostsResponse:
|
|
41
|
+
return await self._run(self._inner.list_ssh_hosts)
|
|
42
|
+
|
|
43
|
+
async def get_host_trusted_keys(self) -> str:
|
|
44
|
+
return await self._run(self._inner.get_host_trusted_keys)
|
|
45
|
+
|
|
46
|
+
async def get_user_certificate(
|
|
47
|
+
self,
|
|
48
|
+
hostname: str,
|
|
49
|
+
username: str,
|
|
50
|
+
action: str,
|
|
51
|
+
public_key: dict[str, typing.Any],
|
|
52
|
+
command: str | None = None,
|
|
53
|
+
) -> schemas.SshUserCertificateResponse:
|
|
54
|
+
return await self._run(
|
|
55
|
+
lambda: self._inner.get_user_certificate(hostname, username, action, public_key, command)
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
async def sign_host_certificates(
|
|
59
|
+
self, public_keys: list[dict[str, typing.Any]]
|
|
60
|
+
) -> schemas.SshHostCertificateResponse:
|
|
61
|
+
return await self._run(lambda: self._inner.sign_host_certificates(public_keys))
|
|
62
|
+
|
|
63
|
+
async def list_self_bastions(self) -> schemas.IdentitySelfBastionListResponse:
|
|
64
|
+
return await self._run(self._inner.list_self_bastions)
|
|
65
|
+
|
|
66
|
+
async def get_self_token(self, service: str) -> schemas.IdentitySelfTokenResponse:
|
|
67
|
+
return await self._run(lambda: self._inner.get_self_token(service))
|
|
68
|
+
|
|
69
|
+
async def list_tags(
|
|
70
|
+
self,
|
|
71
|
+
id: int | None = None,
|
|
72
|
+
name: str | None = None,
|
|
73
|
+
value: str | None = None,
|
|
74
|
+
) -> schemas.TagsResponse:
|
|
75
|
+
return await self._run(lambda: self._inner.list_tags(id, name, value))
|
|
76
|
+
|
|
77
|
+
async def create_tag(self, name: str, value: str) -> schemas.Tag:
|
|
78
|
+
return await self._run(lambda: self._inner.create_tag(name, value))
|
|
79
|
+
|
|
80
|
+
async def delete_tag(self, id: int) -> None:
|
|
81
|
+
return await self._run(lambda: self._inner.delete_tag(id))
|
|
82
|
+
|
|
83
|
+
async def list_tenants(self, id: int | None = None) -> schemas.TenantsResponse:
|
|
84
|
+
return await self._run(lambda: self._inner.list_tenants(id))
|
|
85
|
+
|
|
86
|
+
async def get_tenant(self, id: int) -> schemas.Tenant:
|
|
87
|
+
return await self._run(lambda: self._inner.get_tenant(id))
|
|
88
|
+
|
|
89
|
+
async def create_tenant(self, name: str, display_name: str) -> schemas.Tenant:
|
|
90
|
+
return await self._run(lambda: self._inner.create_tenant(name, display_name))
|
|
91
|
+
|
|
92
|
+
async def update_tenant(
|
|
93
|
+
self,
|
|
94
|
+
id: int,
|
|
95
|
+
display_name: str | None = None,
|
|
96
|
+
is_enabled: bool | None = None,
|
|
97
|
+
) -> None:
|
|
98
|
+
return await self._run(lambda: self._inner.update_tenant(id, display_name, is_enabled))
|
|
99
|
+
|
|
100
|
+
async def delete_tenant(self, id: int) -> None:
|
|
101
|
+
return await self._run(lambda: self._inner.delete_tenant(id))
|
|
102
|
+
|
|
103
|
+
async def list_auths(self) -> schemas.AuthListResponse:
|
|
104
|
+
return await self._run(self._inner.list_auths)
|
|
105
|
+
|
|
106
|
+
async def get_auth(self, id: int) -> schemas.Auth:
|
|
107
|
+
return await self._run(lambda: self._inner.get_auth(id))
|
|
108
|
+
|
|
109
|
+
async def create_auth_http_sig(self, name: str, description: str, tags: list[dict[str, str]]) -> schemas.Auth:
|
|
110
|
+
return await self._run(lambda: self._inner.create_auth_http_sig(name, description, tags))
|
|
111
|
+
|
|
112
|
+
async def create_auth_oidc(
|
|
113
|
+
self,
|
|
114
|
+
name: str,
|
|
115
|
+
description: str,
|
|
116
|
+
tags: list[dict[str, str]],
|
|
117
|
+
issuer: str,
|
|
118
|
+
client_id: str,
|
|
119
|
+
client_secret: str | None,
|
|
120
|
+
) -> schemas.Auth:
|
|
121
|
+
return await self._run(
|
|
122
|
+
lambda: self._inner.create_auth_oidc(name, description, tags, issuer, client_id, client_secret)
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
async def create_auth_oauth2_github(
|
|
126
|
+
self,
|
|
127
|
+
name: str,
|
|
128
|
+
description: str,
|
|
129
|
+
tags: list[dict[str, str]],
|
|
130
|
+
client_id: str,
|
|
131
|
+
client_secret: str,
|
|
132
|
+
) -> schemas.Auth:
|
|
133
|
+
return await self._run(
|
|
134
|
+
lambda: self._inner.create_auth_oauth2_github(name, description, tags, client_id, client_secret)
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
async def update_auth(
|
|
138
|
+
self,
|
|
139
|
+
id: int,
|
|
140
|
+
name: str | None = None,
|
|
141
|
+
description: str | None = None,
|
|
142
|
+
is_enabled: bool | None = None,
|
|
143
|
+
tags: list[schemas.TagNameValue] | None = None,
|
|
144
|
+
) -> None:
|
|
145
|
+
return await self._run(lambda: self._inner.update_auth(id, name, description, is_enabled, tags))
|
|
146
|
+
|
|
147
|
+
async def delete_auth(self, id: int) -> None:
|
|
148
|
+
return await self._run(lambda: self._inner.delete_auth(id))
|
|
149
|
+
|
|
150
|
+
async def list_bastions(self, id: int | None = None) -> schemas.BastionListResponse:
|
|
151
|
+
return await self._run(lambda: self._inner.list_bastions(id))
|
|
152
|
+
|
|
153
|
+
async def get_bastion(self, id: int) -> schemas.Bastion:
|
|
154
|
+
return await self._run(lambda: self._inner.get_bastion(id))
|
|
155
|
+
|
|
156
|
+
async def create_bastion(
|
|
157
|
+
self,
|
|
158
|
+
url: str,
|
|
159
|
+
ssh_proxy_jump: str | None,
|
|
160
|
+
tag_id_list: list[int],
|
|
161
|
+
tag_name_value_list: list[dict[str, str]],
|
|
162
|
+
) -> schemas.Bastion:
|
|
163
|
+
return await self._run(
|
|
164
|
+
lambda: self._inner.create_bastion(url, ssh_proxy_jump, tag_id_list, tag_name_value_list)
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
async def update_bastion(
|
|
168
|
+
self,
|
|
169
|
+
id: int,
|
|
170
|
+
url: str | None = None,
|
|
171
|
+
ssh_proxy_jump: str | None = None,
|
|
172
|
+
tag_id_list: list[int] | None = None,
|
|
173
|
+
tag_name_value_list: list[schemas.TagNameValue] | None = None,
|
|
174
|
+
) -> None:
|
|
175
|
+
return await self._run(
|
|
176
|
+
lambda: self._inner.update_bastion(id, url, ssh_proxy_jump, tag_id_list, tag_name_value_list)
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
async def delete_bastion(self, id: int) -> None:
|
|
180
|
+
return await self._run(lambda: self._inner.delete_bastion(id))
|
|
181
|
+
|
|
182
|
+
async def list_roles(self, id: int | None = None, name: str | None = None) -> schemas.RolesResponse:
|
|
183
|
+
return await self._run(lambda: self._inner.list_roles(id, name))
|
|
184
|
+
|
|
185
|
+
async def get_role(self, id: int) -> schemas.Role:
|
|
186
|
+
return await self._run(lambda: self._inner.get_role(id))
|
|
187
|
+
|
|
188
|
+
async def create_role(self, name: str, description: str) -> schemas.Role:
|
|
189
|
+
return await self._run(lambda: self._inner.create_role(name, description))
|
|
190
|
+
|
|
191
|
+
async def update_role(
|
|
192
|
+
self,
|
|
193
|
+
id: int,
|
|
194
|
+
name: str | None = None,
|
|
195
|
+
description: str | None = None,
|
|
196
|
+
grant_list: list[schemas.Grant] | None = None,
|
|
197
|
+
member_list: list[schemas.RoleMemberRef] | None = None,
|
|
198
|
+
) -> None:
|
|
199
|
+
return await self._run(lambda: self._inner.update_role(id, name, description, grant_list, member_list))
|
|
200
|
+
|
|
201
|
+
async def delete_role(self, id: int) -> None:
|
|
202
|
+
return await self._run(lambda: self._inner.delete_role(id))
|
|
203
|
+
|
|
204
|
+
async def list_boundaries(self, id: int | None = None, name: str | None = None) -> schemas.BoundariesResponse:
|
|
205
|
+
return await self._run(lambda: self._inner.list_boundaries(id, name))
|
|
206
|
+
|
|
207
|
+
async def get_boundary(self, id: int) -> schemas.Boundary:
|
|
208
|
+
return await self._run(lambda: self._inner.get_boundary(id))
|
|
209
|
+
|
|
210
|
+
async def create_boundary(self, name: str, description: str) -> schemas.Boundary:
|
|
211
|
+
return await self._run(lambda: self._inner.create_boundary(name, description))
|
|
212
|
+
|
|
213
|
+
async def update_boundary(
|
|
214
|
+
self,
|
|
215
|
+
id: int,
|
|
216
|
+
name: str | None = None,
|
|
217
|
+
description: str | None = None,
|
|
218
|
+
ceiling_list: list[schemas.Grant] | None = None,
|
|
219
|
+
denied_list: list[schemas.Grant] | None = None,
|
|
220
|
+
) -> None:
|
|
221
|
+
return await self._run(lambda: self._inner.update_boundary(id, name, description, ceiling_list, denied_list))
|
|
222
|
+
|
|
223
|
+
async def delete_boundary(self, id: int) -> None:
|
|
224
|
+
return await self._run(lambda: self._inner.delete_boundary(id))
|
|
225
|
+
|
|
226
|
+
async def list_identities(
|
|
227
|
+
self,
|
|
228
|
+
id: int | None = None,
|
|
229
|
+
name: str | None = None,
|
|
230
|
+
tag_id: list[str] | None = None,
|
|
231
|
+
tag_name: list[str] | None = None,
|
|
232
|
+
boundary_id: list[str] | None = None,
|
|
233
|
+
boundary_name: list[str] | None = None,
|
|
234
|
+
) -> schemas.IdentitiesResponse:
|
|
235
|
+
return await self._run(
|
|
236
|
+
lambda: self._inner.list_identities(id, name, tag_id, tag_name, boundary_id, boundary_name)
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
async def get_identity(self, id: int) -> schemas.Identity:
|
|
240
|
+
return await self._run(lambda: self._inner.get_identity(id))
|
|
241
|
+
|
|
242
|
+
async def create_identity(
|
|
243
|
+
self,
|
|
244
|
+
name: str | None,
|
|
245
|
+
boundary_id_list: list[int],
|
|
246
|
+
boundary_name_list: list[str],
|
|
247
|
+
tag_id_list: list[int],
|
|
248
|
+
tag_name_value_list: list[dict[str, str]],
|
|
249
|
+
) -> schemas.Identity:
|
|
250
|
+
return await self._run(
|
|
251
|
+
lambda: self._inner.create_identity(
|
|
252
|
+
name, boundary_id_list, boundary_name_list, tag_id_list, tag_name_value_list
|
|
253
|
+
)
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
async def invite_identity(self, id: int, delivery: str) -> str | None:
|
|
257
|
+
return await self._run(lambda: self._inner.invite_identity(id, delivery))
|
|
258
|
+
|
|
259
|
+
async def delete_identity(self, id: int) -> None:
|
|
260
|
+
return await self._run(lambda: self._inner.delete_identity(id))
|
|
261
|
+
|
|
262
|
+
async def update_identity(
|
|
263
|
+
self,
|
|
264
|
+
id: int,
|
|
265
|
+
name: str | None = None,
|
|
266
|
+
tags: list[schemas.IdentityTagOp] | None = None,
|
|
267
|
+
) -> None:
|
|
268
|
+
return await self._run(lambda: self._inner.update_identity(id, name, tags))
|
|
269
|
+
|
|
270
|
+
async def login_oidc(
|
|
271
|
+
self,
|
|
272
|
+
auth_name: str,
|
|
273
|
+
id_token: str,
|
|
274
|
+
session_public_key: dict[str, typing.Any],
|
|
275
|
+
) -> None:
|
|
276
|
+
return await self._run(lambda: self._inner.login_oidc(auth_name, id_token, session_public_key))
|
|
277
|
+
|
|
278
|
+
async def login_oauth2_start(
|
|
279
|
+
self,
|
|
280
|
+
auth_name: str,
|
|
281
|
+
session_public_key: dict[str, typing.Any],
|
|
282
|
+
client_redirect_uri: str,
|
|
283
|
+
) -> str:
|
|
284
|
+
return await self._run(
|
|
285
|
+
lambda: self._inner.login_oauth2_start(auth_name, session_public_key, client_redirect_uri)
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
async def list_audit_log(
|
|
289
|
+
self,
|
|
290
|
+
level: int | None = None,
|
|
291
|
+
object_type: str | None = None,
|
|
292
|
+
by_identity_id: str | None = None,
|
|
293
|
+
start_time: int | None = None,
|
|
294
|
+
end_time: int | None = None,
|
|
295
|
+
) -> schemas.AuditLogListResponse:
|
|
296
|
+
return await self._run(
|
|
297
|
+
lambda: self._inner.list_audit_log(level, object_type, by_identity_id, start_time, end_time)
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
class AsyncAccountClient:
|
|
302
|
+
def __init__(self, inner: account_client.AccountClient) -> None:
|
|
303
|
+
self._inner = inner
|
|
304
|
+
self._lock = asyncio.Lock()
|
|
305
|
+
|
|
306
|
+
async def _run(self, fn: typing.Callable[[], typing.Any]) -> typing.Any:
|
|
307
|
+
async with self._lock:
|
|
308
|
+
return await asyncio.to_thread(fn)
|
|
309
|
+
|
|
310
|
+
async def login_http_sig(self, session_public_key: dict[str, typing.Any]) -> None:
|
|
311
|
+
return await self._run(lambda: self._inner.login_http_sig(session_public_key))
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
class AsyncInvitationClient:
|
|
315
|
+
def __init__(self, inner: invitation_client.InvitationClient) -> None:
|
|
316
|
+
self._inner = inner
|
|
317
|
+
self._lock = asyncio.Lock()
|
|
318
|
+
|
|
319
|
+
async def _run(self, fn: typing.Callable[[], typing.Any]) -> typing.Any:
|
|
320
|
+
async with self._lock:
|
|
321
|
+
return await asyncio.to_thread(fn)
|
|
322
|
+
|
|
323
|
+
async def connect(self, account_public_key: dict[str, typing.Any]) -> None:
|
|
324
|
+
return await self._run(lambda: self._inner.connect(account_public_key))
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import requests
|
|
4
|
+
|
|
5
|
+
from . import exceptions
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Directory:
|
|
9
|
+
"""Fetches and caches the server's endpoint URL map from a well-known URL."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, url: str, timeout: float = 5.0) -> None:
|
|
12
|
+
self._url = url
|
|
13
|
+
self._timeout = timeout
|
|
14
|
+
self._data: dict[str, str] | None = None
|
|
15
|
+
|
|
16
|
+
def _load(self) -> dict[str, str]:
|
|
17
|
+
if self._data is None:
|
|
18
|
+
try:
|
|
19
|
+
response = requests.get(self._url, timeout=self._timeout)
|
|
20
|
+
except requests.exceptions.ConnectionError:
|
|
21
|
+
raise exceptions.UI("Unable to connect to server")
|
|
22
|
+
except requests.exceptions.ReadTimeout:
|
|
23
|
+
raise exceptions.UI("Request timed out")
|
|
24
|
+
if response.status_code != 200:
|
|
25
|
+
raise exceptions.UI("Unable to read directory from server")
|
|
26
|
+
self._data = response.json()
|
|
27
|
+
assert self._data is not None
|
|
28
|
+
return self._data
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def accept_invitation(self) -> str:
|
|
32
|
+
return self._load()["accept_invitation"]
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def audit_log(self) -> str:
|
|
36
|
+
return self._load()["audit_log"]
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def auth(self) -> str:
|
|
40
|
+
return self._load()["auth"]
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def bastion(self) -> str:
|
|
44
|
+
return self._load()["bastion"]
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def boundary(self) -> str:
|
|
48
|
+
return self._load()["boundary"]
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def identity(self) -> str:
|
|
52
|
+
return self._load()["identity"]
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def initialize(self) -> str:
|
|
56
|
+
return self._load()["initialize"]
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def login(self) -> str:
|
|
60
|
+
return self._load()["login"]
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def login_oauth2_start(self) -> str:
|
|
64
|
+
return self._load()["login_oauth2_start"]
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def login_oidc(self) -> str:
|
|
68
|
+
return self._load()["login_oidc"]
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def public_auth(self) -> str:
|
|
72
|
+
return self._load()["public_auth"]
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def role(self) -> str:
|
|
76
|
+
return self._load()["role"]
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def ssh(self) -> str:
|
|
80
|
+
return self._load()["ssh"]
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def tag(self) -> str:
|
|
84
|
+
return self._load()["tag"]
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def tenant(self) -> str:
|
|
88
|
+
return self._load()["tenant"]
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import typing
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
|
|
8
|
+
from . import exceptions, http_signatures
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class HttpSession:
|
|
14
|
+
"""Thin wrapper around requests.Session: logging, 400/422 error handling, per-request auth."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, session: requests.Session, timeout: float = 5.0) -> None:
|
|
17
|
+
self._session = session
|
|
18
|
+
self._timeout = timeout
|
|
19
|
+
|
|
20
|
+
def request(
|
|
21
|
+
self,
|
|
22
|
+
method: str,
|
|
23
|
+
url: str,
|
|
24
|
+
*,
|
|
25
|
+
auth: http_signatures.Auth | None = None,
|
|
26
|
+
json: typing.Any = None,
|
|
27
|
+
data: typing.Any = None,
|
|
28
|
+
headers: dict[str, str] | None = None,
|
|
29
|
+
params: dict[str, typing.Any] | None = None,
|
|
30
|
+
timeout: float | None = None,
|
|
31
|
+
) -> requests.Response:
|
|
32
|
+
req = requests.Request(method=method, url=url, json=json, data=data, headers=headers, params=params)
|
|
33
|
+
prepared = req.prepare()
|
|
34
|
+
if auth is not None:
|
|
35
|
+
prepared = auth(prepared)
|
|
36
|
+
|
|
37
|
+
effective_timeout = timeout if timeout is not None else self._timeout
|
|
38
|
+
logger.info(f"tx {prepared.method} {prepared.url}")
|
|
39
|
+
logger.debug(f"tx headers: {prepared.headers}")
|
|
40
|
+
logger.debug(f"tx body: {prepared.body}")
|
|
41
|
+
try:
|
|
42
|
+
response = self._session.send(prepared, timeout=effective_timeout)
|
|
43
|
+
except requests.exceptions.ConnectionError:
|
|
44
|
+
raise exceptions.UI("Unable to connect to server")
|
|
45
|
+
except requests.exceptions.ReadTimeout:
|
|
46
|
+
raise exceptions.UI("Request timed out")
|
|
47
|
+
logger.info(f"rx {response.status_code}")
|
|
48
|
+
logger.debug(f"rx headers: {response.headers}")
|
|
49
|
+
logger.debug(f"rx body: {response.content}")
|
|
50
|
+
|
|
51
|
+
if response.status_code in (400, 422):
|
|
52
|
+
try:
|
|
53
|
+
problem = response.json()
|
|
54
|
+
title = problem.get("title", "")
|
|
55
|
+
detail = problem.get("detail")
|
|
56
|
+
msg = f"{title} {detail}" if detail else title
|
|
57
|
+
except Exception:
|
|
58
|
+
msg = response.text
|
|
59
|
+
raise exceptions.UI(msg or response.text)
|
|
60
|
+
|
|
61
|
+
return response
|
|
62
|
+
|
|
63
|
+
def get(
|
|
64
|
+
self,
|
|
65
|
+
url: str,
|
|
66
|
+
*,
|
|
67
|
+
auth: http_signatures.Auth | None = None,
|
|
68
|
+
params: dict[str, typing.Any] | None = None,
|
|
69
|
+
) -> requests.Response:
|
|
70
|
+
return self.request("GET", url, auth=auth, params=params)
|
|
71
|
+
|
|
72
|
+
def post(
|
|
73
|
+
self,
|
|
74
|
+
url: str,
|
|
75
|
+
*,
|
|
76
|
+
auth: http_signatures.Auth | None = None,
|
|
77
|
+
json: typing.Any = None,
|
|
78
|
+
) -> requests.Response:
|
|
79
|
+
return self.request("POST", url, auth=auth, json=json)
|
|
80
|
+
|
|
81
|
+
def patch(
|
|
82
|
+
self,
|
|
83
|
+
url: str,
|
|
84
|
+
*,
|
|
85
|
+
auth: http_signatures.Auth | None = None,
|
|
86
|
+
json: typing.Any = None,
|
|
87
|
+
) -> requests.Response:
|
|
88
|
+
return self.request("PATCH", url, auth=auth, json=json)
|
|
89
|
+
|
|
90
|
+
def delete(
|
|
91
|
+
self,
|
|
92
|
+
url: str,
|
|
93
|
+
*,
|
|
94
|
+
auth: http_signatures.Auth | None = None,
|
|
95
|
+
) -> requests.Response:
|
|
96
|
+
return self.request("DELETE", url, auth=auth)
|
|
97
|
+
|
|
98
|
+
def put(
|
|
99
|
+
self,
|
|
100
|
+
url: str,
|
|
101
|
+
*,
|
|
102
|
+
auth: http_signatures.Auth | None = None,
|
|
103
|
+
json: typing.Any = None,
|
|
104
|
+
) -> requests.Response:
|
|
105
|
+
return self.request("PUT", url, auth=auth, json=json)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import secrets
|
|
5
|
+
import time
|
|
6
|
+
import typing
|
|
7
|
+
import urllib.parse
|
|
8
|
+
|
|
9
|
+
import http_sfv
|
|
10
|
+
import requests
|
|
11
|
+
|
|
12
|
+
from . import signer
|
|
13
|
+
|
|
14
|
+
# http_sfv type stubs are incomplete (private imports, reportPrivateImportUsage)
|
|
15
|
+
# pyright: reportPrivateImportUsage=false
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _build_signature_base(
|
|
19
|
+
request: requests.PreparedRequest,
|
|
20
|
+
covered: tuple[str, ...],
|
|
21
|
+
sig_params: str,
|
|
22
|
+
) -> str:
|
|
23
|
+
parts: list[str] = []
|
|
24
|
+
for c in covered:
|
|
25
|
+
match c:
|
|
26
|
+
case "@method":
|
|
27
|
+
parts.append(f'"@method": {request.method}')
|
|
28
|
+
case "@authority":
|
|
29
|
+
parts.append(f'"@authority": {urllib.parse.urlparse(request.url).netloc}')
|
|
30
|
+
case "@target-uri":
|
|
31
|
+
parts.append(f'"@target-uri": {request.url}')
|
|
32
|
+
case "@signature-params":
|
|
33
|
+
parts.append(f'"@signature-params": {sig_params}')
|
|
34
|
+
case _:
|
|
35
|
+
parts.append(f'"{c}": {request.headers[c]}')
|
|
36
|
+
return "\n".join(parts)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Auth:
|
|
40
|
+
"""HTTP Message Signatures (RFC 9421) — signs a request with one or more Signer instances."""
|
|
41
|
+
|
|
42
|
+
def __init__(self, signers: typing.Sequence[signer.Signer]) -> None:
|
|
43
|
+
self._signers = signers
|
|
44
|
+
|
|
45
|
+
def _sign(
|
|
46
|
+
self, signer: signer.Signer, request: requests.PreparedRequest, covered: tuple[str, ...]
|
|
47
|
+
) -> tuple[str, str]:
|
|
48
|
+
key_id = f"{signer.prefix()}:{signer.thumbprint()}"
|
|
49
|
+
|
|
50
|
+
inner = http_sfv.InnerList([http_sfv.Item(c) for c in covered])
|
|
51
|
+
inner.params["created"] = int(time.time())
|
|
52
|
+
inner.params["keyid"] = key_id
|
|
53
|
+
inner.params["nonce"] = secrets.token_hex(16)
|
|
54
|
+
|
|
55
|
+
sig_params = str(inner)
|
|
56
|
+
sig_base = _build_signature_base(request, covered, sig_params)
|
|
57
|
+
sig_bytes = signer.sign(sig_base.encode())
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
f"{signer.prefix()}={sig_params}",
|
|
61
|
+
f"{signer.prefix()}={http_sfv.Item(sig_bytes)}",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
def __call__(self, request: requests.PreparedRequest) -> requests.PreparedRequest:
|
|
65
|
+
if "Content-Digest" not in request.headers:
|
|
66
|
+
body = request.body or b""
|
|
67
|
+
if isinstance(body, str):
|
|
68
|
+
body_bytes = body.encode()
|
|
69
|
+
else:
|
|
70
|
+
body_bytes = body if isinstance(body, bytes) else b""
|
|
71
|
+
digest = hashlib.sha256(body_bytes).digest()
|
|
72
|
+
request.headers["Content-Digest"] = str(http_sfv.Dictionary({"sha-256": digest}))
|
|
73
|
+
covered = ("@method", "@authority", "@target-uri", "content-digest", "@signature-params")
|
|
74
|
+
signatures_input: list[str] = []
|
|
75
|
+
signatures: list[str] = []
|
|
76
|
+
for _signer in self._signers:
|
|
77
|
+
signature_input, signature = self._sign(_signer, request, covered)
|
|
78
|
+
signatures_input.append(signature_input)
|
|
79
|
+
signatures.append(signature)
|
|
80
|
+
request.headers["Signature-Input"] = ", ".join(signatures_input)
|
|
81
|
+
request.headers["Signature"] = ", ".join(signatures)
|
|
82
|
+
return request
|