mrok 0.1.6__py3-none-any.whl → 0.1.8__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.
- mrok/__init__.py +6 -0
- mrok/agent/__init__.py +0 -0
- mrok/agent/sidecar/__init__.py +3 -0
- mrok/agent/sidecar/app.py +30 -0
- mrok/agent/sidecar/main.py +27 -0
- mrok/agent/ziticorn.py +29 -0
- mrok/cli/__init__.py +3 -0
- mrok/cli/commands/__init__.py +7 -0
- mrok/cli/commands/admin/__init__.py +12 -0
- mrok/cli/commands/admin/bootstrap.py +58 -0
- mrok/cli/commands/admin/list/__init__.py +8 -0
- mrok/cli/commands/admin/list/extensions.py +144 -0
- mrok/cli/commands/admin/list/instances.py +167 -0
- mrok/cli/commands/admin/register/__init__.py +8 -0
- mrok/cli/commands/admin/register/extensions.py +46 -0
- mrok/cli/commands/admin/register/instances.py +60 -0
- mrok/cli/commands/admin/unregister/__init__.py +8 -0
- mrok/cli/commands/admin/unregister/extensions.py +33 -0
- mrok/cli/commands/admin/unregister/instances.py +34 -0
- mrok/cli/commands/admin/utils.py +49 -0
- mrok/cli/commands/agent/__init__.py +6 -0
- mrok/cli/commands/agent/run/__init__.py +7 -0
- mrok/cli/commands/agent/run/asgi.py +49 -0
- mrok/cli/commands/agent/run/sidecar.py +54 -0
- mrok/cli/commands/controller/__init__.py +7 -0
- mrok/cli/commands/controller/openapi.py +47 -0
- mrok/cli/commands/controller/run.py +87 -0
- mrok/cli/main.py +97 -0
- mrok/cli/rich.py +18 -0
- mrok/conf.py +32 -0
- mrok/controller/__init__.py +0 -0
- mrok/controller/app.py +62 -0
- mrok/controller/auth.py +87 -0
- mrok/controller/dependencies/__init__.py +4 -0
- mrok/controller/dependencies/conf.py +7 -0
- mrok/controller/dependencies/ziti.py +27 -0
- mrok/controller/openapi/__init__.py +3 -0
- mrok/controller/openapi/examples.py +44 -0
- mrok/controller/openapi/utils.py +35 -0
- mrok/controller/pagination.py +79 -0
- mrok/controller/routes.py +294 -0
- mrok/controller/schemas.py +67 -0
- mrok/errors.py +2 -0
- mrok/http/__init__.py +0 -0
- mrok/http/config.py +65 -0
- mrok/http/forwarder.py +299 -0
- mrok/http/lifespan.py +10 -0
- mrok/http/master.py +90 -0
- mrok/http/protocol.py +11 -0
- mrok/http/server.py +14 -0
- mrok/logging.py +76 -0
- mrok/ziti/__init__.py +15 -0
- mrok/ziti/api.py +481 -0
- mrok/ziti/bootstrap.py +71 -0
- mrok/ziti/constants.py +9 -0
- mrok/ziti/errors.py +25 -0
- mrok/ziti/identities.py +169 -0
- mrok/ziti/pki.py +52 -0
- mrok/ziti/services.py +87 -0
- {mrok-0.1.6.dist-info → mrok-0.1.8.dist-info}/METADATA +7 -9
- mrok-0.1.8.dist-info/RECORD +64 -0
- {mrok-0.1.6.dist-info → mrok-0.1.8.dist-info}/WHEEL +1 -2
- mrok-0.1.6.dist-info/RECORD +0 -6
- mrok-0.1.6.dist-info/top_level.txt +0 -1
- {mrok-0.1.6.dist-info → mrok-0.1.8.dist-info}/entry_points.txt +0 -0
- {mrok-0.1.6.dist-info → mrok-0.1.8.dist-info}/licenses/LICENSE.txt +0 -0
mrok/ziti/api.py
ADDED
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import ssl
|
|
4
|
+
import tempfile
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from collections.abc import AsyncGenerator
|
|
7
|
+
from functools import cached_property
|
|
8
|
+
from types import TracebackType
|
|
9
|
+
from typing import Any, Literal
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
|
|
13
|
+
from mrok.conf import Settings
|
|
14
|
+
from mrok.ziti.constants import MROK_VERSION_TAG, MROK_VERSION_TAG_NAME
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
TagsType = dict[str, str | bool | None]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ZitiAPIError(Exception):
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ZitiAuthError(ZitiAPIError):
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ZitiBadRequestError(ZitiAPIError):
|
|
30
|
+
def __init__(self, response: dict[str, Any]):
|
|
31
|
+
self.response = response
|
|
32
|
+
|
|
33
|
+
def __str__(self) -> str:
|
|
34
|
+
err = self.response["error"]
|
|
35
|
+
cause = err["cause"]
|
|
36
|
+
return f"{err['code']} - {err['message']} ({cause['field']}: {cause['reason']})"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class BaseZitiAPI(ABC):
|
|
40
|
+
def __init__(self, settings: Settings):
|
|
41
|
+
self.settings = settings
|
|
42
|
+
self.limit = self.settings.pagination.limit
|
|
43
|
+
self.token = None
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
@abstractmethod
|
|
47
|
+
def base_url(self):
|
|
48
|
+
raise NotImplementedError("base_url property must be implemented in subclasses")
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def auth(self):
|
|
52
|
+
if self.settings.ziti.auth.get("username") and self.settings.ziti.auth.get("password"):
|
|
53
|
+
return ZitiPasswordAuth(self)
|
|
54
|
+
elif self.settings.ziti.auth.get("identity"):
|
|
55
|
+
return ZitiIdentityAuth(self)
|
|
56
|
+
else:
|
|
57
|
+
raise ZitiAuthError("Unsupported authentication method for OpenZiti.")
|
|
58
|
+
|
|
59
|
+
@cached_property
|
|
60
|
+
def httpx_client(self) -> httpx.AsyncClient:
|
|
61
|
+
return httpx.AsyncClient(
|
|
62
|
+
base_url=self.base_url,
|
|
63
|
+
auth=self.auth,
|
|
64
|
+
verify=self.settings.ziti.ssl_verify,
|
|
65
|
+
timeout=httpx.Timeout(
|
|
66
|
+
connect=0.25,
|
|
67
|
+
read=self.settings.ziti.read_timeout,
|
|
68
|
+
write=2.0,
|
|
69
|
+
pool=5.0,
|
|
70
|
+
),
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
async def create(self, endpoint: str, payload: dict[str, Any], tags: TagsType | None) -> str:
|
|
74
|
+
payload["tags"] = self._merge_tags(tags)
|
|
75
|
+
response: httpx.Response = await self.httpx_client.post(
|
|
76
|
+
endpoint,
|
|
77
|
+
json=payload,
|
|
78
|
+
)
|
|
79
|
+
if response.status_code == 400:
|
|
80
|
+
raise ZitiBadRequestError(response.json())
|
|
81
|
+
response.raise_for_status()
|
|
82
|
+
return response.json()["data"]["id"]
|
|
83
|
+
|
|
84
|
+
async def get(
|
|
85
|
+
self,
|
|
86
|
+
endpoint: str,
|
|
87
|
+
id: str,
|
|
88
|
+
additional_path: str | None = None,
|
|
89
|
+
) -> dict[str, Any]:
|
|
90
|
+
url = f"{endpoint}/{id}"
|
|
91
|
+
if additional_path:
|
|
92
|
+
url = f"{url}/{additional_path}"
|
|
93
|
+
response = await self.httpx_client.get(url)
|
|
94
|
+
response.raise_for_status()
|
|
95
|
+
return response.json()["data"]
|
|
96
|
+
|
|
97
|
+
async def delete(self, endpoint: str, id: str) -> None:
|
|
98
|
+
response = await self.httpx_client.delete(f"{endpoint}/{id}")
|
|
99
|
+
response.raise_for_status()
|
|
100
|
+
return response.json()
|
|
101
|
+
|
|
102
|
+
async def search_by_id_or_name(self, endpoint: str, id_or_name: str) -> dict[str, Any] | None:
|
|
103
|
+
query = (
|
|
104
|
+
f'(id="{id_or_name}" or name="{id_or_name.lower()}") '
|
|
105
|
+
f"and tags.{MROK_VERSION_TAG_NAME} != null"
|
|
106
|
+
)
|
|
107
|
+
response = await self.httpx_client.get(
|
|
108
|
+
endpoint,
|
|
109
|
+
params={"filter": query},
|
|
110
|
+
)
|
|
111
|
+
if response.status_code == 400:
|
|
112
|
+
raise ZitiBadRequestError(response.json())
|
|
113
|
+
response.raise_for_status()
|
|
114
|
+
response_data = response.json()
|
|
115
|
+
if response_data["meta"]["pagination"]["totalCount"] == 1:
|
|
116
|
+
return response_data["data"][0]
|
|
117
|
+
|
|
118
|
+
async def get_page(
|
|
119
|
+
self, endpoint: str, limit: int, offset: int, params: dict[str, Any] | None = None
|
|
120
|
+
) -> dict[str, Any]:
|
|
121
|
+
params = params or {}
|
|
122
|
+
params["limit"] = limit
|
|
123
|
+
params["offset"] = offset
|
|
124
|
+
page_response = await self.httpx_client.get(endpoint, params=params)
|
|
125
|
+
page_response.raise_for_status()
|
|
126
|
+
page = page_response.json()
|
|
127
|
+
return page
|
|
128
|
+
|
|
129
|
+
async def collection_iterator(
|
|
130
|
+
self, endpoint: str, params: dict[str, Any] | None = None
|
|
131
|
+
) -> AsyncGenerator[dict, None]:
|
|
132
|
+
offset = 0
|
|
133
|
+
while True:
|
|
134
|
+
page = await self.get_page(endpoint, self.limit, offset, params=params)
|
|
135
|
+
items = page["data"]
|
|
136
|
+
|
|
137
|
+
for item in items:
|
|
138
|
+
yield item
|
|
139
|
+
|
|
140
|
+
pagination_meta = page["meta"]["pagination"]
|
|
141
|
+
total = pagination_meta["totalCount"]
|
|
142
|
+
if total <= self.limit + offset:
|
|
143
|
+
break
|
|
144
|
+
|
|
145
|
+
offset = offset + self.limit
|
|
146
|
+
|
|
147
|
+
async def __aenter__(self):
|
|
148
|
+
await self.httpx_client.__aenter__()
|
|
149
|
+
return self
|
|
150
|
+
|
|
151
|
+
async def __aexit__(
|
|
152
|
+
self,
|
|
153
|
+
exc_type: type[BaseException] | None = None,
|
|
154
|
+
exc_val: BaseException | None = None,
|
|
155
|
+
exc_tb: TracebackType | None = None,
|
|
156
|
+
) -> None:
|
|
157
|
+
return await self.httpx_client.__aexit__(exc_type, exc_val, exc_tb)
|
|
158
|
+
|
|
159
|
+
def _merge_tags(self, tags: TagsType | None) -> TagsType:
|
|
160
|
+
prepared_tags: TagsType = tags or {}
|
|
161
|
+
prepared_tags.update(MROK_VERSION_TAG)
|
|
162
|
+
return prepared_tags
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class BaseZitiAuth(httpx.Auth):
|
|
166
|
+
def __init__(self, api: BaseZitiAPI):
|
|
167
|
+
self.api = api
|
|
168
|
+
|
|
169
|
+
def update_token(self, response: httpx.Response) -> None:
|
|
170
|
+
response.raise_for_status()
|
|
171
|
+
data = response.json()
|
|
172
|
+
self.api.token = data["data"]["token"]
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class ZitiIdentityAuthContext:
|
|
176
|
+
def __init__(self, identity_path: str):
|
|
177
|
+
identity = json.load(open(identity_path))
|
|
178
|
+
cert_data = identity["id"]["cert"][4:]
|
|
179
|
+
key_data = identity["id"]["key"][4:]
|
|
180
|
+
ca_data = identity["id"]["ca"][4:]
|
|
181
|
+
with (
|
|
182
|
+
tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".pem") as key_file,
|
|
183
|
+
tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".pem") as cert_file,
|
|
184
|
+
tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".pem") as ca_file,
|
|
185
|
+
):
|
|
186
|
+
key_file.write(key_data)
|
|
187
|
+
key_file.flush()
|
|
188
|
+
cert_file.write(cert_data)
|
|
189
|
+
cert_file.flush()
|
|
190
|
+
ca_file.write(ca_data)
|
|
191
|
+
ca_file.flush()
|
|
192
|
+
self.ssl_context = ssl.create_default_context(
|
|
193
|
+
cafile=ca_file.name,
|
|
194
|
+
)
|
|
195
|
+
self.ssl_context.load_cert_chain(
|
|
196
|
+
keyfile=key_file.name,
|
|
197
|
+
certfile=cert_file.name,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class ZitiPasswordAuth(BaseZitiAuth):
|
|
202
|
+
requires_response_body = True
|
|
203
|
+
|
|
204
|
+
async def async_auth_flow(
|
|
205
|
+
self, request: httpx.Request
|
|
206
|
+
) -> AsyncGenerator[httpx.Request, httpx.Response]:
|
|
207
|
+
request.headers["zt-session"] = self.api.token or ""
|
|
208
|
+
response = yield request
|
|
209
|
+
|
|
210
|
+
if response.status_code == 401: # pragma: no branch
|
|
211
|
+
refresh_request = self.build_refresh_request()
|
|
212
|
+
refresh_response = yield refresh_request
|
|
213
|
+
await refresh_response.aread()
|
|
214
|
+
self.update_token(refresh_response)
|
|
215
|
+
request.headers["zt-session"] = self.api.token or ""
|
|
216
|
+
yield request
|
|
217
|
+
|
|
218
|
+
def build_refresh_request(self) -> httpx.Request:
|
|
219
|
+
"""Builds the token refresh request."""
|
|
220
|
+
return httpx.Request(
|
|
221
|
+
"POST",
|
|
222
|
+
f"{self.api.base_url}/authenticate",
|
|
223
|
+
params={"method": "password"},
|
|
224
|
+
json={
|
|
225
|
+
"username": self.api.settings.ziti.auth.username,
|
|
226
|
+
"password": self.api.settings.ziti.auth.password,
|
|
227
|
+
},
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class ZitiIdentityAuth(BaseZitiAuth):
|
|
232
|
+
requires_response_body = True
|
|
233
|
+
|
|
234
|
+
def __init__(self, client: BaseZitiAPI):
|
|
235
|
+
super().__init__(client)
|
|
236
|
+
self.identity_context = ZitiIdentityAuthContext(self.api.settings.ziti.auth.identity)
|
|
237
|
+
|
|
238
|
+
async def async_auth_flow(
|
|
239
|
+
self, request: httpx.Request
|
|
240
|
+
) -> AsyncGenerator[httpx.Request, httpx.Response]:
|
|
241
|
+
request.headers["zt-session"] = self.api.token or ""
|
|
242
|
+
response = yield request
|
|
243
|
+
|
|
244
|
+
if response.status_code == 401: # pragma: no cover
|
|
245
|
+
# Use the new client certificate authentication method
|
|
246
|
+
refresh_response = await self.get_auth_token()
|
|
247
|
+
# await refresh_response.aread()
|
|
248
|
+
self.update_token(refresh_response)
|
|
249
|
+
request.headers["zt-session"] = self.api.token or ""
|
|
250
|
+
yield request
|
|
251
|
+
|
|
252
|
+
async def get_auth_token(self):
|
|
253
|
+
async with httpx.AsyncClient(
|
|
254
|
+
base_url=self.api.base_url,
|
|
255
|
+
verify=self.identity_context.ssl_context,
|
|
256
|
+
) as client:
|
|
257
|
+
response = await client.post(
|
|
258
|
+
"/authenticate",
|
|
259
|
+
params={"method": "cert"},
|
|
260
|
+
)
|
|
261
|
+
return response
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
class ZitiManagementAPI(BaseZitiAPI):
|
|
265
|
+
@property
|
|
266
|
+
def base_url(self):
|
|
267
|
+
return f"{self.settings.ziti.api.management}/edge/management/v1"
|
|
268
|
+
|
|
269
|
+
def services(
|
|
270
|
+
self,
|
|
271
|
+
params: dict[str, Any] | None = None,
|
|
272
|
+
) -> AsyncGenerator[dict[str, Any], None]:
|
|
273
|
+
return self.collection_iterator("/services", params=params)
|
|
274
|
+
|
|
275
|
+
def identities(
|
|
276
|
+
self,
|
|
277
|
+
params: dict[str, Any] | None = None,
|
|
278
|
+
) -> AsyncGenerator[dict[str, Any], None]:
|
|
279
|
+
return self.collection_iterator("/identities", params=params)
|
|
280
|
+
|
|
281
|
+
async def search_config(self, id_or_name) -> dict[str, Any] | None:
|
|
282
|
+
return await self.search_by_id_or_name("/configs", id_or_name)
|
|
283
|
+
|
|
284
|
+
async def create_config(
|
|
285
|
+
self, name: str, config_type_id: str, tags: TagsType | None = None
|
|
286
|
+
) -> str:
|
|
287
|
+
return await self.create(
|
|
288
|
+
"/configs",
|
|
289
|
+
{
|
|
290
|
+
"configTypeId": config_type_id,
|
|
291
|
+
"name": name,
|
|
292
|
+
"data": {
|
|
293
|
+
"auth_scheme": "none",
|
|
294
|
+
"basic_auth": None,
|
|
295
|
+
"interstitial": True,
|
|
296
|
+
"oauth": None,
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
tags,
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
async def delete_config(self, config_id: str) -> None:
|
|
303
|
+
return await self.delete("/configs", config_id)
|
|
304
|
+
|
|
305
|
+
async def create_config_type(self, name: str, tags: TagsType | None = None) -> str:
|
|
306
|
+
return await self.create(
|
|
307
|
+
"/config-types",
|
|
308
|
+
{
|
|
309
|
+
"name": name,
|
|
310
|
+
"schema": {},
|
|
311
|
+
},
|
|
312
|
+
tags,
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
async def create_service(
|
|
316
|
+
self,
|
|
317
|
+
name: str,
|
|
318
|
+
config_id: str,
|
|
319
|
+
tags: TagsType | None = None,
|
|
320
|
+
) -> str:
|
|
321
|
+
return await self.create(
|
|
322
|
+
"/services",
|
|
323
|
+
{
|
|
324
|
+
"name": name,
|
|
325
|
+
"configs": [config_id],
|
|
326
|
+
"encryptionRequired": True,
|
|
327
|
+
},
|
|
328
|
+
tags,
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
async def create_service_router_policy(
|
|
332
|
+
self,
|
|
333
|
+
name: str,
|
|
334
|
+
service_id: str,
|
|
335
|
+
tags: TagsType | None = None,
|
|
336
|
+
) -> str:
|
|
337
|
+
return await self.create(
|
|
338
|
+
"/service-edge-router-policies",
|
|
339
|
+
{
|
|
340
|
+
"name": name,
|
|
341
|
+
"edgeRouterRoles": ["#all"],
|
|
342
|
+
"serviceRoles": [
|
|
343
|
+
f"@{service_id}",
|
|
344
|
+
],
|
|
345
|
+
"semantic": "AllOf",
|
|
346
|
+
},
|
|
347
|
+
tags,
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
async def create_router_policy(
|
|
351
|
+
self,
|
|
352
|
+
name: str,
|
|
353
|
+
identity_id: str,
|
|
354
|
+
tags: TagsType | None = None,
|
|
355
|
+
) -> str:
|
|
356
|
+
return await self.create(
|
|
357
|
+
"/edge-router-policies",
|
|
358
|
+
{
|
|
359
|
+
"name": name,
|
|
360
|
+
"edgeRouterRoles": ["#all"],
|
|
361
|
+
"identityRoles": [f"@{identity_id}"],
|
|
362
|
+
"semantic": "AllOf",
|
|
363
|
+
},
|
|
364
|
+
tags,
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
async def search_service_router_policy(self, id_or_name: str) -> dict[str, Any] | None:
|
|
368
|
+
return await self.search_by_id_or_name("/service-edge-router-policies", id_or_name)
|
|
369
|
+
|
|
370
|
+
async def search_router_policy(self, id_or_name: str) -> dict[str, Any] | None:
|
|
371
|
+
return await self.search_by_id_or_name("/edge-router-policies", id_or_name)
|
|
372
|
+
|
|
373
|
+
async def delete_service_router_policy(self, policy_id: str) -> None:
|
|
374
|
+
return await self.delete("/service-edge-router-policies", policy_id)
|
|
375
|
+
|
|
376
|
+
async def delete_router_policy(self, policy_id: str) -> None:
|
|
377
|
+
return await self.delete("/edge-router-policies", policy_id)
|
|
378
|
+
|
|
379
|
+
async def search_service(self, id_or_name: str) -> dict[str, Any] | None:
|
|
380
|
+
return await self.search_by_id_or_name("/services", id_or_name)
|
|
381
|
+
|
|
382
|
+
async def get_service(self, service_id: str) -> dict[str, Any]:
|
|
383
|
+
return await self.get("/services", service_id)
|
|
384
|
+
|
|
385
|
+
async def delete_service(self, service_id: str) -> None:
|
|
386
|
+
return await self.delete("/services", service_id)
|
|
387
|
+
|
|
388
|
+
async def create_user_identity(self, name: str, tags: TagsType | None = None) -> str:
|
|
389
|
+
return await self._create_identity(name, "User", tags=tags)
|
|
390
|
+
|
|
391
|
+
async def create_device_identity(self, name: str, tags: TagsType | None = None) -> str:
|
|
392
|
+
return await self._create_identity(name, "Device", tags=tags)
|
|
393
|
+
|
|
394
|
+
async def search_identity(self, id_or_name: str) -> dict[str, Any] | None:
|
|
395
|
+
return await self.search_by_id_or_name("/identities", id_or_name)
|
|
396
|
+
|
|
397
|
+
async def search_config_type(self, id_or_name: str) -> dict[str, Any] | None:
|
|
398
|
+
return await self.search_by_id_or_name("/config-types", id_or_name)
|
|
399
|
+
|
|
400
|
+
async def get_identity(self, identity_id: str) -> dict[str, Any]:
|
|
401
|
+
return await self.get("/identities", identity_id)
|
|
402
|
+
|
|
403
|
+
async def delete_identity(self, identity_id: str) -> None:
|
|
404
|
+
return await self.delete("/identities", identity_id)
|
|
405
|
+
|
|
406
|
+
async def fetch_ca_certificates(self) -> str:
|
|
407
|
+
response = await self.httpx_client.get("/.well-known/est/cacerts")
|
|
408
|
+
response.raise_for_status()
|
|
409
|
+
return response.text
|
|
410
|
+
|
|
411
|
+
async def create_dial_service_policy(
|
|
412
|
+
self, name: str, service_id: str, identity_id: str, tags: TagsType | None = None
|
|
413
|
+
) -> str:
|
|
414
|
+
return await self._create_service_policy("Dial", name, service_id, identity_id, tags)
|
|
415
|
+
|
|
416
|
+
async def create_bind_service_policy(
|
|
417
|
+
self, name: str, service_id: str, identity_id: str, tags: TagsType | None = None
|
|
418
|
+
) -> str:
|
|
419
|
+
return await self._create_service_policy("Bind", name, service_id, identity_id, tags)
|
|
420
|
+
|
|
421
|
+
async def search_service_policy(self, id_or_name: str) -> dict[str, Any] | None:
|
|
422
|
+
return await self.search_by_id_or_name("/service-policies", id_or_name)
|
|
423
|
+
|
|
424
|
+
async def delete_service_policy(self, policy_id: str) -> None:
|
|
425
|
+
return await self.delete("/service-policies", policy_id)
|
|
426
|
+
|
|
427
|
+
async def _create_service_policy(
|
|
428
|
+
self,
|
|
429
|
+
type: Literal["Dial", "Bind"],
|
|
430
|
+
name: str,
|
|
431
|
+
service_id: str,
|
|
432
|
+
identity_id: str,
|
|
433
|
+
tags: TagsType | None = None,
|
|
434
|
+
) -> str:
|
|
435
|
+
return await self.create(
|
|
436
|
+
"/service-policies",
|
|
437
|
+
{
|
|
438
|
+
"name": name,
|
|
439
|
+
"type": type,
|
|
440
|
+
"serviceRoles": [f"@{service_id}"],
|
|
441
|
+
"identityRoles": [f"@{identity_id}"],
|
|
442
|
+
"semantic": "AllOf",
|
|
443
|
+
},
|
|
444
|
+
tags,
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
async def _create_identity(
|
|
448
|
+
self,
|
|
449
|
+
name: str,
|
|
450
|
+
type: Literal["User", "Device", "Default"],
|
|
451
|
+
tags: TagsType | None = None,
|
|
452
|
+
) -> str:
|
|
453
|
+
return await self.create(
|
|
454
|
+
"/identities",
|
|
455
|
+
{
|
|
456
|
+
"name": name,
|
|
457
|
+
"type": type,
|
|
458
|
+
"isAdmin": False,
|
|
459
|
+
"enrollment": {"ott": True},
|
|
460
|
+
},
|
|
461
|
+
tags,
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
class ZitiClientAPI(BaseZitiAPI):
|
|
466
|
+
@property
|
|
467
|
+
def base_url(self):
|
|
468
|
+
return f"{self.settings.ziti.api.client}/edge/client/v1"
|
|
469
|
+
|
|
470
|
+
async def enroll_identity(self, jti: str, csr_pem: str) -> dict[str, Any]:
|
|
471
|
+
response = await self.httpx_client.post(
|
|
472
|
+
"/enroll",
|
|
473
|
+
params={
|
|
474
|
+
"method": "ott",
|
|
475
|
+
"token": jti,
|
|
476
|
+
},
|
|
477
|
+
headers={"Content-Type": "application/x-pem-file"},
|
|
478
|
+
content=csr_pem,
|
|
479
|
+
)
|
|
480
|
+
response.raise_for_status()
|
|
481
|
+
return response.json()
|
mrok/ziti/bootstrap.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from mrok.ziti.api import TagsType, ZitiClientAPI, ZitiManagementAPI
|
|
5
|
+
from mrok.ziti.identities import enroll_proxy_identity
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
async def bootstrap_identity(
|
|
11
|
+
mgmt_api: ZitiManagementAPI,
|
|
12
|
+
client_api: ZitiClientAPI,
|
|
13
|
+
identity_name: str,
|
|
14
|
+
mode: str,
|
|
15
|
+
forced: bool,
|
|
16
|
+
tags: TagsType | None,
|
|
17
|
+
) -> tuple[str, dict[str, Any] | None]:
|
|
18
|
+
logger.info(f"Bootstrapping '{identity_name}' identity...")
|
|
19
|
+
|
|
20
|
+
identity_json = None
|
|
21
|
+
existing_identity = await mgmt_api.search_identity(identity_name)
|
|
22
|
+
|
|
23
|
+
if forced and existing_identity:
|
|
24
|
+
logger.info(f"Deleting existing identity '{identity_name}' ({existing_identity['id']})")
|
|
25
|
+
|
|
26
|
+
policy = await mgmt_api.search_router_policy(identity_name)
|
|
27
|
+
if policy:
|
|
28
|
+
await mgmt_api.delete_router_policy(policy["id"])
|
|
29
|
+
logger.info(f"Deleted existing ERP '{policy['name']}' ({policy['id']})")
|
|
30
|
+
|
|
31
|
+
await mgmt_api.delete_identity(existing_identity["id"])
|
|
32
|
+
logger.info("Deleted existing identity")
|
|
33
|
+
existing_identity = None
|
|
34
|
+
|
|
35
|
+
if existing_identity:
|
|
36
|
+
frontend_id = existing_identity["id"]
|
|
37
|
+
logger.info(f"Identity '{identity_name}' ({frontend_id}) is already enrolled")
|
|
38
|
+
else:
|
|
39
|
+
frontend_id, identity_json = await enroll_proxy_identity(
|
|
40
|
+
mgmt_api,
|
|
41
|
+
client_api,
|
|
42
|
+
identity_name,
|
|
43
|
+
tags=tags,
|
|
44
|
+
)
|
|
45
|
+
logger.info(f"Identity '{identity_name}' ({frontend_id}) successfully enrolled")
|
|
46
|
+
|
|
47
|
+
policy = await mgmt_api.search_router_policy(identity_name)
|
|
48
|
+
if not policy:
|
|
49
|
+
policy_id = await mgmt_api.create_router_policy(
|
|
50
|
+
identity_name,
|
|
51
|
+
frontend_id,
|
|
52
|
+
tags=tags,
|
|
53
|
+
)
|
|
54
|
+
logger.info(f"Created ERP '{identity_name}' ({policy_id})")
|
|
55
|
+
else:
|
|
56
|
+
logger.info(f"Found ERP '{policy['name']}' ({policy['id']})")
|
|
57
|
+
|
|
58
|
+
config_type_name = f"{mode}.proxy.v1"
|
|
59
|
+
config_type = await mgmt_api.search_config_type(config_type_name)
|
|
60
|
+
if config_type is None:
|
|
61
|
+
config_type_id = await mgmt_api.create_config_type(config_type_name, tags=tags)
|
|
62
|
+
logger.info(f"Created '{config_type_name}' ({config_type_id}) config type")
|
|
63
|
+
else:
|
|
64
|
+
logger.info(f"Found '{config_type_name}' ({config_type['id']}) config type")
|
|
65
|
+
|
|
66
|
+
if config_type and existing_identity:
|
|
67
|
+
logger.info(f"Identity '{identity_name}' was already bootstrapped")
|
|
68
|
+
else:
|
|
69
|
+
logger.info("Bootstrap completed")
|
|
70
|
+
|
|
71
|
+
return frontend_id, identity_json
|
mrok/ziti/constants.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import mrok
|
|
2
|
+
|
|
3
|
+
MROK_VERSION_TAG_NAME = "mrok"
|
|
4
|
+
MROK_VERSION_TAG_VALUE = mrok.__version__
|
|
5
|
+
MROK_VERSION_TAG = {MROK_VERSION_TAG_NAME: MROK_VERSION_TAG_VALUE}
|
|
6
|
+
MROK_SERVICE_TAG_NAME = "mrok-service"
|
|
7
|
+
MROK_IDENTITY_TYPE_TAG_NAME = "mrok-identity-type"
|
|
8
|
+
MROK_IDENTITY_TYPE_TAG_VALUE_INSTANCE = "instance"
|
|
9
|
+
MROK_IDENTITY_TYPE_TAG_VALUE_PROXY = "proxy"
|
mrok/ziti/errors.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from mrok.errors import MrokError
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ProxyIdentityNotFoundError(MrokError):
|
|
5
|
+
pass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ProxyIdentityAlreadyExistsError(MrokError):
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ServiceNotFoundError(MrokError):
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class UserIdentityNotFoundError(MrokError):
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ConfigTypeNotFoundError(MrokError):
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ServiceAlreadyRegisteredError(MrokError):
|
|
25
|
+
pass
|