conduit-admin-client 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,15 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .env
4
+ .vscode/settings.json
5
+ Resources/BCP/*.xlsx
6
+ scratchpad/*.sql
7
+ .env.*
8
+ .vscode/settings.json
9
+
10
+ # Dependencies and build output
11
+ node_modules/
12
+ dist/
13
+
14
+ .claude/settings.local.json
15
+ shared/keys.txt
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 2.4
2
+ Name: conduit-admin-client
3
+ Version: 0.1.0
4
+ Summary: Shared client for the Conduit Admin authorization service
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: cachetools>=5.3
7
+ Requires-Dist: httpx>=0.25
8
+ Provides-Extra: dev
9
+ Requires-Dist: httpx>=0.25; extra == 'dev'
10
+ Requires-Dist: pytest>=7.0; extra == 'dev'
11
+ Requires-Dist: respx>=0.20; extra == 'dev'
@@ -0,0 +1,26 @@
1
+ [project]
2
+ name = "conduit-admin-client"
3
+ version = "0.1.0"
4
+ description = "Shared client for the Conduit Admin authorization service"
5
+ requires-python = ">=3.11"
6
+ dependencies = [
7
+ "httpx>=0.25",
8
+ "cachetools>=5.3",
9
+ ]
10
+
11
+ [project.optional-dependencies]
12
+ dev = [
13
+ "pytest>=7.0",
14
+ "respx>=0.20",
15
+ "httpx>=0.25",
16
+ ]
17
+
18
+ [build-system]
19
+ requires = ["hatchling"]
20
+ build-backend = "hatchling.build"
21
+
22
+ [tool.hatch.build.targets.wheel]
23
+ packages = ["src/conduit_admin_client"]
24
+
25
+ [tool.pytest.ini_options]
26
+ testpaths = ["tests"]
@@ -0,0 +1,25 @@
1
+ """Shared client for the Conduit Admin authorization service.
2
+
3
+ Usage:
4
+ from conduit_admin_client import AdminClient
5
+
6
+ client = AdminClient(
7
+ base_url="http://admin-api:8000",
8
+ app_code="extractor",
9
+ service_token="shared-secret",
10
+ )
11
+
12
+ # In a request handler, forward the user's JWT
13
+ client.set_auth_token(jwt_token)
14
+
15
+ # Get authz context (cached 60s per user)
16
+ ctx = client.get_user_context(object_id)
17
+
18
+ # JIT provision if user not found
19
+ if not ctx:
20
+ ctx = client.jit_provision(object_id, email, display_name, tenant_id)
21
+ """
22
+
23
+ from conduit_admin_client.client import AdminClient
24
+
25
+ __all__ = ["AdminClient"]
@@ -0,0 +1,126 @@
1
+ """HTTP client for the Conduit Admin authorization service.
2
+
3
+ Provides authz resolution and JIT user provisioning with per-user TTL caching.
4
+ Each consumer app creates an instance with its own app_code (e.g. "extractor",
5
+ "mapper", "scripter") so the Admin API resolves app-scoped roles correctly.
6
+ """
7
+
8
+ import logging
9
+ import threading
10
+
11
+ import httpx
12
+ from cachetools import TTLCache
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class AdminClient:
18
+ """Client for the Conduit Admin authz API.
19
+
20
+ Args:
21
+ base_url: Admin API base URL (e.g. "http://admin-api:8000").
22
+ app_code: This app's code for role resolution (e.g. "extractor").
23
+ service_token: Shared secret for service-to-service auth (optional).
24
+ cache_ttl: Seconds to cache authz responses per user. Default 60.
25
+ cache_maxsize: Max cached users. Default 500.
26
+ timeout: HTTP request timeout in seconds. Default 5.
27
+ """
28
+
29
+ def __init__(
30
+ self,
31
+ base_url: str,
32
+ app_code: str,
33
+ service_token: str | None = None,
34
+ cache_ttl: int = 60,
35
+ cache_maxsize: int = 500,
36
+ timeout: float = 5.0,
37
+ ):
38
+ self._base_url = base_url.rstrip("/")
39
+ self._app_code = app_code
40
+ self._service_token = service_token
41
+ self._timeout = timeout
42
+ self._cache: TTLCache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl)
43
+ self._local = threading.local()
44
+
45
+ def set_auth_token(self, token: str | None):
46
+ """Store the current request's JWT token for forwarding to Admin API."""
47
+ self._local.token = token
48
+
49
+ def _headers(self) -> dict:
50
+ """Build headers -- forwards JWT token + service token."""
51
+ h: dict[str, str] = {"Content-Type": "application/json"}
52
+ if self._service_token:
53
+ h["X-Conduit-Service-Token"] = self._service_token
54
+ token = getattr(self._local, "token", None)
55
+ if token:
56
+ h["Authorization"] = f"Bearer {token}"
57
+ return h
58
+
59
+ def get_user_context(self, object_id: str) -> dict | None:
60
+ """Get authorization context for a user from Admin.
61
+
62
+ Returns the full authz response dict, or None if user not found.
63
+ Cached for ``cache_ttl`` seconds per object_id.
64
+ """
65
+ cache_key = f"authz:{object_id}"
66
+ cached = self._cache.get(cache_key)
67
+ if cached is not None:
68
+ return cached
69
+
70
+ try:
71
+ resp = httpx.get(
72
+ f"{self._base_url}/api/authz/{object_id}",
73
+ params={"app": self._app_code},
74
+ headers=self._headers(),
75
+ timeout=self._timeout,
76
+ )
77
+ if resp.status_code == 200:
78
+ data = resp.json()
79
+ self._cache[cache_key] = data
80
+ return data
81
+ elif resp.status_code == 404:
82
+ return None
83
+ else:
84
+ logger.error("Admin authz returned %s: %s", resp.status_code, resp.text)
85
+ return None
86
+ except Exception as e:
87
+ logger.error("Failed to call Admin authz API: %s", e)
88
+ return None
89
+
90
+ def jit_provision(
91
+ self,
92
+ object_id: str,
93
+ email: str,
94
+ display_name: str,
95
+ tenant_id: str | None = None,
96
+ ) -> dict | None:
97
+ """JIT-provision a user via Admin and return authz context."""
98
+ try:
99
+ resp = httpx.post(
100
+ f"{self._base_url}/api/authz/jit",
101
+ json={
102
+ "object_id": object_id,
103
+ "email": email,
104
+ "display_name": display_name,
105
+ "tenant_id": tenant_id,
106
+ },
107
+ headers=self._headers(),
108
+ timeout=self._timeout,
109
+ )
110
+ if resp.status_code == 200:
111
+ data = resp.json()
112
+ self._cache[f"authz:{object_id}"] = data
113
+ return data
114
+ else:
115
+ logger.error("Admin JIT returned %s: %s", resp.status_code, resp.text)
116
+ return None
117
+ except Exception as e:
118
+ logger.error("Failed to call Admin JIT API: %s", e)
119
+ return None
120
+
121
+ def invalidate_cache(self, object_id: str | None = None):
122
+ """Clear cached authz data for one user or all users."""
123
+ if object_id:
124
+ self._cache.pop(f"authz:{object_id}", None)
125
+ else:
126
+ self._cache.clear()