jaaql-middleware-python 5.1.0__tar.gz → 5.2.7__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.
- {jaaql_middleware_python-5.1.0/jaaql_middleware_python.egg-info → jaaql_middleware_python-5.2.7}/PKG-INFO +1 -1
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/constants.py +2 -1
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/db/db_pg_interface.py +35 -1
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/documentation/documentation_internal.py +23 -1
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/mvc/controller.py +31 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/mvc/model.py +178 -50
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/scripts/03.install_super_user.handwritten.sql +17 -7
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/services/migrations_manager_service.py +50 -2
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7/jaaql_middleware_python.egg-info}/PKG-INFO +1 -1
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/LICENSE.txt +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/README.md +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/__init__.py +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/config/__init__.py +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/config/config-docker.ini +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/config/config-test.ini +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/config/config.ini +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/config_constants.py +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/db/__init__.py +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/db/db_interface.py +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/db/db_utils.py +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/db/db_utils_no_circ.py +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/documentation/__init__.py +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/documentation/documentation_public.py +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/documentation/documentation_shared.py +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/email/__init__.py +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/email/email_manager.py +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/email/email_manager_service.py +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/email/patch_ems.py +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/exceptions/__init__.py +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/exceptions/custom_http_status.py +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/exceptions/http_status_exception.py +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/exceptions/jaaql_interpretable_handled_errors.py +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/exceptions/not_yet_implement_exception.py +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/generated_constants.py +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/interpreter/__init__.py +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/interpreter/interpret_jaaql.py +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/jaaql.py +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/migrations/__init__.py +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/migrations/migrations.py +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/mvc/__init__.py +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/mvc/base_controller.py +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/mvc/base_model.py +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/mvc/controller_interface.py +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/mvc/exception_queries.py +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/mvc/generated_queries.py +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/mvc/handmade_queries.py +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/mvc/model_interface.py +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/mvc/response.py +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/openapi/__init__.py +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/openapi/swagger_documentation.py +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/patch.py +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/scripts/01.install_domains.generated.sql +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/scripts/02.install_super_user.exceptions.sql +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/scripts/04.install_jaaql_data_structures.generated.sql +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/scripts/05.install_static_data.generated.sql +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/scripts/06.install_jaaql.exceptions.sql +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/scripts/ZZZZ.generated_functions_views_and_permissions.sql +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/scripts/ZZZZ.reset_references.sql +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/scripts/swagger_template.html +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/services/__init__.py +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/services/cached_canned_query_service.py +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/services/patch_mms.py +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/services/patch_shared_var_service.py +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/services/shared_var_service.py +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/utilities/__init__.py +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/utilities/bootstrap_secrets.py +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/utilities/cron.py +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/utilities/crypt_utils.py +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/utilities/options.py +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/utilities/utils.py +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/utilities/utils_no_project_imports.py +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/utilities/vault.py +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql_middleware_python.egg-info/SOURCES.txt +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql_middleware_python.egg-info/dependency_links.txt +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql_middleware_python.egg-info/requires.txt +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql_middleware_python.egg-info/top_level.txt +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/setup.cfg +0 -0
- {jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: jaaql-middleware-python
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.2.7
|
|
4
4
|
Summary: The jaaql package, allowing for rapid development and deployment of RESTful HTTP applications
|
|
5
5
|
Home-page: https://github.com/JAAQL/JAAQL-middleware-python
|
|
6
6
|
Author: Software Quality Measurement and Improvement bv
|
|
@@ -207,8 +207,9 @@ USERNAME__anonymous = "anonymous"
|
|
|
207
207
|
PASSWORD__anonymous = "jaaql_public_password"
|
|
208
208
|
ROLE__jaaql = "jaaql"
|
|
209
209
|
ROLE__postgres = "postgres"
|
|
210
|
+
ROLE__dba = "dba"
|
|
210
211
|
|
|
211
212
|
PROTOCOL__postgres = "postgresql://"
|
|
212
213
|
|
|
213
|
-
VERSION = "5.
|
|
214
|
+
VERSION = "5.2.7"
|
|
214
215
|
|
|
@@ -28,6 +28,40 @@ QUERY__dba_query = "SELECT pg_has_role(datdba::regrole, 'MEMBER') FROM pg_databa
|
|
|
28
28
|
QUERY__dba_query_external = "SELECT pg_has_role(datdba::regrole, 'MEMBER') FROM pg_database WHERE datname = current_database();"
|
|
29
29
|
|
|
30
30
|
|
|
31
|
+
def _escape_unescaped_percent(query: str) -> str:
|
|
32
|
+
# psycopg3 scans every '%' in the SQL when parameters are supplied and
|
|
33
|
+
# rejects anything that isn't a placeholder or '%%'. JAAQL uses named
|
|
34
|
+
# parameters only (:parameter -> %(name)s), so positional %s/%b/%t must
|
|
35
|
+
# never survive: any '%s' in author SQL is a literal inside a string
|
|
36
|
+
# (LIKE '%saas%', LIKE '%twynstra%', etc.), not a placeholder. Escape
|
|
37
|
+
# everything except '%%' and '%(name)X'.
|
|
38
|
+
out = []
|
|
39
|
+
i = 0
|
|
40
|
+
n = len(query)
|
|
41
|
+
while i < n:
|
|
42
|
+
if query[i] != '%':
|
|
43
|
+
out.append(query[i])
|
|
44
|
+
i += 1
|
|
45
|
+
continue
|
|
46
|
+
|
|
47
|
+
nxt = query[i + 1] if i + 1 < n else ''
|
|
48
|
+
if nxt == '%':
|
|
49
|
+
out.append('%%')
|
|
50
|
+
i += 2
|
|
51
|
+
elif nxt == '(':
|
|
52
|
+
close = query.find(')', i + 2)
|
|
53
|
+
if close == -1 or close + 1 >= n:
|
|
54
|
+
out.append('%%')
|
|
55
|
+
i += 1
|
|
56
|
+
else:
|
|
57
|
+
out.append(query[i:close + 2])
|
|
58
|
+
i = close + 2
|
|
59
|
+
else:
|
|
60
|
+
out.append('%%')
|
|
61
|
+
i += 1
|
|
62
|
+
return ''.join(out)
|
|
63
|
+
|
|
64
|
+
|
|
31
65
|
class DBPGInterface(DBInterface):
|
|
32
66
|
|
|
33
67
|
HOST_POOLS = {}
|
|
@@ -210,7 +244,7 @@ class DBPGInterface(DBInterface):
|
|
|
210
244
|
if parameters is None or len(parameters.keys()) == 0:
|
|
211
245
|
cursor.execute(query, prepare=do_prepare)
|
|
212
246
|
else:
|
|
213
|
-
cursor.execute(query, parameters, prepare=do_prepare)
|
|
247
|
+
cursor.execute(_escape_unescaped_percent(query), parameters, prepare=do_prepare)
|
|
214
248
|
if cursor.description is None:
|
|
215
249
|
return [], [], []
|
|
216
250
|
else:
|
|
@@ -605,4 +605,26 @@ DOCUMENTATION__security_event = SwaggerDocumentation(
|
|
|
605
605
|
],
|
|
606
606
|
response=RES__allow_all
|
|
607
607
|
)
|
|
608
|
-
)
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
DOCUMENTATION__report_sentinel_error = SwaggerDocumentation(
|
|
611
|
+
tags="Reporting",
|
|
612
|
+
security=False, # PUBLIC: also receives self-posts from the in-core sentinel reporter (SENTINEL_URL=_)
|
|
613
|
+
methods=SwaggerMethod(
|
|
614
|
+
name="Report error",
|
|
615
|
+
description="Logs an application error (user_agent + ip_address encrypted at rest) and triggers Sentinel "
|
|
616
|
+
"alert processing. Never returns 500 (would otherwise make JAAQL report itself recursively).",
|
|
617
|
+
method=REST__POST,
|
|
618
|
+
body=[
|
|
619
|
+
SwaggerArgumentResponse(name="location", description="URL/location where the error occurred", arg_type=str, example=["https://app/index.html"]),
|
|
620
|
+
SwaggerArgumentResponse(name="source_file", description="Source file the error occurred in", arg_type=str, example=["common.js"]),
|
|
621
|
+
SwaggerArgumentResponse(name="error_condensed", description="Condensed error (truncated to 200 chars for grouping)", arg_type=str, example=["Uncaught TypeError"]),
|
|
622
|
+
SwaggerArgumentResponse(name="stacktrace", description="Full stacktrace", arg_type=str, example=["at <anonymous>:1:16"]),
|
|
623
|
+
SwaggerArgumentResponse(name="version", description="Product version", arg_type=str, example=["1.2.3"]),
|
|
624
|
+
SwaggerArgumentResponse(name="source_system", description="Originating system/application", arg_type=str, example=["JAAQL"]),
|
|
625
|
+
SwaggerArgumentResponse(name="file_line_number", description="Line number", arg_type=int, required=False, condition="If available", example=[123]),
|
|
626
|
+
SwaggerArgumentResponse(name="file_col_number", description="Column number", arg_type=int, required=False, condition="If available", example=[21]),
|
|
627
|
+
SwaggerArgumentResponse(name="user_agent", description="Browser user agent (encrypted at rest). Absent on internal self-posts.", arg_type=str, required=False, condition="If present", example=["Mozilla/5.0"]),
|
|
628
|
+
]
|
|
629
|
+
)
|
|
630
|
+
)
|
|
@@ -13,6 +13,7 @@ from flask import request
|
|
|
13
13
|
import queue
|
|
14
14
|
|
|
15
15
|
from jaaql.utilities.utils_no_project_imports import COOKIE_OIDC, COOKIE_OIDC_RETURN
|
|
16
|
+
from jaaql.interpreter.interpret_jaaql import KEY_query, KEY_parameters
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
class JAAQLController(BaseJAAQLController):
|
|
@@ -221,3 +222,33 @@ class JAAQLController(BaseJAAQLController):
|
|
|
221
222
|
{"state": http_inputs["state"], "oidc_cookie": request.cookies.get(COOKIE_OIDC)},
|
|
222
223
|
response
|
|
223
224
|
)
|
|
225
|
+
|
|
226
|
+
@self.publish_route(ENDPOINT__report_sentinel_error, DOCUMENTATION__report_sentinel_error, True)
|
|
227
|
+
def report_sentinel_error(http_inputs: dict, ip_address: str):
|
|
228
|
+
# Public error-reporting endpoint, moved here from the deprecated jaaql-sentinel-middleware.
|
|
229
|
+
# MUST NEVER return a 500: a 500 makes JAAQL self-report (SENTINEL_URL=_) and recurse.
|
|
230
|
+
ins_error = (
|
|
231
|
+
"insert into error (location, source_file, user_agent, ip_address, error_condensed, stacktrace, "
|
|
232
|
+
"file_line_number, file_col_number, version, source_system) "
|
|
233
|
+
"VALUES (:location, :source_file, #user_agent, #ip_address, left(:error_condensed, 200), :stacktrace, "
|
|
234
|
+
":file_line_number, :file_col_number, :version, :source_system) "
|
|
235
|
+
"RETURNING id::text as error_id"
|
|
236
|
+
)
|
|
237
|
+
try:
|
|
238
|
+
http_inputs["ip_address"] = ip_address # plaintext -> #ip_address (field-level encrypted)
|
|
239
|
+
inserted = self.model.submit(
|
|
240
|
+
{KEY_query: ins_error, KEY_parameters: http_inputs, KEY__application: "sentinel"},
|
|
241
|
+
ROLE__dba, as_objects=True, singleton=True
|
|
242
|
+
)
|
|
243
|
+
self.model.submit(
|
|
244
|
+
{
|
|
245
|
+
KEY_query: 'SELECT "error.process_alert"(:id, :raw_ip_address)',
|
|
246
|
+
KEY_parameters: {"id": inserted["error_id"], "raw_ip_address": ip_address},
|
|
247
|
+
KEY__application: "sentinel"
|
|
248
|
+
},
|
|
249
|
+
ROLE__dba
|
|
250
|
+
)
|
|
251
|
+
except Exception as ex:
|
|
252
|
+
import traceback
|
|
253
|
+
traceback.print_exc()
|
|
254
|
+
raise HttpStatusException(str(ex))
|
|
@@ -124,6 +124,51 @@ QUERY__purge_rendered_document = "DELETE FROM document_request WHERE completed i
|
|
|
124
124
|
QUERY__fetch_rendered_document = "SELECT app.base_url, rd.uuid as document_id, 'pdf' as render_as, rd.file_name, rd.create_file, rd.completed, rd.encrypted_access_token as oauth_token FROM document_request rd INNER JOIN document_template able ON rd.template = able.name INNER JOIN application app ON app.name = rd.application WHERE rd.uuid = :document_id"
|
|
125
125
|
QUERY__fetch_cron_jobs = "SELECT * FROM remote_procedure WHERE access = 'S'";
|
|
126
126
|
|
|
127
|
+
|
|
128
|
+
def _rewrite_endpoint_to_origin(endpoint_url: str, target_origin_url: str) -> str:
|
|
129
|
+
# Swap scheme+host of `endpoint_url` for those of `target_origin_url`,
|
|
130
|
+
# preserving path/query/fragment. Returns endpoint_url unchanged if either
|
|
131
|
+
# input is missing a scheme or host. Used to keep the OIDC auth iframe
|
|
132
|
+
# same-origin with the parent app — see fetch_redirect_uri.
|
|
133
|
+
if not endpoint_url or not target_origin_url:
|
|
134
|
+
return endpoint_url
|
|
135
|
+
parsed_endpoint = urllib.parse.urlparse(endpoint_url)
|
|
136
|
+
parsed_origin = urllib.parse.urlparse(target_origin_url)
|
|
137
|
+
if not parsed_origin.scheme or not parsed_origin.netloc:
|
|
138
|
+
return endpoint_url
|
|
139
|
+
return urllib.parse.urlunparse((
|
|
140
|
+
parsed_origin.scheme,
|
|
141
|
+
parsed_origin.netloc,
|
|
142
|
+
parsed_endpoint.path,
|
|
143
|
+
parsed_endpoint.params,
|
|
144
|
+
parsed_endpoint.query,
|
|
145
|
+
parsed_endpoint.fragment,
|
|
146
|
+
))
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _keycloak_backchannel_origin(discovery_url: str) -> str:
|
|
150
|
+
# Canonical scheme+host that JAAQL uses for SERVER-side Keycloak calls
|
|
151
|
+
# (PAR, token, JWKS). With per-realm frontendUrl set, the discovery doc
|
|
152
|
+
# advertises endpoint URLs against the BROWSER host (the app's domain),
|
|
153
|
+
# which routes through the same-origin nginx proxy. That proxy terminates
|
|
154
|
+
# the middleware's mTLS handshake at the app's nginx and opens a fresh
|
|
155
|
+
# upstream TLS connection — the FAPI client cert never reaches Keycloak.
|
|
156
|
+
# So for backchannel we must skip the proxy.
|
|
157
|
+
#
|
|
158
|
+
# Source of truth: KEYCLOAK_URL env var (already set per-deployment).
|
|
159
|
+
# Fallback: the origin of user_registry.discovery_url, which is also the
|
|
160
|
+
# canonical server-side host for the deployment.
|
|
161
|
+
env_url = os.environ.get("KEYCLOAK_URL", "").strip().rstrip("/")
|
|
162
|
+
if env_url:
|
|
163
|
+
return env_url
|
|
164
|
+
if not discovery_url:
|
|
165
|
+
return ""
|
|
166
|
+
parsed = urllib.parse.urlparse(discovery_url)
|
|
167
|
+
if not parsed.scheme or not parsed.netloc:
|
|
168
|
+
return ""
|
|
169
|
+
return f"{parsed.scheme}://{parsed.netloc}"
|
|
170
|
+
|
|
171
|
+
|
|
127
172
|
class JAAQLModel(BaseJAAQLModel):
|
|
128
173
|
VERIFICATION_QUEUE = None
|
|
129
174
|
|
|
@@ -648,9 +693,16 @@ WHERE
|
|
|
648
693
|
return discovery
|
|
649
694
|
|
|
650
695
|
def fetch_jwks_client(self, database: str, provider: str, tenant: str, discovery):
|
|
651
|
-
jwks_url = discovery.get("jwks_uri")
|
|
696
|
+
jwks_url = discovery.get("jwks_uri")
|
|
652
697
|
if not jwks_url:
|
|
653
698
|
raise Exception(f"Discovery document for {provider}, {tenant} did not have JWKS url")
|
|
699
|
+
# Force backchannel: skip the app same-origin proxy and hit Keycloak directly.
|
|
700
|
+
# JWKS doesn't require mTLS, but keeping all backchannel calls on the same
|
|
701
|
+
# canonical host avoids surprise routing and an extra hop. KEYCLOAK_URL env
|
|
702
|
+
# is the source of truth; if absent we leave the discovery URL untouched.
|
|
703
|
+
backchannel_origin = os.environ.get("KEYCLOAK_URL", "").strip().rstrip("/")
|
|
704
|
+
if backchannel_origin:
|
|
705
|
+
jwks_url = _rewrite_endpoint_to_origin(jwks_url, backchannel_origin)
|
|
654
706
|
lookup = database + ":" + provider + ":" + tenant
|
|
655
707
|
jwks = self.JWKS_CACHE.get(lookup)
|
|
656
708
|
if not jwks:
|
|
@@ -697,13 +749,25 @@ WHERE
|
|
|
697
749
|
discovery = self.fetch_discovery_content(database, provider, tenant, user_registry[KG__user_registry__discovery_url])
|
|
698
750
|
jwk_client = self.fetch_jwks_client(database, provider, tenant, discovery)
|
|
699
751
|
|
|
700
|
-
|
|
701
|
-
if
|
|
702
|
-
|
|
752
|
+
oidc_issuer_override = os.environ.get("OIDC_ISSUER", "").strip()
|
|
753
|
+
if oidc_issuer_override:
|
|
754
|
+
expected_issuer = oidc_issuer_override
|
|
755
|
+
else:
|
|
756
|
+
expected_issuer = discovery.get("issuer")
|
|
757
|
+
if not expected_issuer:
|
|
758
|
+
raise Exception(f"Failed to fetch issuer in discovery document for {provider}, {tenant}")
|
|
759
|
+
# JARMS and id_tokens echo Keycloak's view of the host the BROWSER
|
|
760
|
+
# hit — which, with the same-origin proxy in place, is the app's
|
|
761
|
+
# base_url, not the server-side discovery host. Rewrite the
|
|
762
|
+
# discovery issuer to match so jwt.decode(...issuer=...) lines up.
|
|
763
|
+
expected_issuer = _rewrite_endpoint_to_origin(expected_issuer, application_tuple[KG__application__base_url])
|
|
703
764
|
|
|
704
765
|
token_endpoint = discovery.get("token_endpoint")
|
|
705
766
|
if not token_endpoint:
|
|
706
767
|
raise Exception(f"No token endpoint for {provider}, {tenant}")
|
|
768
|
+
# Force backchannel: hit Keycloak directly (skip the app same-origin proxy)
|
|
769
|
+
# so the FAPI client cert presented at TLS handshake actually reaches Keycloak.
|
|
770
|
+
token_endpoint = _rewrite_endpoint_to_origin(token_endpoint, _keycloak_backchannel_origin(user_registry[KG__user_registry__discovery_url]))
|
|
707
771
|
allowed_algs = discovery.get("id_token_signing_alg_values_supported")
|
|
708
772
|
if not allowed_algs:
|
|
709
773
|
raise Exception(f"No allowed algs for {provider}, {tenant}")
|
|
@@ -782,7 +846,7 @@ WHERE
|
|
|
782
846
|
})
|
|
783
847
|
|
|
784
848
|
token_response = self.idp_session.post(
|
|
785
|
-
token_endpoint
|
|
849
|
+
token_endpoint,
|
|
786
850
|
data=token_request_payload,
|
|
787
851
|
**kwargs
|
|
788
852
|
)
|
|
@@ -850,6 +914,47 @@ WHERE
|
|
|
850
914
|
response.response_code = HTTPStatus.FOUND
|
|
851
915
|
response.raw_headers["Location"] = oidc_state[KEY__redirect_uri]
|
|
852
916
|
|
|
917
|
+
def _run_federation_procedure(self, database_user_registry, application, schema,
|
|
918
|
+
account_id, provider, tenant, email, sub, id_payload):
|
|
919
|
+
# Runs the app database's federation procedure (e.g. linking the JAAQL account to an
|
|
920
|
+
# app user row). Safe to call on every login: the procedure is expected to be idempotent.
|
|
921
|
+
if database_user_registry is None or application is None or schema is None:
|
|
922
|
+
return
|
|
923
|
+
|
|
924
|
+
db_params = {"tenant": tenant, "application": application, "account_id": account_id, "provider": provider,
|
|
925
|
+
"email": email, "sub": sub}
|
|
926
|
+
parameters = fetch_parameters_for_federation_procedure(self.jaaql_lookup_connection,
|
|
927
|
+
database_user_registry[KG__database_user_registry__federation_procedure])
|
|
928
|
+
for claim in parameters:
|
|
929
|
+
claim_name = claim[KG__federation_procedure_parameter__name]
|
|
930
|
+
db_params[claim_name] = id_payload.get(claim_name, "")
|
|
931
|
+
|
|
932
|
+
procedure_name = database_user_registry[KG__database_user_registry__federation_procedure]
|
|
933
|
+
if re.match(REGEX__dmbs_procedure_name, procedure_name) is None:
|
|
934
|
+
raise HttpStatusException("Unsafe data federation procedure")
|
|
935
|
+
|
|
936
|
+
procedure_params = []
|
|
937
|
+
for key, _ in db_params.items():
|
|
938
|
+
if re.match(REGEX__dmbs_object_name, key) is None:
|
|
939
|
+
raise HttpStatusException("Unsafe data federation parameter " + key)
|
|
940
|
+
procedure_params.append(f"{key} => :{key}")
|
|
941
|
+
|
|
942
|
+
submit_data = {
|
|
943
|
+
KEY__application: application,
|
|
944
|
+
KEY_query: f"SELECT * FROM \"{procedure_name}\"({', '.join(procedure_params)});",
|
|
945
|
+
KEY__schema: schema,
|
|
946
|
+
KEY__parameters: db_params
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
print("Preparing federating procedure")
|
|
950
|
+
|
|
951
|
+
submit(self.vault, self.config, self.get_db_crypt_key(),
|
|
952
|
+
self.jaaql_lookup_connection, submit_data, ROLE__jaaql,
|
|
953
|
+
None, self.cached_canned_query_service, as_objects=True, singleton=True)
|
|
954
|
+
|
|
955
|
+
print("Federated user")
|
|
956
|
+
print(submit_data)
|
|
957
|
+
|
|
853
958
|
def _federate_and_issue_jwt(self, sub, provider, tenant, id_payload, ip_address, response,
|
|
854
959
|
application=None, schema=None, database_user_registry=None):
|
|
855
960
|
account = None
|
|
@@ -862,6 +967,18 @@ WHERE
|
|
|
862
967
|
email_verified = id_payload.get('email_verified') if require_email_verification else True
|
|
863
968
|
if email_verified:
|
|
864
969
|
mark_account_registered(self.jaaql_lookup_connection, account[KG__account__id])
|
|
970
|
+
account[KG__account__email_verified] = True
|
|
971
|
+
|
|
972
|
+
# Re-run the federation procedure for already-known accounts so the app-side
|
|
973
|
+
# person<->account linkage self-heals when the app user was created after the
|
|
974
|
+
# account was first federated. Best-effort: a federation procedure failure (e.g. the
|
|
975
|
+
# app user does not exist yet) must not break an otherwise valid login.
|
|
976
|
+
try:
|
|
977
|
+
self._run_federation_procedure(database_user_registry, application, schema,
|
|
978
|
+
account[KG__account__id], provider, tenant,
|
|
979
|
+
id_payload.get('email'), sub, id_payload)
|
|
980
|
+
except Exception as e:
|
|
981
|
+
print(f"Federation procedure (existing-account re-link) skipped: {e}")
|
|
865
982
|
|
|
866
983
|
except HttpSingletonStatusException:
|
|
867
984
|
# User does not exist, federate it
|
|
@@ -874,40 +991,8 @@ WHERE
|
|
|
874
991
|
print("new federated user with account id " + account_id)
|
|
875
992
|
account = account__select(self.jaaql_lookup_connection, self.get_db_crypt_key(), account_id)
|
|
876
993
|
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
"email": email, "sub": sub}
|
|
880
|
-
parameters = fetch_parameters_for_federation_procedure(self.jaaql_lookup_connection,
|
|
881
|
-
database_user_registry[KG__database_user_registry__federation_procedure])
|
|
882
|
-
for claim in parameters:
|
|
883
|
-
claim_name = claim[KG__federation_procedure_parameter__name]
|
|
884
|
-
db_params[claim_name] = id_payload.get(claim_name, "")
|
|
885
|
-
|
|
886
|
-
procedure_name = database_user_registry[KG__database_user_registry__federation_procedure]
|
|
887
|
-
if re.match(REGEX__dmbs_procedure_name, procedure_name) is None:
|
|
888
|
-
raise HttpStatusException("Unsafe data federation procedure")
|
|
889
|
-
|
|
890
|
-
procedure_params = []
|
|
891
|
-
for key, _ in db_params.items():
|
|
892
|
-
if re.match(REGEX__dmbs_object_name, key) is None:
|
|
893
|
-
raise HttpStatusException("Unsafe data federation parameter " + key)
|
|
894
|
-
procedure_params.append(f"{key} => :{key}")
|
|
895
|
-
|
|
896
|
-
submit_data = {
|
|
897
|
-
KEY__application: application,
|
|
898
|
-
KEY_query: f"SELECT * FROM \"{procedure_name}\"({', '.join(procedure_params)});",
|
|
899
|
-
KEY__schema: schema,
|
|
900
|
-
KEY__parameters: db_params
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
print("Preparing federating procedure")
|
|
904
|
-
|
|
905
|
-
submit(self.vault, self.config, self.get_db_crypt_key(),
|
|
906
|
-
self.jaaql_lookup_connection, submit_data, ROLE__jaaql,
|
|
907
|
-
None, self.cached_canned_query_service, as_objects=True, singleton=True)
|
|
908
|
-
|
|
909
|
-
print("Federated user")
|
|
910
|
-
print(submit_data)
|
|
994
|
+
self._run_federation_procedure(database_user_registry, application, schema,
|
|
995
|
+
account_id, provider, tenant, email, sub, id_payload)
|
|
911
996
|
|
|
912
997
|
# Insert into federation.federated_user on the app database
|
|
913
998
|
if database_user_registry is not None:
|
|
@@ -987,7 +1072,7 @@ WHERE
|
|
|
987
1072
|
attributes=get_cookie_attrs(self.vigilant_sessions, False, self.is_container),
|
|
988
1073
|
is_https=self.is_https)
|
|
989
1074
|
|
|
990
|
-
response.set_cookie(COOKIE_LOGIN_MARKER, value="true", is_https=
|
|
1075
|
+
response.set_cookie(COOKIE_LOGIN_MARKER, value="true", is_https=self.is_https, attributes=get_sloppy_cookie_attrs())
|
|
991
1076
|
|
|
992
1077
|
return account, address
|
|
993
1078
|
|
|
@@ -1125,6 +1210,15 @@ WHERE
|
|
|
1125
1210
|
if not auth_endpoint:
|
|
1126
1211
|
raise Exception("Authorization endpoint not found in discovery document")
|
|
1127
1212
|
|
|
1213
|
+
# Rewrite host to the application's own origin so the OIDC login iframe
|
|
1214
|
+
# stays same-origin with the parent page (password managers refuse to
|
|
1215
|
+
# autofill cross-origin iframes). The nginx proxy in the app container
|
|
1216
|
+
# forwards /realms/* and /resources/* back to the identity host.
|
|
1217
|
+
# Only the host changes; the path (/realms/<R>/protocol/openid-connect/auth)
|
|
1218
|
+
# is preserved verbatim so the proxy can route it as-is. PAR, token,
|
|
1219
|
+
# JWKS and discovery endpoints are server-to-server and stay on identity.
|
|
1220
|
+
auth_endpoint = _rewrite_endpoint_to_origin(auth_endpoint, application[KG__application__base_url])
|
|
1221
|
+
|
|
1128
1222
|
parameters = fetch_parameters_for_federation_procedure(self.jaaql_lookup_connection,
|
|
1129
1223
|
database_user_registry[KG__database_user_registry__federation_procedure])
|
|
1130
1224
|
scope_list = [parameter[KG__federation_procedure_parameter__name] for parameter in parameters]
|
|
@@ -1177,7 +1271,11 @@ WHERE
|
|
|
1177
1271
|
par_endpoint = discovery.get("pushed_authorization_request_endpoint")
|
|
1178
1272
|
if not par_endpoint:
|
|
1179
1273
|
raise Exception("Pushed Authorization Request endpoint not found in discovery document.")
|
|
1180
|
-
|
|
1274
|
+
# Force backchannel calls to go DIRECT to Keycloak (skip the app's
|
|
1275
|
+
# same-origin proxy, which would terminate mTLS at the app's nginx
|
|
1276
|
+
# and never forward the FAPI client cert to identity). See
|
|
1277
|
+
# _keycloak_backchannel_origin.
|
|
1278
|
+
par_endpoint = _rewrite_endpoint_to_origin(par_endpoint, _keycloak_backchannel_origin(user_registry[KG__user_registry__discovery_url]))
|
|
1181
1279
|
|
|
1182
1280
|
par_payload = {
|
|
1183
1281
|
"client_id": database_user_registry[KG__database_user_registry__client_id],
|
|
@@ -1274,15 +1372,18 @@ WHERE
|
|
|
1274
1372
|
with open(file_path, 'r') as file:
|
|
1275
1373
|
lines = file.readlines()
|
|
1276
1374
|
|
|
1375
|
+
# Strip existing QUIC/HTTP3 lines so they can be cleanly re-added
|
|
1376
|
+
# in the second pass without preventing the config update.
|
|
1377
|
+
lines = [l for l in lines if "listen 443 quic" not in l
|
|
1378
|
+
and "listen [::]:443 quic" not in l
|
|
1379
|
+
and "http3 on;" not in l.strip()
|
|
1380
|
+
and not (l.strip().startswith("add_header Alt-Svc") and "h3=" in l)]
|
|
1381
|
+
|
|
1277
1382
|
# Initialize variables to track the current state and to store updated lines
|
|
1278
1383
|
in_section = False
|
|
1279
1384
|
updated_lines = []
|
|
1280
1385
|
|
|
1281
|
-
do_write = True
|
|
1282
|
-
|
|
1283
1386
|
for line in lines:
|
|
1284
|
-
if "listen 443 quic" in line:
|
|
1285
|
-
do_write = False
|
|
1286
1387
|
if line.strip().startswith('charset'):
|
|
1287
1388
|
in_section = True
|
|
1288
1389
|
continue # Skip to the next iteration
|
|
@@ -1312,10 +1413,18 @@ WHERE
|
|
|
1312
1413
|
if not in_localhost or not line.strip().startswith("limit_req"): # Ignore limit req for local requests
|
|
1313
1414
|
second_iteration_updated_lines.append(line)
|
|
1314
1415
|
|
|
1416
|
+
# Defensive: if the input had `listen 443 ssl` (Certbot installed it) but
|
|
1417
|
+
# our output dropped it, refuse to write. Better to leave Certbot's working
|
|
1418
|
+
# config in place than overwrite with a broken one.
|
|
1419
|
+
input_had_ssl = any('listen 443 ssl' in l for l in open(file_path).readlines())
|
|
1420
|
+
output_has_ssl = any('listen 443 ssl' in l for l in second_iteration_updated_lines)
|
|
1421
|
+
if input_had_ssl and not output_has_ssl:
|
|
1422
|
+
print('set_web_config aborting: would have lost listen 443 ssl', file=sys.stderr)
|
|
1423
|
+
return
|
|
1424
|
+
|
|
1315
1425
|
# Write the updated content back to the file
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
file.writelines(second_iteration_updated_lines)
|
|
1426
|
+
with open(file_path, 'w') as file:
|
|
1427
|
+
file.writelines(second_iteration_updated_lines)
|
|
1319
1428
|
|
|
1320
1429
|
subprocess.call(['nginx', '-s', 'reload'])
|
|
1321
1430
|
else:
|
|
@@ -1687,6 +1796,14 @@ WHERE
|
|
|
1687
1796
|
submit_data, account_id, None, self.cached_canned_query_service,
|
|
1688
1797
|
as_objects=True, singleton=True)
|
|
1689
1798
|
|
|
1799
|
+
# Inject the JAAQL system email params so templates can deep-link back to
|
|
1800
|
+
# the app via {{JAAQL__APP_URL}} (and {{JAAQL__APP_NAME}} / {{JAAQL__EMAIL_ADDRESS}}).
|
|
1801
|
+
# The replacement set was otherwise the data-view row only, so these tokens
|
|
1802
|
+
# went unresolved on every send path (e.g. cloud-proc alert mail).
|
|
1803
|
+
email_replacement_data[EMAIL_PARAM__app_url] = self.replace_default_app_url(app[KG__application__base_url])
|
|
1804
|
+
email_replacement_data[EMAIL_PARAM__app_name] = app[KG__application__name]
|
|
1805
|
+
email_replacement_data[EMAIL_PARAM__email_address] = username
|
|
1806
|
+
|
|
1690
1807
|
document_templates = fetch_document_templates_for_email_template(self.jaaql_lookup_connection, inputs[KEY__application],
|
|
1691
1808
|
inputs[KEY__template])
|
|
1692
1809
|
attachments = [
|
|
@@ -2074,6 +2191,17 @@ WHERE
|
|
|
2074
2191
|
finally:
|
|
2075
2192
|
app_connection.close()
|
|
2076
2193
|
|
|
2194
|
+
# Link the freshly-created app user (the person row created by the gate procedure
|
|
2195
|
+
# above) to this account now, instead of waiting for a first login that will never
|
|
2196
|
+
# trigger federation (the account already exists by then). Best-effort: do not fail
|
|
2197
|
+
# user creation if the federation procedure errors.
|
|
2198
|
+
try:
|
|
2199
|
+
self._run_federation_procedure(registry, application, "default",
|
|
2200
|
+
new_account_id, provider, tenant,
|
|
2201
|
+
username, user_id, {"email": username})
|
|
2202
|
+
except Exception as e:
|
|
2203
|
+
print(f"Federation procedure (admin create user) skipped: {e}")
|
|
2204
|
+
|
|
2077
2205
|
return {
|
|
2078
2206
|
"temporary_password": temp_pw,
|
|
2079
2207
|
"response": response_obj,
|
|
@@ -2329,7 +2457,7 @@ WHERE
|
|
|
2329
2457
|
application = application__select(self.jaaql_lookup_connection, inputs[KEY__application])
|
|
2330
2458
|
redirect_uri = inputs.get(KEY__redirect_uri, "")
|
|
2331
2459
|
post_logout_url = application[KG__application__base_url] + "/" + redirect_uri
|
|
2332
|
-
response.set_cookie(COOKIE_LOGIN_MARKER, value="", is_https=
|
|
2460
|
+
response.set_cookie(COOKIE_LOGIN_MARKER, value="", is_https=self.is_https, attributes=get_sloppy_cookie_attrs())
|
|
2333
2461
|
response.response_code = HTTPStatus.FOUND
|
|
2334
2462
|
response.raw_headers["Location"] = "/.auth/logout?post_logout_redirect_uri=" + urllib.parse.quote(post_logout_url)
|
|
2335
2463
|
return
|
|
@@ -2393,7 +2521,7 @@ WHERE
|
|
|
2393
2521
|
# Kill your own app session immediately
|
|
2394
2522
|
response.delete_cookie(COOKIE_JAAQL_AUTH, self.is_https)
|
|
2395
2523
|
# Optional: clear the UI helper marker
|
|
2396
|
-
response.set_cookie(COOKIE_LOGIN_MARKER, value="", is_https=
|
|
2524
|
+
response.set_cookie(COOKIE_LOGIN_MARKER, value="", is_https=self.is_https, attributes=get_sloppy_cookie_attrs())
|
|
2397
2525
|
|
|
2398
2526
|
# Build front-channel logout URL (Keycloak requires either id_token_hint OR client_id with post_logout_redirect_uri)
|
|
2399
2527
|
url = urllib.parse.urlsplit(end_session)
|
|
@@ -27,26 +27,36 @@ BEGIN
|
|
|
27
27
|
|
|
28
28
|
-- Federation schema: stores OIDC identity linkage for federated/invited users
|
|
29
29
|
PERFORM dblink_exec('dbname=' || database, 'CREATE SCHEMA IF NOT EXISTS federation;');
|
|
30
|
+
-- The public-schema copies are kept for backwards compatibility with code that
|
|
31
|
+
-- references the domains unqualified. The federation-schema copies are what the
|
|
32
|
+
-- federation tables themselves type against, so they survive a `DROP SCHEMA public
|
|
33
|
+
-- CASCADE` (e.g. from JAAQL's own folder-migration step in migrations_manager_service.py).
|
|
30
34
|
PERFORM dblink_exec('dbname=' || database, '
|
|
31
35
|
DO $d$ BEGIN CREATE DOMAIN postgres_user_id AS character varying(63); EXCEPTION WHEN duplicate_object THEN NULL; END $d$;
|
|
32
36
|
');
|
|
33
37
|
PERFORM dblink_exec('dbname=' || database, '
|
|
34
38
|
DO $d$ BEGIN CREATE DOMAIN oidc_subject_id AS character varying(255); EXCEPTION WHEN duplicate_object THEN NULL; END $d$;
|
|
35
39
|
');
|
|
40
|
+
PERFORM dblink_exec('dbname=' || database, '
|
|
41
|
+
DO $d$ BEGIN CREATE DOMAIN federation.postgres_user_id AS character varying(63); EXCEPTION WHEN duplicate_object THEN NULL; END $d$;
|
|
42
|
+
');
|
|
43
|
+
PERFORM dblink_exec('dbname=' || database, '
|
|
44
|
+
DO $d$ BEGIN CREATE DOMAIN federation.oidc_subject_id AS character varying(255); EXCEPTION WHEN duplicate_object THEN NULL; END $d$;
|
|
45
|
+
');
|
|
36
46
|
PERFORM dblink_exec('dbname=' || database, '
|
|
37
47
|
CREATE TABLE IF NOT EXISTS federation.account (
|
|
38
|
-
account_id postgres_user_id NOT NULL,
|
|
48
|
+
account_id federation.postgres_user_id NOT NULL,
|
|
39
49
|
PRIMARY KEY (account_id)
|
|
40
50
|
);
|
|
41
51
|
');
|
|
42
52
|
PERFORM dblink_exec('dbname=' || database, '
|
|
43
53
|
CREATE TABLE IF NOT EXISTS federation.federated_user (
|
|
44
|
-
account_id postgres_user_id NOT NULL,
|
|
45
|
-
sub oidc_subject_id NOT NULL,
|
|
46
|
-
registered_at timestamptz
|
|
47
|
-
tenant character varying(256)
|
|
48
|
-
provider character varying(256)
|
|
49
|
-
is_active boolean
|
|
54
|
+
account_id federation.postgres_user_id NOT NULL,
|
|
55
|
+
sub federation.oidc_subject_id NOT NULL,
|
|
56
|
+
registered_at timestamptz NOT NULL DEFAULT NOW(),
|
|
57
|
+
tenant character varying(256) NOT NULL,
|
|
58
|
+
provider character varying(256) NOT NULL,
|
|
59
|
+
is_active boolean NOT NULL DEFAULT TRUE,
|
|
50
60
|
encrypted_email character varying(512),
|
|
51
61
|
PRIMARY KEY (tenant, provider, sub),
|
|
52
62
|
FOREIGN KEY (account_id) REFERENCES federation.account (account_id)
|
|
@@ -393,16 +393,64 @@ def _run_folder_migration(folder_path: str, folder_name: str,
|
|
|
393
393
|
_run_psql_file(pre_wipe, "dba", app_database)
|
|
394
394
|
|
|
395
395
|
# Step 3: Wipe public schema.
|
|
396
|
+
#
|
|
397
|
+
# DROP SCHEMA public CASCADE wipes any extension installed in public (unaccent,
|
|
398
|
+
# pg_trgm, pgcrypto, ...) AND any column typed against a public-schema domain.
|
|
399
|
+
# JAAQL's federation tables historically typed account_id/sub against
|
|
400
|
+
# public.postgres_user_id / public.oidc_subject_id, so the cascade silently
|
|
401
|
+
# removed those columns. We snapshot extensions before the drop and restore
|
|
402
|
+
# them afterwards, and migrate the federation column types onto the federation-
|
|
403
|
+
# schema copies of the domains so they survive the wipe in future.
|
|
396
404
|
print(f" Wiping public schema", file=sys.stderr)
|
|
405
|
+
|
|
406
|
+
public_extensions_raw = _run_psql_query(
|
|
407
|
+
"SELECT extname FROM pg_extension "
|
|
408
|
+
"WHERE extnamespace = 'public'::regnamespace AND extname <> 'plpgsql_check' "
|
|
409
|
+
"ORDER BY extname;",
|
|
410
|
+
"postgres", app_database,
|
|
411
|
+
)
|
|
412
|
+
public_extensions = [e.strip() for e in public_extensions_raw.split("\n") if e.strip()]
|
|
413
|
+
print(f" Preserving public-schema extensions: {public_extensions or '(none)'}", file=sys.stderr)
|
|
414
|
+
|
|
415
|
+
# Re-house federation domains and any federation columns that still type against
|
|
416
|
+
# the public-schema versions. Idempotent: healthy / already-migrated systems no-op.
|
|
397
417
|
_run_psql_query(
|
|
418
|
+
"CREATE SCHEMA IF NOT EXISTS federation; "
|
|
419
|
+
"DO $d$ BEGIN CREATE DOMAIN federation.postgres_user_id AS character varying(63); EXCEPTION WHEN duplicate_object THEN NULL; END $d$; "
|
|
420
|
+
"DO $d$ BEGIN CREATE DOMAIN federation.oidc_subject_id AS character varying(255); EXCEPTION WHEN duplicate_object THEN NULL; END $d$; "
|
|
421
|
+
"DO $alter$ BEGIN "
|
|
422
|
+
" IF EXISTS (SELECT 1 FROM information_schema.columns "
|
|
423
|
+
" WHERE table_schema='federation' AND table_name='account' "
|
|
424
|
+
" AND column_name='account_id' AND udt_schema='public' AND udt_name='postgres_user_id') THEN "
|
|
425
|
+
" ALTER TABLE federation.account ALTER COLUMN account_id TYPE federation.postgres_user_id; "
|
|
426
|
+
" END IF; "
|
|
427
|
+
" IF EXISTS (SELECT 1 FROM information_schema.columns "
|
|
428
|
+
" WHERE table_schema='federation' AND table_name='federated_user' "
|
|
429
|
+
" AND column_name='account_id' AND udt_schema='public' AND udt_name='postgres_user_id') THEN "
|
|
430
|
+
" ALTER TABLE federation.federated_user ALTER COLUMN account_id TYPE federation.postgres_user_id; "
|
|
431
|
+
" END IF; "
|
|
432
|
+
" IF EXISTS (SELECT 1 FROM information_schema.columns "
|
|
433
|
+
" WHERE table_schema='federation' AND table_name='federated_user' "
|
|
434
|
+
" AND column_name='sub' AND udt_schema='public' AND udt_name='oidc_subject_id') THEN "
|
|
435
|
+
" ALTER TABLE federation.federated_user ALTER COLUMN sub TYPE federation.oidc_subject_id; "
|
|
436
|
+
" END IF; "
|
|
437
|
+
"END $alter$;",
|
|
438
|
+
"postgres", app_database,
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
drop_recreate = (
|
|
398
442
|
"SET search_path = public, pg_catalog; "
|
|
399
443
|
"DROP EXTENSION IF EXISTS plpgsql_check; "
|
|
400
444
|
"DROP SCHEMA IF EXISTS public CASCADE; "
|
|
401
445
|
"CREATE SCHEMA public; "
|
|
402
446
|
"GRANT USAGE, CREATE ON SCHEMA public TO PUBLIC; "
|
|
403
|
-
"CREATE EXTENSION IF NOT EXISTS plpgsql_check SCHEMA public;"
|
|
404
|
-
"postgres", app_database,
|
|
447
|
+
"CREATE EXTENSION IF NOT EXISTS plpgsql_check SCHEMA public; "
|
|
405
448
|
)
|
|
449
|
+
for ext in public_extensions:
|
|
450
|
+
# extname comes from pg_extension; safe to interpolate as a quoted identifier.
|
|
451
|
+
drop_recreate += f'CREATE EXTENSION IF NOT EXISTS "{ext}" SCHEMA public; '
|
|
452
|
+
|
|
453
|
+
_run_psql_query(drop_recreate, "postgres", app_database)
|
|
406
454
|
|
|
407
455
|
# Step 4: Rebuild schema from data model (migration pt1).
|
|
408
456
|
print(f" Rebuilding schema via migration pt1: {pt1}", file=sys.stderr)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: jaaql-middleware-python
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.2.7
|
|
4
4
|
Summary: The jaaql package, allowing for rapid development and deployment of RESTful HTTP applications
|
|
5
5
|
Home-page: https://github.com/JAAQL/JAAQL-middleware-python
|
|
6
6
|
Author: Software Quality Measurement and Improvement bv
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/config/config-docker.ini
RENAMED
|
File without changes
|
{jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/config/config-test.ini
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/db/db_utils_no_circ.py
RENAMED
|
File without changes
|
{jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/documentation/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/email/email_manager.py
RENAMED
|
File without changes
|
{jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/email/email_manager_service.py
RENAMED
|
File without changes
|
|
File without changes
|
{jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/exceptions/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/generated_constants.py
RENAMED
|
File without changes
|
{jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/interpreter/__init__.py
RENAMED
|
File without changes
|
{jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/interpreter/interpret_jaaql.py
RENAMED
|
File without changes
|
|
File without changes
|
{jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/migrations/__init__.py
RENAMED
|
File without changes
|
{jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/migrations/migrations.py
RENAMED
|
File without changes
|
|
File without changes
|
{jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/mvc/base_controller.py
RENAMED
|
File without changes
|
|
File without changes
|
{jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/mvc/controller_interface.py
RENAMED
|
File without changes
|
{jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/mvc/exception_queries.py
RENAMED
|
File without changes
|
{jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/mvc/generated_queries.py
RENAMED
|
File without changes
|
{jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/mvc/handmade_queries.py
RENAMED
|
File without changes
|
{jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/mvc/model_interface.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/scripts/swagger_template.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/services/shared_var_service.py
RENAMED
|
File without changes
|
|
File without changes
|
{jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/utilities/bootstrap_secrets.py
RENAMED
|
File without changes
|
|
File without changes
|
{jaaql_middleware_python-5.1.0 → jaaql_middleware_python-5.2.7}/jaaql/utilities/crypt_utils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|