yirifi-ops-auth-client 3.2.3__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.
- yirifi_ops_auth/__init__.py +58 -0
- yirifi_ops_auth/client.py +154 -0
- yirifi_ops_auth/decorators.py +213 -0
- yirifi_ops_auth/deeplink/__init__.py +210 -0
- yirifi_ops_auth/deeplink/blueprint.py +155 -0
- yirifi_ops_auth/deeplink/environment.py +156 -0
- yirifi_ops_auth/deeplink/federation.py +409 -0
- yirifi_ops_auth/deeplink/jinja.py +316 -0
- yirifi_ops_auth/deeplink/registry.py +401 -0
- yirifi_ops_auth/deeplink/resolver.py +208 -0
- yirifi_ops_auth/deeplink/yaml_loader.py +242 -0
- yirifi_ops_auth/exceptions.py +32 -0
- yirifi_ops_auth/local_user.py +124 -0
- yirifi_ops_auth/middleware.py +281 -0
- yirifi_ops_auth/models.py +80 -0
- yirifi_ops_auth_client-3.2.3.dist-info/METADATA +15 -0
- yirifi_ops_auth_client-3.2.3.dist-info/RECORD +19 -0
- yirifi_ops_auth_client-3.2.3.dist-info/WHEEL +5 -0
- yirifi_ops_auth_client-3.2.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
"""Dynamic entity registry for cross-site deep linking.
|
|
2
|
+
|
|
3
|
+
Microsites register their entity definitions at startup. The registry is
|
|
4
|
+
empty by default - no hardcoded definitions.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
# Option 1: Register entities programmatically
|
|
8
|
+
from yirifi_ops_auth.deeplink import register_entities
|
|
9
|
+
|
|
10
|
+
register_entities(
|
|
11
|
+
microsite_id="risk",
|
|
12
|
+
name="Risk Dashboard",
|
|
13
|
+
urls={"dev": "http://localhost:5012", "uat": "...", "prd": "..."},
|
|
14
|
+
entities=[
|
|
15
|
+
{"type": "risk_item", "path": "/risk-management/collections/risk_items/{id}"},
|
|
16
|
+
]
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
# Option 2: Load from YAML file
|
|
20
|
+
from yirifi_ops_auth.deeplink import load_from_yaml
|
|
21
|
+
load_from_yaml("deeplinks.yaml")
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from dataclasses import dataclass, field
|
|
25
|
+
from typing import Dict, List, Optional, Any
|
|
26
|
+
import logging
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class RegistrationError(Exception):
|
|
32
|
+
"""Raised when entity/microsite registration fails validation."""
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class EntityDefinition:
|
|
38
|
+
"""Definition of an entity type for deep linking."""
|
|
39
|
+
entity_type: str
|
|
40
|
+
microsite: str
|
|
41
|
+
path_template: str
|
|
42
|
+
description: Optional[str] = None
|
|
43
|
+
|
|
44
|
+
def __post_init__(self) -> None:
|
|
45
|
+
if "{id}" not in self.path_template:
|
|
46
|
+
raise ValueError(
|
|
47
|
+
f"path_template must contain {{id}} placeholder: {self.path_template}"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True)
|
|
52
|
+
class MicrositeConfig:
|
|
53
|
+
"""Configuration for a microsite."""
|
|
54
|
+
id: str
|
|
55
|
+
name: str
|
|
56
|
+
urls: Dict[str, str] = field(default_factory=dict) # {"dev": "...", "uat": "...", "prd": "..."}
|
|
57
|
+
|
|
58
|
+
def get_url(self, env: str) -> Optional[str]:
|
|
59
|
+
"""Get the URL for a specific environment."""
|
|
60
|
+
return self.urls.get(env)
|
|
61
|
+
|
|
62
|
+
# Backwards compatibility properties
|
|
63
|
+
@property
|
|
64
|
+
def dev_url(self) -> str:
|
|
65
|
+
return self.urls.get("dev", "")
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def uat_url(self) -> str:
|
|
69
|
+
return self.urls.get("uat", "")
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def prd_url(self) -> str:
|
|
73
|
+
return self.urls.get("prd", "")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class DeepLinkRegistry:
|
|
77
|
+
"""Dynamic registry for entity definitions and microsite configurations.
|
|
78
|
+
|
|
79
|
+
Thread-safe registry that microsites populate at startup.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
def __init__(self) -> None:
|
|
83
|
+
self._entities: Dict[str, EntityDefinition] = {}
|
|
84
|
+
self._microsites: Dict[str, MicrositeConfig] = {}
|
|
85
|
+
|
|
86
|
+
def register_microsite(
|
|
87
|
+
self,
|
|
88
|
+
id: str,
|
|
89
|
+
name: str,
|
|
90
|
+
urls: Dict[str, str],
|
|
91
|
+
) -> None:
|
|
92
|
+
"""Register a microsite configuration.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
id: Microsite identifier (e.g., 'risk', 'reg')
|
|
96
|
+
name: Human-readable name (e.g., 'Risk Dashboard')
|
|
97
|
+
urls: Dict mapping env to URL (e.g., {"dev": "http://localhost:5012", ...})
|
|
98
|
+
|
|
99
|
+
Raises:
|
|
100
|
+
RegistrationError: If validation fails
|
|
101
|
+
"""
|
|
102
|
+
# Validate URLs
|
|
103
|
+
required_envs = {"dev", "uat", "prd"}
|
|
104
|
+
missing = required_envs - set(urls.keys())
|
|
105
|
+
if missing:
|
|
106
|
+
raise RegistrationError(
|
|
107
|
+
f"Microsite '{id}' missing URLs for environments: {missing}"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
config = MicrositeConfig(id=id, name=name, urls=urls)
|
|
111
|
+
self._microsites[id] = config
|
|
112
|
+
logger.debug(f"Registered microsite: {id}")
|
|
113
|
+
|
|
114
|
+
def register_entity(
|
|
115
|
+
self,
|
|
116
|
+
entity_type: str,
|
|
117
|
+
microsite: str,
|
|
118
|
+
path_template: str,
|
|
119
|
+
description: Optional[str] = None,
|
|
120
|
+
auto_create_microsite: bool = False,
|
|
121
|
+
) -> None:
|
|
122
|
+
"""Register a single entity definition.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
entity_type: Entity type identifier (e.g., 'risk_item')
|
|
126
|
+
microsite: Microsite ID that owns this entity
|
|
127
|
+
path_template: URL path with {id} placeholder
|
|
128
|
+
description: Optional description
|
|
129
|
+
auto_create_microsite: If True, skip microsite existence check
|
|
130
|
+
|
|
131
|
+
Raises:
|
|
132
|
+
RegistrationError: If validation fails
|
|
133
|
+
"""
|
|
134
|
+
# Validate microsite exists (unless auto_create is True)
|
|
135
|
+
if not auto_create_microsite and microsite not in self._microsites:
|
|
136
|
+
raise RegistrationError(
|
|
137
|
+
f"Cannot register entity '{entity_type}': microsite '{microsite}' not registered. "
|
|
138
|
+
f"Register the microsite first with register_microsite()."
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Validate path template
|
|
142
|
+
if "{id}" not in path_template:
|
|
143
|
+
raise RegistrationError(
|
|
144
|
+
f"Entity '{entity_type}' path_template must contain {{id}}: {path_template}"
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# Warn on duplicate (but allow override)
|
|
148
|
+
if entity_type in self._entities:
|
|
149
|
+
logger.warning(f"Overwriting existing entity definition: {entity_type}")
|
|
150
|
+
|
|
151
|
+
defn = EntityDefinition(
|
|
152
|
+
entity_type=entity_type,
|
|
153
|
+
microsite=microsite,
|
|
154
|
+
path_template=path_template,
|
|
155
|
+
description=description,
|
|
156
|
+
)
|
|
157
|
+
self._entities[entity_type] = defn
|
|
158
|
+
logger.debug(f"Registered entity: {entity_type} -> {microsite}")
|
|
159
|
+
|
|
160
|
+
def register_entities(
|
|
161
|
+
self,
|
|
162
|
+
microsite_id: str,
|
|
163
|
+
entities: List[Dict[str, Any]],
|
|
164
|
+
urls: Dict[str, str],
|
|
165
|
+
name: Optional[str] = None,
|
|
166
|
+
) -> None:
|
|
167
|
+
"""Bulk register a microsite and its entities.
|
|
168
|
+
|
|
169
|
+
Convenience method for registering everything at once.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
microsite_id: Microsite identifier
|
|
173
|
+
entities: List of dicts with 'type', 'path', and optional 'description'
|
|
174
|
+
urls: URL mapping for the microsite
|
|
175
|
+
name: Human-readable name (defaults to title-cased microsite_id)
|
|
176
|
+
|
|
177
|
+
Example:
|
|
178
|
+
register_entities(
|
|
179
|
+
microsite_id="risk",
|
|
180
|
+
name="Risk Dashboard",
|
|
181
|
+
urls={"dev": "http://localhost:5012", "uat": "...", "prd": "..."},
|
|
182
|
+
entities=[
|
|
183
|
+
{"type": "risk_item", "path": "/items/{id}", "description": "Risk item"},
|
|
184
|
+
{"type": "risk_category", "path": "/categories/{id}"},
|
|
185
|
+
]
|
|
186
|
+
)
|
|
187
|
+
"""
|
|
188
|
+
# Default name from ID
|
|
189
|
+
if name is None:
|
|
190
|
+
name = microsite_id.replace("_", " ").title()
|
|
191
|
+
|
|
192
|
+
# Register microsite first
|
|
193
|
+
self.register_microsite(id=microsite_id, name=name, urls=urls)
|
|
194
|
+
|
|
195
|
+
# Register entities
|
|
196
|
+
for entity in entities:
|
|
197
|
+
self.register_entity(
|
|
198
|
+
entity_type=entity["type"],
|
|
199
|
+
microsite=microsite_id,
|
|
200
|
+
path_template=entity["path"],
|
|
201
|
+
description=entity.get("description"),
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
def get_entity(self, entity_type: str) -> Optional[EntityDefinition]:
|
|
205
|
+
"""Get entity definition by type."""
|
|
206
|
+
return self._entities.get(entity_type)
|
|
207
|
+
|
|
208
|
+
def get_microsite(self, microsite_id: str) -> Optional[MicrositeConfig]:
|
|
209
|
+
"""Get microsite configuration by ID."""
|
|
210
|
+
return self._microsites.get(microsite_id)
|
|
211
|
+
|
|
212
|
+
def list_entities(self) -> List[str]:
|
|
213
|
+
"""List all registered entity types."""
|
|
214
|
+
return list(self._entities.keys())
|
|
215
|
+
|
|
216
|
+
def list_microsites(self) -> List[str]:
|
|
217
|
+
"""List all registered microsite IDs."""
|
|
218
|
+
return list(self._microsites.keys())
|
|
219
|
+
|
|
220
|
+
def list_entities_for_microsite(self, microsite: str) -> List[str]:
|
|
221
|
+
"""List entity types for a specific microsite."""
|
|
222
|
+
return [
|
|
223
|
+
et for et, defn in self._entities.items()
|
|
224
|
+
if defn.microsite == microsite
|
|
225
|
+
]
|
|
226
|
+
|
|
227
|
+
def clear(self) -> None:
|
|
228
|
+
"""Clear all registrations. Useful for testing."""
|
|
229
|
+
self._entities.clear()
|
|
230
|
+
self._microsites.clear()
|
|
231
|
+
logger.debug("Registry cleared")
|
|
232
|
+
|
|
233
|
+
def is_empty(self) -> bool:
|
|
234
|
+
"""Check if registry has no registrations."""
|
|
235
|
+
return len(self._entities) == 0 and len(self._microsites) == 0
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
# Module-level singleton
|
|
239
|
+
_registry = DeepLinkRegistry()
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
# Public API functions that delegate to the singleton
|
|
243
|
+
def register_microsite(
|
|
244
|
+
id: str,
|
|
245
|
+
name: str,
|
|
246
|
+
urls: Dict[str, str],
|
|
247
|
+
) -> None:
|
|
248
|
+
"""Register a microsite configuration."""
|
|
249
|
+
_registry.register_microsite(id=id, name=name, urls=urls)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def register_entity(
|
|
253
|
+
entity_type: str,
|
|
254
|
+
microsite: str,
|
|
255
|
+
path_template: str,
|
|
256
|
+
description: Optional[str] = None,
|
|
257
|
+
) -> None:
|
|
258
|
+
"""Register a single entity definition."""
|
|
259
|
+
_registry.register_entity(
|
|
260
|
+
entity_type=entity_type,
|
|
261
|
+
microsite=microsite,
|
|
262
|
+
path_template=path_template,
|
|
263
|
+
description=description,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def register_entities(
|
|
268
|
+
microsite_id: str,
|
|
269
|
+
entities: List[Dict[str, Any]],
|
|
270
|
+
urls: Dict[str, str],
|
|
271
|
+
name: Optional[str] = None,
|
|
272
|
+
) -> None:
|
|
273
|
+
"""Bulk register a microsite and its entities."""
|
|
274
|
+
_registry.register_entities(
|
|
275
|
+
microsite_id=microsite_id,
|
|
276
|
+
entities=entities,
|
|
277
|
+
urls=urls,
|
|
278
|
+
name=name,
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def get_entity_definition(entity_type: str) -> Optional[EntityDefinition]:
|
|
283
|
+
"""Get entity definition by type.
|
|
284
|
+
|
|
285
|
+
Checks local registry first, then federated sources if available.
|
|
286
|
+
Local definitions take priority over federated ones.
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
entity_type: The entity type to look up
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
EntityDefinition if found, None otherwise
|
|
293
|
+
"""
|
|
294
|
+
# 1. Check local registry first (local takes priority)
|
|
295
|
+
defn = _registry.get_entity(entity_type)
|
|
296
|
+
if defn:
|
|
297
|
+
return defn
|
|
298
|
+
|
|
299
|
+
# 2. Fallback to federation cache if available
|
|
300
|
+
try:
|
|
301
|
+
from .federation import get_federation_client
|
|
302
|
+
|
|
303
|
+
federation = get_federation_client()
|
|
304
|
+
if federation:
|
|
305
|
+
remote = federation.get_entity(entity_type)
|
|
306
|
+
if remote:
|
|
307
|
+
# Convert federation response to EntityDefinition
|
|
308
|
+
return EntityDefinition(
|
|
309
|
+
entity_type=remote["entity_type"],
|
|
310
|
+
microsite=remote["microsite"],
|
|
311
|
+
path_template=remote["path_template"],
|
|
312
|
+
description=remote.get("description"),
|
|
313
|
+
)
|
|
314
|
+
except ImportError:
|
|
315
|
+
# Federation module not available
|
|
316
|
+
pass
|
|
317
|
+
except Exception as e:
|
|
318
|
+
logger.debug(f"Federation lookup failed for {entity_type}: {e}")
|
|
319
|
+
|
|
320
|
+
return None
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def get_microsite_for_entity(entity_type: str) -> Optional[str]:
|
|
324
|
+
"""Get the microsite ID for an entity type."""
|
|
325
|
+
defn = _registry.get_entity(entity_type)
|
|
326
|
+
return defn.microsite if defn else None
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def list_entity_types() -> List[str]:
|
|
330
|
+
"""List all registered entity types."""
|
|
331
|
+
return _registry.list_entities()
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def list_entity_types_for_microsite(microsite: str) -> List[str]:
|
|
335
|
+
"""List entity types for a specific microsite."""
|
|
336
|
+
return _registry.list_entities_for_microsite(microsite)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def list_microsites() -> List[str]:
|
|
340
|
+
"""List all registered microsite IDs."""
|
|
341
|
+
return _registry.list_microsites()
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def get_microsite_config(microsite_id: str) -> Optional[MicrositeConfig]:
|
|
345
|
+
"""Get microsite configuration by ID."""
|
|
346
|
+
return _registry.get_microsite(microsite_id)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def get_microsite_name(microsite_id: str) -> Optional[str]:
|
|
350
|
+
"""Get human-readable name for a microsite."""
|
|
351
|
+
config = _registry.get_microsite(microsite_id)
|
|
352
|
+
return config.name if config else None
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def get_microsite_base_url(microsite: str, env: Optional[str] = None) -> Optional[str]:
|
|
356
|
+
"""Get base URL for a microsite in a specific environment.
|
|
357
|
+
|
|
358
|
+
Checks local registry first, then federated sources if available.
|
|
359
|
+
|
|
360
|
+
Args:
|
|
361
|
+
microsite: Microsite ID
|
|
362
|
+
env: Environment ('dev', 'uat', 'prd'). If None, uses Environment.get()
|
|
363
|
+
|
|
364
|
+
Returns:
|
|
365
|
+
Base URL string or None if microsite not found
|
|
366
|
+
"""
|
|
367
|
+
from .environment import Environment
|
|
368
|
+
|
|
369
|
+
env = env or Environment.get()
|
|
370
|
+
|
|
371
|
+
# 1. Check local registry first
|
|
372
|
+
config = _registry.get_microsite(microsite)
|
|
373
|
+
if config:
|
|
374
|
+
return config.get_url(env)
|
|
375
|
+
|
|
376
|
+
# 2. Fallback to federation cache if available
|
|
377
|
+
try:
|
|
378
|
+
from .federation import get_federation_client
|
|
379
|
+
|
|
380
|
+
federation = get_federation_client()
|
|
381
|
+
if federation:
|
|
382
|
+
urls = federation.get_microsite_urls(microsite)
|
|
383
|
+
if urls:
|
|
384
|
+
return urls.get(env)
|
|
385
|
+
except ImportError:
|
|
386
|
+
# Federation module not available
|
|
387
|
+
pass
|
|
388
|
+
except Exception as e:
|
|
389
|
+
logger.debug(f"Federation URL lookup failed for {microsite}: {e}")
|
|
390
|
+
|
|
391
|
+
return None
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def clear_registry() -> None:
|
|
395
|
+
"""Clear all registrations. Useful for testing."""
|
|
396
|
+
_registry.clear()
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def get_registry() -> DeepLinkRegistry:
|
|
400
|
+
"""Get the registry singleton. Useful for advanced use cases."""
|
|
401
|
+
return _registry
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""Deep link resolution - generates URLs to entities across microsites.
|
|
2
|
+
|
|
3
|
+
Core functions:
|
|
4
|
+
- resolve_link(): Generate a URL for an entity
|
|
5
|
+
- deep_link_info(): Get URL + accessibility info for permission-aware rendering
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import Optional, Any
|
|
10
|
+
from urllib.parse import urlencode, quote
|
|
11
|
+
|
|
12
|
+
from .registry import (
|
|
13
|
+
get_entity_definition,
|
|
14
|
+
get_microsite_base_url,
|
|
15
|
+
get_microsite_name,
|
|
16
|
+
EntityDefinition,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class DeepLinkInfo:
|
|
22
|
+
"""Information about a deep link including accessibility.
|
|
23
|
+
|
|
24
|
+
Attributes:
|
|
25
|
+
url: The resolved URL to the entity
|
|
26
|
+
accessible: Whether the current user can access the target microsite
|
|
27
|
+
microsite: The target microsite ID
|
|
28
|
+
microsite_name: Human-readable name of the target microsite
|
|
29
|
+
entity_type: The entity type that was resolved
|
|
30
|
+
entity_id: The entity ID used in resolution
|
|
31
|
+
"""
|
|
32
|
+
url: str
|
|
33
|
+
accessible: bool
|
|
34
|
+
microsite: str
|
|
35
|
+
microsite_name: str
|
|
36
|
+
entity_type: str
|
|
37
|
+
entity_id: str
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class DeepLinkError(Exception):
|
|
41
|
+
"""Raised when deep link resolution fails."""
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class UnknownEntityTypeError(DeepLinkError):
|
|
46
|
+
"""Raised when an unknown entity type is provided."""
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class UnknownMicrositeError(DeepLinkError):
|
|
51
|
+
"""Raised when a microsite URL cannot be resolved."""
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def resolve_link(
|
|
56
|
+
entity_type: str,
|
|
57
|
+
entity_id: Any,
|
|
58
|
+
env: Optional[str] = None,
|
|
59
|
+
query_params: Optional[dict] = None,
|
|
60
|
+
) -> str:
|
|
61
|
+
"""Resolve a deep link URL for an entity.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
entity_type: The type of entity (e.g., 'risk_item', 'user')
|
|
65
|
+
entity_id: The entity's identifier (will be converted to string)
|
|
66
|
+
env: Environment ('dev', 'uat', 'prd'). If None, uses current environment.
|
|
67
|
+
query_params: Optional query parameters to append to URL
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Full URL to the entity
|
|
71
|
+
|
|
72
|
+
Raises:
|
|
73
|
+
UnknownEntityTypeError: If entity_type is not in the registry
|
|
74
|
+
UnknownMicrositeError: If microsite URL cannot be resolved
|
|
75
|
+
|
|
76
|
+
Example:
|
|
77
|
+
>>> resolve_link('risk_item', 'r_yid_123')
|
|
78
|
+
'http://localhost:5012/risk-management/collections/risk_items/r_yid_123'
|
|
79
|
+
|
|
80
|
+
>>> resolve_link('user', 42, query_params={'tab': 'permissions'})
|
|
81
|
+
'http://localhost:5013/users/42?tab=permissions'
|
|
82
|
+
"""
|
|
83
|
+
# Get entity definition
|
|
84
|
+
defn = get_entity_definition(entity_type)
|
|
85
|
+
if not defn:
|
|
86
|
+
raise UnknownEntityTypeError(f"Unknown entity type: {entity_type}")
|
|
87
|
+
|
|
88
|
+
# Get base URL for the microsite
|
|
89
|
+
base_url = get_microsite_base_url(defn.microsite, env)
|
|
90
|
+
if not base_url:
|
|
91
|
+
raise UnknownMicrositeError(f"No URL configured for microsite: {defn.microsite}")
|
|
92
|
+
|
|
93
|
+
# Build the path with entity ID
|
|
94
|
+
entity_id_str = str(entity_id)
|
|
95
|
+
path = defn.path_template.format(id=quote(entity_id_str, safe=''))
|
|
96
|
+
|
|
97
|
+
# Construct full URL
|
|
98
|
+
url = f"{base_url.rstrip('/')}{path}"
|
|
99
|
+
|
|
100
|
+
# Add query parameters if provided
|
|
101
|
+
if query_params:
|
|
102
|
+
url = f"{url}?{urlencode(query_params)}"
|
|
103
|
+
|
|
104
|
+
return url
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def deep_link_info(
|
|
108
|
+
entity_type: str,
|
|
109
|
+
entity_id: Any,
|
|
110
|
+
user: Optional[Any] = None,
|
|
111
|
+
env: Optional[str] = None,
|
|
112
|
+
query_params: Optional[dict] = None,
|
|
113
|
+
) -> DeepLinkInfo:
|
|
114
|
+
"""Get deep link information including accessibility status.
|
|
115
|
+
|
|
116
|
+
This is the preferred function for permission-aware rendering.
|
|
117
|
+
It returns all information needed to render either an active link
|
|
118
|
+
or a disabled placeholder.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
entity_type: The type of entity (e.g., 'risk_item', 'user')
|
|
122
|
+
entity_id: The entity's identifier
|
|
123
|
+
user: AuthUser object (from g.current_user) for access checking.
|
|
124
|
+
If None, accessible defaults to True.
|
|
125
|
+
env: Environment. If None, uses current environment.
|
|
126
|
+
query_params: Optional query parameters
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
DeepLinkInfo with url, accessibility, and metadata
|
|
130
|
+
|
|
131
|
+
Raises:
|
|
132
|
+
UnknownEntityTypeError: If entity_type is not in the registry
|
|
133
|
+
|
|
134
|
+
Example:
|
|
135
|
+
>>> info = deep_link_info('risk_item', 'r_yid_123', g.current_user)
|
|
136
|
+
>>> if info.accessible:
|
|
137
|
+
... print(f'<a href="{info.url}">Open in {info.microsite_name}</a>')
|
|
138
|
+
... else:
|
|
139
|
+
... print(f'<span title="No access">{info.microsite_name}</span>')
|
|
140
|
+
"""
|
|
141
|
+
# Get entity definition
|
|
142
|
+
defn = get_entity_definition(entity_type)
|
|
143
|
+
if not defn:
|
|
144
|
+
raise UnknownEntityTypeError(f"Unknown entity type: {entity_type}")
|
|
145
|
+
|
|
146
|
+
# Resolve the URL
|
|
147
|
+
url = resolve_link(entity_type, entity_id, env, query_params)
|
|
148
|
+
|
|
149
|
+
# Check accessibility
|
|
150
|
+
accessible = True
|
|
151
|
+
if user is not None:
|
|
152
|
+
# Use the auth client's has_access_to method
|
|
153
|
+
if hasattr(user, 'has_access_to'):
|
|
154
|
+
accessible = user.has_access_to(defn.microsite)
|
|
155
|
+
elif hasattr(user, 'microsites'):
|
|
156
|
+
# Fallback: check microsites list
|
|
157
|
+
accessible = defn.microsite in (user.microsites or [])
|
|
158
|
+
|
|
159
|
+
return DeepLinkInfo(
|
|
160
|
+
url=url,
|
|
161
|
+
accessible=accessible,
|
|
162
|
+
microsite=defn.microsite,
|
|
163
|
+
microsite_name=get_microsite_name(defn.microsite) or defn.microsite.title(),
|
|
164
|
+
entity_type=entity_type,
|
|
165
|
+
entity_id=str(entity_id),
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def can_link_to(entity_type: str, user: Optional[Any] = None) -> bool:
|
|
170
|
+
"""Check if a user can access the microsite for an entity type.
|
|
171
|
+
|
|
172
|
+
Useful for conditionally showing cross-link UI elements.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
entity_type: The type of entity
|
|
176
|
+
user: AuthUser object for access checking
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
True if user can access the target microsite, False otherwise
|
|
180
|
+
"""
|
|
181
|
+
defn = get_entity_definition(entity_type)
|
|
182
|
+
if not defn:
|
|
183
|
+
return False
|
|
184
|
+
|
|
185
|
+
if user is None:
|
|
186
|
+
return True
|
|
187
|
+
|
|
188
|
+
if hasattr(user, 'has_access_to'):
|
|
189
|
+
return user.has_access_to(defn.microsite)
|
|
190
|
+
elif hasattr(user, 'microsites'):
|
|
191
|
+
return defn.microsite in (user.microsites or [])
|
|
192
|
+
|
|
193
|
+
return True
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def get_entity_microsite_name(entity_type: str) -> Optional[str]:
|
|
197
|
+
"""Get the display name of the microsite for an entity type.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
entity_type: The type of entity
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
Human-readable microsite name, or None if entity type unknown
|
|
204
|
+
"""
|
|
205
|
+
defn = get_entity_definition(entity_type)
|
|
206
|
+
if not defn:
|
|
207
|
+
return None
|
|
208
|
+
return get_microsite_name(defn.microsite) or defn.microsite.title()
|