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,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
|
+
)
|