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.
@@ -0,0 +1,316 @@
1
+ """Jinja2 template helpers for deep linking.
2
+
3
+ Provides template functions and macros for easy cross-site link rendering.
4
+
5
+ Usage in templates:
6
+ {{ deep_link('risk_item', item.r_yid) }}
7
+ {{ deep_link_info('user', user.id) }}
8
+ {{ cross_link('risk_item', item.r_yid, 'Open in Risk Dashboard') }}
9
+ """
10
+
11
+ from typing import Any, Optional, List, Dict
12
+ from pathlib import Path
13
+ from flask import Flask, g, current_app
14
+ from markupsafe import Markup
15
+
16
+ from .resolver import resolve_link, deep_link_info as _deep_link_info, DeepLinkInfo
17
+ from .environment import Environment
18
+
19
+
20
+ def get_current_user() -> Optional[Any]:
21
+ """Get the current user from Flask's g context."""
22
+ return getattr(g, 'current_user', None)
23
+
24
+
25
+ def deep_link(
26
+ entity_type: str,
27
+ entity_id: Any,
28
+ query_params: Optional[dict] = None,
29
+ ) -> str:
30
+ """Jinja2 function to generate a deep link URL.
31
+
32
+ Usage:
33
+ <a href="{{ deep_link('risk_item', item.r_yid) }}">View</a>
34
+
35
+ Args:
36
+ entity_type: The type of entity (e.g., 'risk_item', 'user')
37
+ entity_id: The entity's identifier
38
+ query_params: Optional query parameters
39
+
40
+ Returns:
41
+ URL string
42
+ """
43
+ try:
44
+ return resolve_link(entity_type, entity_id, query_params=query_params)
45
+ except Exception as e:
46
+ current_app.logger.warning(f"Deep link resolution failed: {e}")
47
+ return "#"
48
+
49
+
50
+ def jinja_deep_link_info(
51
+ entity_type: str,
52
+ entity_id: Any,
53
+ query_params: Optional[dict] = None,
54
+ ) -> DeepLinkInfo:
55
+ """Jinja2 function to get deep link info including accessibility.
56
+
57
+ Usage:
58
+ {% set link = deep_link_info('risk_item', item.r_yid) %}
59
+ {% if link.accessible %}
60
+ <a href="{{ link.url }}">{{ link.microsite_name }}</a>
61
+ {% else %}
62
+ <span class="disabled">{{ link.microsite_name }}</span>
63
+ {% endif %}
64
+
65
+ Args:
66
+ entity_type: The type of entity
67
+ entity_id: The entity's identifier
68
+ query_params: Optional query parameters
69
+
70
+ Returns:
71
+ DeepLinkInfo object with url, accessible, microsite, microsite_name
72
+ """
73
+ user = get_current_user()
74
+ return _deep_link_info(entity_type, entity_id, user=user, query_params=query_params)
75
+
76
+
77
+ def cross_link(
78
+ entity_type: str,
79
+ entity_id: Any,
80
+ label: Optional[str] = None,
81
+ css_class: str = "",
82
+ disabled_class: str = "text-gray-400 cursor-not-allowed",
83
+ icon: bool = True,
84
+ query_params: Optional[dict] = None,
85
+ ) -> Markup:
86
+ """Jinja2 function to render a permission-aware cross-site link.
87
+
88
+ Renders an active link if user has access, or a disabled span if not.
89
+
90
+ Usage:
91
+ {{ cross_link('risk_item', item.r_yid, 'Open in Risk Dashboard') }}
92
+ {{ cross_link('user', user.id, icon=False) }}
93
+ {{ cross_link('risk_item', item.r_yid, css_class='btn btn-sm') }}
94
+
95
+ Args:
96
+ entity_type: The type of entity
97
+ entity_id: The entity's identifier
98
+ label: Link text. If None, uses microsite name.
99
+ css_class: CSS classes for the link element
100
+ disabled_class: CSS classes when link is disabled
101
+ icon: Whether to show external link icon
102
+ query_params: Optional query parameters
103
+
104
+ Returns:
105
+ Markup (safe HTML) for the link or disabled span
106
+ """
107
+ try:
108
+ info = jinja_deep_link_info(entity_type, entity_id, query_params)
109
+ except Exception as e:
110
+ current_app.logger.warning(f"Cross link resolution failed: {e}")
111
+ return Markup(f'<span class="{disabled_class}">Link unavailable</span>')
112
+
113
+ link_label = label or f"Open in {info.microsite_name}"
114
+ icon_html = ' <svg class="inline w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/></svg>' if icon else ''
115
+
116
+ if info.accessible:
117
+ return Markup(
118
+ f'<a href="{info.url}" class="{css_class}" '
119
+ f'target="_blank" rel="noopener noreferrer">'
120
+ f'{link_label}{icon_html}</a>'
121
+ )
122
+ else:
123
+ return Markup(
124
+ f'<span class="{disabled_class}" '
125
+ f'title="No access to {info.microsite_name}">'
126
+ f'{link_label}</span>'
127
+ )
128
+
129
+
130
+ def cross_link_button(
131
+ entity_type: str,
132
+ entity_id: Any,
133
+ label: Optional[str] = None,
134
+ btn_class: str = "inline-flex items-center px-3 py-1.5 text-sm font-medium rounded-md",
135
+ active_class: str = "text-blue-600 bg-blue-50 hover:bg-blue-100",
136
+ disabled_class: str = "text-gray-400 bg-gray-100 cursor-not-allowed",
137
+ query_params: Optional[dict] = None,
138
+ ) -> Markup:
139
+ """Jinja2 function to render a cross-site link as a styled button.
140
+
141
+ Usage:
142
+ {{ cross_link_button('risk_item', item.r_yid, 'View Risk') }}
143
+
144
+ Args:
145
+ entity_type: The type of entity
146
+ entity_id: The entity's identifier
147
+ label: Button text. If None, uses "Open in {microsite_name}".
148
+ btn_class: Base CSS classes for the button
149
+ active_class: Additional classes when accessible
150
+ disabled_class: Additional classes when not accessible
151
+ query_params: Optional query parameters
152
+
153
+ Returns:
154
+ Markup for a styled button/link
155
+ """
156
+ try:
157
+ info = jinja_deep_link_info(entity_type, entity_id, query_params)
158
+ except Exception as e:
159
+ current_app.logger.warning(f"Cross link button failed: {e}")
160
+ return Markup(
161
+ f'<span class="{btn_class} {disabled_class}">Unavailable</span>'
162
+ )
163
+
164
+ btn_label = label or f"Open in {info.microsite_name}"
165
+ icon_svg = '<svg class="ml-1.5 w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/></svg>'
166
+
167
+ if info.accessible:
168
+ return Markup(
169
+ f'<a href="{info.url}" class="{btn_class} {active_class}" '
170
+ f'target="_blank" rel="noopener noreferrer">'
171
+ f'{btn_label}{icon_svg}</a>'
172
+ )
173
+ else:
174
+ return Markup(
175
+ f'<span class="{btn_class} {disabled_class}" '
176
+ f'title="No access to {info.microsite_name}">'
177
+ f'{btn_label}</span>'
178
+ )
179
+
180
+
181
+ def setup_deeplinks(
182
+ app: Flask,
183
+ env: Optional[str] = None,
184
+ yaml_path: Optional[str] = None,
185
+ microsite: Optional[Dict[str, Any]] = None,
186
+ entities: Optional[List[Dict[str, Any]]] = None,
187
+ # Federation options
188
+ enable_federation: bool = False,
189
+ auth_service_url: Optional[str] = None,
190
+ expose_references: bool = False,
191
+ ) -> None:
192
+ """Set up deep linking for a Flask application.
193
+
194
+ Registers Jinja2 global functions for template use and optionally
195
+ registers entity definitions and federation.
196
+
197
+ Usage Options:
198
+
199
+ Option 1: Load from YAML file
200
+ setup_deeplinks(app, yaml_path="deeplinks.yaml", env="dev")
201
+
202
+ Option 2: Register programmatically
203
+ setup_deeplinks(
204
+ app,
205
+ env="dev",
206
+ microsite={
207
+ "id": "risk",
208
+ "name": "Risk Dashboard",
209
+ "urls": {"dev": "http://localhost:5012", "uat": "...", "prd": "..."}
210
+ },
211
+ entities=[
212
+ {"type": "risk_item", "path": "/items/{id}"},
213
+ ]
214
+ )
215
+
216
+ Option 3: With federation (discover entities from other microsites)
217
+ setup_deeplinks(
218
+ app,
219
+ env="dev",
220
+ enable_federation=True, # Queries auth service + microsites
221
+ expose_references=True, # Exposes /api/v1/references
222
+ )
223
+
224
+ Option 4: Just set up Jinja (entities registered elsewhere)
225
+ setup_deeplinks(app, env="dev")
226
+
227
+ Args:
228
+ app: Flask application instance
229
+ env: Environment ('dev', 'uat', 'prd'). If None, auto-detects.
230
+ yaml_path: Path to YAML configuration file
231
+ microsite: Microsite configuration dict (id, name, urls)
232
+ entities: List of entity definitions (type, path, description)
233
+ enable_federation: If True, discover entities from other microsites
234
+ auth_service_url: URL for auth service (for federation discovery).
235
+ If None, uses app.config['AUTH_SERVICE_URL']
236
+ expose_references: If True, register /api/v1/references endpoint
237
+ """
238
+ # Set environment if specified
239
+ if env:
240
+ Environment.set(env)
241
+
242
+ # Load from YAML if specified
243
+ if yaml_path:
244
+ from .yaml_loader import load_from_yaml
245
+ load_from_yaml(yaml_path)
246
+
247
+ # Register entities programmatically if specified
248
+ if microsite and entities:
249
+ from .registry import register_entities
250
+ register_entities(
251
+ microsite_id=microsite["id"],
252
+ name=microsite.get("name"),
253
+ urls=microsite["urls"],
254
+ entities=entities,
255
+ )
256
+ # Store microsite ID for references endpoint
257
+ app.config["DEEPLINK_MICROSITE_ID"] = microsite["id"]
258
+ elif microsite or entities:
259
+ app.logger.warning(
260
+ "setup_deeplinks: Both 'microsite' and 'entities' must be provided "
261
+ "for programmatic registration. Skipping registration."
262
+ )
263
+
264
+ # Store microsite ID from YAML if loaded
265
+ if yaml_path and "DEEPLINK_MICROSITE_ID" not in app.config:
266
+ from .registry import list_microsites
267
+ microsites = list_microsites()
268
+ if microsites:
269
+ app.config["DEEPLINK_MICROSITE_ID"] = microsites[0]
270
+
271
+ # Set up federation if enabled
272
+ if enable_federation:
273
+ # Get auth service URL
274
+ federation_url = auth_service_url or app.config.get("AUTH_SERVICE_URL")
275
+
276
+ if federation_url:
277
+ from .federation import configure_federation, FederationConfig
278
+
279
+ config = FederationConfig(
280
+ auth_service_url=federation_url,
281
+ startup_fetch=True,
282
+ refresh_interval_seconds=app.config.get(
283
+ "DEEPLINK_REFRESH_INTERVAL", 300
284
+ ),
285
+ self_microsite_id=app.config.get("DEEPLINK_MICROSITE_ID"),
286
+ )
287
+ configure_federation(config)
288
+ app.logger.info(
289
+ f"Deep link federation enabled: auth_service={federation_url}"
290
+ )
291
+ else:
292
+ app.logger.warning(
293
+ "setup_deeplinks: enable_federation=True but no auth_service_url provided. "
294
+ "Set auth_service_url or app.config['AUTH_SERVICE_URL']."
295
+ )
296
+
297
+ # Expose references endpoint if requested
298
+ if expose_references:
299
+ from .blueprint import references_bp
300
+
301
+ app.register_blueprint(references_bp)
302
+ app.logger.info("Deep link references endpoint registered: /api/v1/references")
303
+
304
+ # Register Jinja2 globals
305
+ app.jinja_env.globals["deep_link"] = deep_link
306
+ app.jinja_env.globals["deep_link_info"] = jinja_deep_link_info
307
+ app.jinja_env.globals["cross_link"] = cross_link
308
+ app.jinja_env.globals["cross_link_button"] = cross_link_button
309
+
310
+ # Log setup
311
+ from .registry import list_entity_types, list_microsites
312
+
313
+ app.logger.info(
314
+ f"Deep links configured: env={Environment.get()}, "
315
+ f"microsites={len(list_microsites())}, entities={len(list_entity_types())}"
316
+ )