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,155 @@
|
|
|
1
|
+
"""Flask blueprint for exposing entity references via API.
|
|
2
|
+
|
|
3
|
+
This blueprint exposes the /api/v1/references endpoint that returns
|
|
4
|
+
this microsite's entity definitions for federation discovery.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
from yirifi_ops_auth.deeplink import setup_deeplinks
|
|
8
|
+
|
|
9
|
+
# In your app factory
|
|
10
|
+
setup_deeplinks(
|
|
11
|
+
app,
|
|
12
|
+
expose_references=True, # Registers this blueprint
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
# Or manually:
|
|
16
|
+
from yirifi_ops_auth.deeplink.blueprint import references_bp
|
|
17
|
+
app.register_blueprint(references_bp)
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from datetime import datetime
|
|
21
|
+
from flask import Blueprint, jsonify, request, current_app
|
|
22
|
+
|
|
23
|
+
from .registry import (
|
|
24
|
+
get_microsite_config,
|
|
25
|
+
list_entity_types_for_microsite,
|
|
26
|
+
get_entity_definition,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
references_bp = Blueprint("deeplink_references", __name__, url_prefix="/api/v1")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@references_bp.route("/references", methods=["GET"])
|
|
34
|
+
def get_references():
|
|
35
|
+
"""Expose this microsite's entity definitions for federation.
|
|
36
|
+
|
|
37
|
+
This endpoint allows other microsites to discover what entity types
|
|
38
|
+
this microsite serves and how to construct URLs for them.
|
|
39
|
+
|
|
40
|
+
Query Parameters:
|
|
41
|
+
entity_type (str, optional): Filter to specific entity type
|
|
42
|
+
include_urls (bool, default=true): Include URL mappings in response
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
JSON response with schema:
|
|
46
|
+
{
|
|
47
|
+
"schema_version": "1.0",
|
|
48
|
+
"microsite": {
|
|
49
|
+
"id": "risk",
|
|
50
|
+
"name": "Risk Dashboard",
|
|
51
|
+
"urls": {
|
|
52
|
+
"dev": "http://localhost:5012",
|
|
53
|
+
"uat": "https://risk-uat.ops.yirifi.com",
|
|
54
|
+
"prd": "https://risk.ops.yirifi.com"
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"entities": [
|
|
58
|
+
{
|
|
59
|
+
"type": "risk_item",
|
|
60
|
+
"path": "/items/{id}",
|
|
61
|
+
"description": "Risk management item"
|
|
62
|
+
}
|
|
63
|
+
],
|
|
64
|
+
"timestamp": "2024-12-23T10:30:00Z"
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
Response Headers:
|
|
68
|
+
Cache-Control: public, max-age=300
|
|
69
|
+
X-Federation-Version: 1.0
|
|
70
|
+
"""
|
|
71
|
+
# Get microsite config (set during setup)
|
|
72
|
+
microsite_id = current_app.config.get("DEEPLINK_MICROSITE_ID")
|
|
73
|
+
if not microsite_id:
|
|
74
|
+
return (
|
|
75
|
+
jsonify(
|
|
76
|
+
{
|
|
77
|
+
"error": "microsite_not_configured",
|
|
78
|
+
"message": "This microsite has not configured deep linking. "
|
|
79
|
+
"Call setup_deeplinks() with a microsite parameter.",
|
|
80
|
+
}
|
|
81
|
+
),
|
|
82
|
+
500,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
config = get_microsite_config(microsite_id)
|
|
86
|
+
if not config:
|
|
87
|
+
return (
|
|
88
|
+
jsonify(
|
|
89
|
+
{
|
|
90
|
+
"error": "microsite_not_found",
|
|
91
|
+
"message": f"Microsite '{microsite_id}' not registered in local registry.",
|
|
92
|
+
}
|
|
93
|
+
),
|
|
94
|
+
500,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Parse query parameters
|
|
98
|
+
entity_type_filter = request.args.get("entity_type")
|
|
99
|
+
include_urls = request.args.get("include_urls", "true").lower() == "true"
|
|
100
|
+
|
|
101
|
+
# Get entities for this microsite
|
|
102
|
+
entity_types = list_entity_types_for_microsite(microsite_id)
|
|
103
|
+
|
|
104
|
+
if entity_type_filter:
|
|
105
|
+
entity_types = [et for et in entity_types if et == entity_type_filter]
|
|
106
|
+
|
|
107
|
+
entities = []
|
|
108
|
+
for entity_type in entity_types:
|
|
109
|
+
defn = get_entity_definition(entity_type)
|
|
110
|
+
if defn:
|
|
111
|
+
entity_data = {
|
|
112
|
+
"type": defn.entity_type,
|
|
113
|
+
"path": defn.path_template,
|
|
114
|
+
}
|
|
115
|
+
if defn.description:
|
|
116
|
+
entity_data["description"] = defn.description
|
|
117
|
+
entities.append(entity_data)
|
|
118
|
+
|
|
119
|
+
# Build response
|
|
120
|
+
response_data = {
|
|
121
|
+
"schema_version": "1.0",
|
|
122
|
+
"microsite": {
|
|
123
|
+
"id": config.id,
|
|
124
|
+
"name": config.name,
|
|
125
|
+
},
|
|
126
|
+
"entities": entities,
|
|
127
|
+
"timestamp": datetime.utcnow().isoformat() + "Z",
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if include_urls:
|
|
131
|
+
response_data["microsite"]["urls"] = config.urls
|
|
132
|
+
|
|
133
|
+
# Build response with cache headers
|
|
134
|
+
resp = jsonify(response_data)
|
|
135
|
+
resp.headers["Cache-Control"] = "public, max-age=300"
|
|
136
|
+
resp.headers["X-Federation-Version"] = "1.0"
|
|
137
|
+
|
|
138
|
+
return resp
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@references_bp.route("/references/health", methods=["GET"])
|
|
142
|
+
def references_health():
|
|
143
|
+
"""Health check for the references endpoint.
|
|
144
|
+
|
|
145
|
+
Returns simple status for monitoring/load balancer health checks.
|
|
146
|
+
"""
|
|
147
|
+
microsite_id = current_app.config.get("DEEPLINK_MICROSITE_ID")
|
|
148
|
+
|
|
149
|
+
return jsonify(
|
|
150
|
+
{
|
|
151
|
+
"status": "ok",
|
|
152
|
+
"microsite_id": microsite_id,
|
|
153
|
+
"federation_version": "1.0",
|
|
154
|
+
}
|
|
155
|
+
)
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Thread-safe environment configuration using contextvars.
|
|
2
|
+
|
|
3
|
+
Provides isolation for environment settings across threads and async contexts,
|
|
4
|
+
with a context manager for temporary overrides.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from contextvars import ContextVar
|
|
9
|
+
from contextlib import contextmanager
|
|
10
|
+
from typing import Optional, Generator
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# Thread-safe environment storage
|
|
14
|
+
_env_var: ContextVar[Optional[str]] = ContextVar('deeplink_env', default=None)
|
|
15
|
+
|
|
16
|
+
# Valid environments
|
|
17
|
+
VALID_ENVIRONMENTS = ("dev", "uat", "prd")
|
|
18
|
+
|
|
19
|
+
# Environment name aliases
|
|
20
|
+
ENV_ALIASES = {
|
|
21
|
+
"development": "dev",
|
|
22
|
+
"local": "dev",
|
|
23
|
+
"staging": "uat",
|
|
24
|
+
"test": "uat",
|
|
25
|
+
"production": "prd",
|
|
26
|
+
"prod": "prd",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Environment:
|
|
31
|
+
"""Thread-safe environment configuration.
|
|
32
|
+
|
|
33
|
+
Usage:
|
|
34
|
+
# Get current environment
|
|
35
|
+
env = Environment.get() # -> 'dev', 'uat', or 'prd'
|
|
36
|
+
|
|
37
|
+
# Set environment
|
|
38
|
+
Environment.set('prd')
|
|
39
|
+
|
|
40
|
+
# Temporary override (for testing)
|
|
41
|
+
with Environment.override('uat'):
|
|
42
|
+
# Inside this block, env is 'uat'
|
|
43
|
+
do_something()
|
|
44
|
+
# Outside, env is restored to previous value
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
VALID = VALID_ENVIRONMENTS
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def get(cls) -> str:
|
|
51
|
+
"""Get the current environment.
|
|
52
|
+
|
|
53
|
+
Resolution order:
|
|
54
|
+
1. Explicitly set via Environment.set() (thread-local)
|
|
55
|
+
2. YIRIFI_ENV environment variable
|
|
56
|
+
3. FLASK_ENV environment variable
|
|
57
|
+
4. Default to 'dev'
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Environment string: 'dev', 'uat', or 'prd'
|
|
61
|
+
"""
|
|
62
|
+
# Check thread-local context first
|
|
63
|
+
env = _env_var.get()
|
|
64
|
+
if env is not None:
|
|
65
|
+
return env
|
|
66
|
+
|
|
67
|
+
# Fall back to environment variables
|
|
68
|
+
return cls._from_env_vars()
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def _from_env_vars(cls) -> str:
|
|
72
|
+
"""Read environment from OS environment variables."""
|
|
73
|
+
env = os.getenv("YIRIFI_ENV") or os.getenv("FLASK_ENV", "dev")
|
|
74
|
+
return cls._normalize(env)
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
def _normalize(cls, env: str) -> str:
|
|
78
|
+
"""Normalize environment name to standard form.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
env: Raw environment string
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Normalized environment: 'dev', 'uat', or 'prd'
|
|
85
|
+
"""
|
|
86
|
+
env = env.lower()
|
|
87
|
+
env = ENV_ALIASES.get(env, env)
|
|
88
|
+
return env if env in VALID_ENVIRONMENTS else "dev"
|
|
89
|
+
|
|
90
|
+
@classmethod
|
|
91
|
+
def set(cls, env: str) -> None:
|
|
92
|
+
"""Set the environment for the current context.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
env: Environment string ('dev', 'uat', or 'prd') or alias
|
|
96
|
+
|
|
97
|
+
Raises:
|
|
98
|
+
ValueError: If env is not a valid environment or alias
|
|
99
|
+
"""
|
|
100
|
+
env_lower = env.lower()
|
|
101
|
+
# Check if it's a valid environment or a known alias
|
|
102
|
+
if env_lower not in VALID_ENVIRONMENTS and env_lower not in ENV_ALIASES:
|
|
103
|
+
raise ValueError(
|
|
104
|
+
f"Invalid environment: {env}. Must be one of {VALID_ENVIRONMENTS} "
|
|
105
|
+
f"or aliases: {list(ENV_ALIASES.keys())}"
|
|
106
|
+
)
|
|
107
|
+
normalized = cls._normalize(env)
|
|
108
|
+
_env_var.set(normalized)
|
|
109
|
+
|
|
110
|
+
@classmethod
|
|
111
|
+
def reset(cls) -> None:
|
|
112
|
+
"""Reset environment to auto-detection (clear explicit setting)."""
|
|
113
|
+
_env_var.set(None)
|
|
114
|
+
|
|
115
|
+
@classmethod
|
|
116
|
+
@contextmanager
|
|
117
|
+
def override(cls, env: str) -> Generator[None, None, None]:
|
|
118
|
+
"""Temporarily override the environment.
|
|
119
|
+
|
|
120
|
+
Useful for testing or when you need to resolve links for a different
|
|
121
|
+
environment temporarily.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
env: Environment to use within the context
|
|
125
|
+
|
|
126
|
+
Example:
|
|
127
|
+
Environment.set('dev')
|
|
128
|
+
print(Environment.get()) # 'dev'
|
|
129
|
+
|
|
130
|
+
with Environment.override('prd'):
|
|
131
|
+
print(Environment.get()) # 'prd'
|
|
132
|
+
|
|
133
|
+
print(Environment.get()) # 'dev' (restored)
|
|
134
|
+
"""
|
|
135
|
+
normalized = cls._normalize(env)
|
|
136
|
+
if normalized not in VALID_ENVIRONMENTS:
|
|
137
|
+
raise ValueError(
|
|
138
|
+
f"Invalid environment: {env}. Must be one of {VALID_ENVIRONMENTS}"
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
token = _env_var.set(normalized)
|
|
142
|
+
try:
|
|
143
|
+
yield
|
|
144
|
+
finally:
|
|
145
|
+
_env_var.reset(token)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
# Convenience functions for backwards compatibility and simpler imports
|
|
149
|
+
def get_environment() -> str:
|
|
150
|
+
"""Get the current environment. Alias for Environment.get()."""
|
|
151
|
+
return Environment.get()
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def set_environment(env: str) -> None:
|
|
155
|
+
"""Set the environment. Alias for Environment.set()."""
|
|
156
|
+
Environment.set(env)
|