From b12884d133ba9348575c63cfcffb8ff26f594ef9 Mon Sep 17 00:00:00 2001 From: Manoj Kumar Date: Tue, 4 Aug 2026 12:03:04 +0530 Subject: [PATCH 1/3] Revert "G2P-5222 Repoint Partner Management to commons-services and align PM-seed auth to the g2p-bridge pmSeedClientId pattern." This reverts commit 7d798a430fb4ae254fdd749ed06b766bff8f2fc7. --- .../src/openg2p_registry_partner_api/config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apis/openg2p-registry-partner-api/src/openg2p_registry_partner_api/config.py b/apis/openg2p-registry-partner-api/src/openg2p_registry_partner_api/config.py index 760eb014..b2eaa3f6 100644 --- a/apis/openg2p-registry-partner-api/src/openg2p_registry_partner_api/config.py +++ b/apis/openg2p-registry-partner-api/src/openg2p_registry_partner_api/config.py @@ -74,10 +74,10 @@ class Settings(ExtSettings): # Algorithms accepted on the partner's DCI envelope JWS. fastapi-common # defaults to "RS256" only; partners commonly use EdDSA/ES256, so widen it. crypto_allowed_algorithms: str = "EdDSA,ES256,RS256" - partner_mgmt_api_url: str = "" # e.g. http://commons-services-pm-partner-api + partner_mgmt_api_url: str = "" # e.g. http://partner-management-partner-api:80 # Consent Manager (PDP) — the /validate endpoint the registry (PEP) calls. - consent_manager_url: str = "" # e.g. http://consent-manager-partner-api + consent_manager_url: str = "" # e.g. http://consent-manager-partner-api:80 consent_manager_timeout: float = 5.0 # Keymanager settings From 5b12f1b45efc998d257f047a4a2684ad43f343f5 Mon Sep 17 00:00:00 2001 From: Manoj Kumar Date: Tue, 4 Aug 2026 12:03:21 +0530 Subject: [PATCH 2/3] Revert "G2P-5222 Consent and partner management related." This reverts commit 873f96aa7c2624e5bcee3ca51df344d659f6cdd2. --- .../openg2p_registry_partner_api/config.py | 4 ++-- .../dci/controller/g2p_dci_controller.py | 11 ++------- .../search/dci/helpers/keymanager_helper.py | 23 +++++++------------ 3 files changed, 12 insertions(+), 26 deletions(-) diff --git a/apis/openg2p-registry-partner-api/src/openg2p_registry_partner_api/config.py b/apis/openg2p-registry-partner-api/src/openg2p_registry_partner_api/config.py index b2eaa3f6..0ec22693 100644 --- a/apis/openg2p-registry-partner-api/src/openg2p_registry_partner_api/config.py +++ b/apis/openg2p-registry-partner-api/src/openg2p_registry_partner_api/config.py @@ -74,10 +74,10 @@ class Settings(ExtSettings): # Algorithms accepted on the partner's DCI envelope JWS. fastapi-common # defaults to "RS256" only; partners commonly use EdDSA/ES256, so widen it. crypto_allowed_algorithms: str = "EdDSA,ES256,RS256" - partner_mgmt_api_url: str = "" # e.g. http://partner-management-partner-api:80 + partner_mgmt_api_url: str = "" # e.g. http://partner-management-partner-api:8000 # Consent Manager (PDP) — the /validate endpoint the registry (PEP) calls. - consent_manager_url: str = "" # e.g. http://consent-manager-partner-api:80 + consent_manager_url: str = "" # e.g. http://consent-manager-partner-api:8000 consent_manager_timeout: float = 5.0 # Keymanager settings diff --git a/apis/openg2p-registry-partner-api/src/openg2p_registry_partner_api/search/dci/controller/g2p_dci_controller.py b/apis/openg2p-registry-partner-api/src/openg2p_registry_partner_api/search/dci/controller/g2p_dci_controller.py index 9421e1c9..3c23e756 100644 --- a/apis/openg2p-registry-partner-api/src/openg2p_registry_partner_api/search/dci/controller/g2p_dci_controller.py +++ b/apis/openg2p-registry-partner-api/src/openg2p_registry_partner_api/search/dci/controller/g2p_dci_controller.py @@ -54,17 +54,9 @@ async def search( header: DciRequestHeader = dci_search_request_env.header message: DciSearchRequest = dci_search_request_env.message - # The raw body is the source of truth for both signature verification - # (exact signed bytes) and consent-object extraction. - raw_body: Dict[str, Any] = await request.json() - # --- 1. Partner signature (transport auth) ---------------------- if _config.signature_validation_enabled: - await self.keymanager_helper.validate_signature( - signature, - (raw_body or {}).get("header") or {}, - (raw_body or {}).get("message") or {}, - ) + await self.keymanager_helper.validate_signature(signature, header, message) else: _logger.warning( "signature_validation_enabled=false — SKIPPING DCI envelope " @@ -76,6 +68,7 @@ async def search( # the response to. None (whole map) => enforcement disabled, no clamp. scopes_by_ref: Optional[Dict[str, Optional[List[str]]]] = None if _config.consent_enforcement_enabled: + raw_body: Dict[str, Any] = await request.json() scopes_by_ref = await self._enforce_consent(raw_body, header, message) else: _logger.warning( diff --git a/apis/openg2p-registry-partner-api/src/openg2p_registry_partner_api/search/dci/helpers/keymanager_helper.py b/apis/openg2p-registry-partner-api/src/openg2p_registry_partner_api/search/dci/helpers/keymanager_helper.py index d2a44b75..61d62af2 100644 --- a/apis/openg2p-registry-partner-api/src/openg2p_registry_partner_api/search/dci/helpers/keymanager_helper.py +++ b/apis/openg2p-registry-partner-api/src/openg2p_registry_partner_api/search/dci/helpers/keymanager_helper.py @@ -5,8 +5,10 @@ from ....config import Settings from ..schemas import ( + DciRequestHeader, DciResponseHeader, DciSearchResponse, + DciSearchRequest, ) _config = Settings.get_config() @@ -60,27 +62,18 @@ async def generate_signature( async def validate_signature( self, signature: str, - raw_header: Dict[str, Any], - raw_message: Dict[str, Any], + header: DciRequestHeader, + message: DciSearchRequest, ) -> bool: - """Verify the DCI envelope signature over the **raw** ``{header, message}`` - exactly as the partner sent it — NOT reparsed pydantic models. - - Verifying over reparsed models would re-serialise through pydantic - (defaults filled, aliases applied) and diverge from the bytes the partner - actually signed. Verifying the raw body is what g2p-bridge's - ``JWTSignatureValidator`` does, and keeps the detached-JWS signing input - exact. - """ signature_payload: Dict[str, Any] = { - "header": raw_header, - "message": raw_message, + "header": header.model_dump(by_alias=True), + "message": message.model_dump(by_alias=True), } signature_valid = await self.crypto_helper.verify_jwt( orig_jwt=signature, payload=signature_payload, - km_app_id=(raw_header or {}).get("receiver_id"), - km_ref_id=partner_reference_id((raw_header or {}).get("sender_id", "")), + km_app_id=header.receiver_id, + km_ref_id=partner_reference_id(header.sender_id), ) if not signature_valid: raise G2PRegistryException( From 7b60b8a6c494e48483edad37975b4aaa1e9b3775 Mon Sep 17 00:00:00 2001 From: Manoj Kumar Date: Tue, 4 Aug 2026 12:08:56 +0530 Subject: [PATCH 3/3] Revert "G2P-5222 Enhancements for consent management, partner management and WJS support." This reverts commit a9ba084b7654de83842e97cbf3c8f4f29ec6edd4. --- .../pyproject.toml | 8 - .../src/openg2p_registry_partner_api/app.py | 4 +- .../openg2p_registry_partner_api/config.py | 34 ---- .../search/__init__.py | 1 - .../search/dci/__init__.py | 2 +- .../dci/controller/g2p_dci_controller.py | 148 ++---------------- .../search/dci/helpers/__init__.py | 3 +- .../search/dci/helpers/consent_helper.py | 98 ------------ .../search/dci/helpers/keymanager_helper.py | 42 +---- .../search/dci/services/g2p_dci_service.py | 49 +----- 10 files changed, 32 insertions(+), 357 deletions(-) delete mode 100644 apis/openg2p-registry-partner-api/src/openg2p_registry_partner_api/search/dci/helpers/consent_helper.py diff --git a/apis/openg2p-registry-partner-api/pyproject.toml b/apis/openg2p-registry-partner-api/pyproject.toml index 8108a9e1..77f7a937 100644 --- a/apis/openg2p-registry-partner-api/pyproject.toml +++ b/apis/openg2p-registry-partner-api/pyproject.toml @@ -14,16 +14,8 @@ classifiers = [ "License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)", "Operating System :: OS Independent", ] -# NOTE: partner signature verification + consent enforcement use -# openg2p-fastapi-common's `build_crypto_helper` (partner-mgmt backend), which -# currently lives on the `develop` branch and is NOT yet in a tagged release. -# The container images install fastapi-common from the `develop` git ref -# (see farmer-registry/docker/partner-api/develop.txt), so this floor pin is -# satisfied there. When fastapi-common cuts a release containing -# `build_crypto_helper`, bump this floor and the tagged requirement files. dependencies = [ "openg2p-fastapi-common >=1.1.4", - "httpx", "jsonpath_ng", "asyncpg", "psycopg2" diff --git a/apis/openg2p-registry-partner-api/src/openg2p_registry_partner_api/app.py b/apis/openg2p-registry-partner-api/src/openg2p_registry_partner_api/app.py index ca825d89..264ae0a3 100644 --- a/apis/openg2p-registry-partner-api/src/openg2p_registry_partner_api/app.py +++ b/apis/openg2p-registry-partner-api/src/openg2p_registry_partner_api/app.py @@ -16,8 +16,7 @@ G2PDciController, G2PDciService, DciRequestResponseHelper, - DciKeymanagerHelper, - DciConsentHelper + DciKeymanagerHelper ) _logger = logging.getLogger(_config.logging_default_logger_name) @@ -34,7 +33,6 @@ def initialize(self, **kwargs): G2PDciController().post_init() G2PDciService() DciKeymanagerHelper() - DciConsentHelper() DciRequestResponseHelper() def migrate_database(self, args): diff --git a/apis/openg2p-registry-partner-api/src/openg2p_registry_partner_api/config.py b/apis/openg2p-registry-partner-api/src/openg2p_registry_partner_api/config.py index 0ec22693..22df7a51 100644 --- a/apis/openg2p-registry-partner-api/src/openg2p_registry_partner_api/config.py +++ b/apis/openg2p-registry-partner-api/src/openg2p_registry_partner_api/config.py @@ -46,40 +46,6 @@ class Settings(ExtSettings): "displacement_status" ] - # ------------------------------------------------------------------ - # Partner signature verification (transport) + Consent enforcement - # ------------------------------------------------------------------ - # Two INDEPENDENT kill-switches. Both default ON (safe PII-egress - # posture); dev/sanity deployments turn them OFF for testing. When a - # switch is OFF the bypass is logged loudly and stamped into the - # response header meta so a bypassed record is never mistaken for a - # legitimately-authorised one. - # - # * signature_validation_enabled -> verify the DCI envelope signature - # (the partner's detached JWS over {header, message}). OFF = accept - # any/unsigned caller. - # * consent_enforcement_enabled -> call the Consent Manager /validate - # for the embedded consent object and clamp returned fields to the - # effective data scopes. OFF = skip CM entirely, return ALL fields. - signature_validation_enabled: bool = True - consent_enforcement_enabled: bool = True - - # Crypto backend selector (openg2p-fastapi-common build_crypto_helper): - # "partner-mgmt" -> verify partner keys fetched from Partner Management - # (GET {partner_mgmt_api_url}/keys/{reference_id}). - # "keymanager" -> legacy Mosip Keymanager service (kept selectable, - # not the default; we are not encrypting yet). - # "local" -> seed keys, for tests. - crypto_backend: str = "partner-mgmt" - # Algorithms accepted on the partner's DCI envelope JWS. fastapi-common - # defaults to "RS256" only; partners commonly use EdDSA/ES256, so widen it. - crypto_allowed_algorithms: str = "EdDSA,ES256,RS256" - partner_mgmt_api_url: str = "" # e.g. http://partner-management-partner-api:8000 - - # Consent Manager (PDP) — the /validate endpoint the registry (PEP) calls. - consent_manager_url: str = "" # e.g. http://consent-manager-partner-api:8000 - consent_manager_timeout: float = 5.0 - # Keymanager settings keymanager_api_base_url: str = "" keymanager_api_timeout: int = 10 diff --git a/apis/openg2p-registry-partner-api/src/openg2p_registry_partner_api/search/__init__.py b/apis/openg2p-registry-partner-api/src/openg2p_registry_partner_api/search/__init__.py index 3d76622e..05f07083 100644 --- a/apis/openg2p-registry-partner-api/src/openg2p_registry_partner_api/search/__init__.py +++ b/apis/openg2p-registry-partner-api/src/openg2p_registry_partner_api/search/__init__.py @@ -1,7 +1,6 @@ from .dci import ( DciRequestResponseHelper, DciKeymanagerHelper, - DciConsentHelper, G2PDciService, G2PDciController ) \ No newline at end of file diff --git a/apis/openg2p-registry-partner-api/src/openg2p_registry_partner_api/search/dci/__init__.py b/apis/openg2p-registry-partner-api/src/openg2p_registry_partner_api/search/dci/__init__.py index c000763a..575b8f1f 100644 --- a/apis/openg2p-registry-partner-api/src/openg2p_registry_partner_api/search/dci/__init__.py +++ b/apis/openg2p-registry-partner-api/src/openg2p_registry_partner_api/search/dci/__init__.py @@ -1,3 +1,3 @@ from .controller import G2PDciController from .services import G2PDciService -from .helpers import DciRequestResponseHelper, DciKeymanagerHelper, DciConsentHelper \ No newline at end of file +from .helpers import DciRequestResponseHelper, DciKeymanagerHelper \ No newline at end of file diff --git a/apis/openg2p-registry-partner-api/src/openg2p_registry_partner_api/search/dci/controller/g2p_dci_controller.py b/apis/openg2p-registry-partner-api/src/openg2p_registry_partner_api/search/dci/controller/g2p_dci_controller.py index 3c23e756..af983f4c 100644 --- a/apis/openg2p-registry-partner-api/src/openg2p_registry_partner_api/search/dci/controller/g2p_dci_controller.py +++ b/apis/openg2p-registry-partner-api/src/openg2p_registry_partner_api/search/dci/controller/g2p_dci_controller.py @@ -1,9 +1,5 @@ import logging -from typing import Any, Dict, List, Optional - -from fastapi import Request from openg2p_fastapi_common.controller import BaseController -from openg2p_registry_core.errors import G2PRegistryException, G2PRegistryErrorCodes from ..schemas import ( DciSearchRequestEnvelope, @@ -15,7 +11,6 @@ from ..helpers import ( DciRequestResponseHelper, DciKeymanagerHelper, - DciConsentHelper, ) from ..services import G2PDciService from ....config import Settings @@ -23,8 +18,6 @@ _config = Settings.get_config() _logger = logging.getLogger(_config.logging_default_logger_name) -_SIGNATURE_DISABLED_MARKER = "signature_validation_disabled" - class G2PDciController(BaseController): def __init__(self, **kwargs): @@ -34,7 +27,6 @@ def __init__(self, **kwargs): self.g2p_dci_service = G2PDciService() self.request_response_helper = DciRequestResponseHelper() self.keymanager_helper = DciKeymanagerHelper() - self.consent_helper = DciConsentHelper() self.router.prefix = "/dci/registry" self.router.add_api_route( @@ -44,137 +36,29 @@ def __init__(self, **kwargs): methods=["POST"], ) - async def search( - self, dci_search_request_env: DciSearchRequestEnvelope, request: Request - ) -> DciSearchResponseEnvelope: + async def search(self, dci_search_request_env: DciSearchRequestEnvelope) -> DciSearchResponseEnvelope: try: _logger.info("DCI search request received") signature: str = dci_search_request_env.signature header: DciRequestHeader = dci_search_request_env.header message: DciSearchRequest = dci_search_request_env.message - - # --- 1. Partner signature (transport auth) ---------------------- - if _config.signature_validation_enabled: - await self.keymanager_helper.validate_signature(signature, header, message) - else: - _logger.warning( - "signature_validation_enabled=false — SKIPPING DCI envelope " - "signature verification (testing bypass; do not use in production)" - ) - - # --- 2. Consent (authorisation) --------------------------------- - # scopes_by_ref maps reference_id -> effective_data_scopes to clamp - # the response to. None (whole map) => enforcement disabled, no clamp. - scopes_by_ref: Optional[Dict[str, Optional[List[str]]]] = None - if _config.consent_enforcement_enabled: - raw_body: Dict[str, Any] = await request.json() - scopes_by_ref = await self._enforce_consent(raw_body, header, message) - else: - _logger.warning( - "consent_enforcement_enabled=false — SKIPPING Consent Manager " - "validation, returning ALL fields (testing bypass; do not use in production)" - ) - - # --- 3. Search (with field clamp when enforcing) ---------------- - dci_search_response_items: list[DciSearchResponseItem] = await self.g2p_dci_service.search( - signature, header, message, consent_scopes_by_ref=scopes_by_ref - ) - - dci_search_response_env: DciSearchResponseEnvelope = ( - self.request_response_helper.construct_dci_search_success_response( - dci_search_response_items, dci_search_request_env - ) - ) - - # --- 4. Stamp enforcement posture + sign ------------------------ - self._stamp_enforcement_meta(dci_search_response_env) - dci_search_response_env.signature = await self._sign_response(dci_search_response_env) - + # Skip keymanager auth for testing + # await self.keymanager_helper.validate_signature(signature, header, message) + + dci_search_response_items: list[DciSearchResponseItem] = await self.g2p_dci_service.search(signature, header, message) + + dci_search_response_env: DciSearchResponseEnvelope = self.request_response_helper.construct_dci_search_success_response(dci_search_response_items, dci_search_request_env) + # Skip keymanager auth for testing + dci_search_response_env.signature = "Signature not implemented" + # dci_search_response_env.signature = await self.keymanager_helper.sign_response(dci_search_response_env.header, dci_search_response_env.message) + return dci_search_response_env except Exception as error_exception: _logger.error(f"Error in search: {str(error_exception)}") - error_response: DciSearchResponseEnvelope = self.request_response_helper.construct_error_response( - error_exception, dci_search_request_env - ) - self._stamp_enforcement_meta(error_response) - error_response.signature = await self._sign_response(error_response) - return error_response - - async def _enforce_consent( - self, raw_body: Dict[str, Any], header: DciRequestHeader, message: DciSearchRequest - ) -> Dict[str, Optional[List[str]]]: - """Validate the embedded consent object for each search item against the - Consent Manager and return {reference_id -> effective_data_scopes}. - - Fail-closed: a missing consent object or any non-permit decision raises, - rejecting the whole request (no partial data leak). - """ - raw_items = ((raw_body or {}).get("message") or {}).get("search_request") or [] - raw_by_ref: Dict[str, Dict[str, Any]] = { - item.get("reference_id"): item for item in raw_items if isinstance(item, dict) - } - - scopes_by_ref: Dict[str, Optional[List[str]]] = {} - for search_request_item in message.search_request: - reference_id = search_request_item.reference_id - raw_item = raw_by_ref.get(reference_id, {}) - consent_jws = self._extract_consent_jws(raw_item) - - decision = await self.consent_helper.validate( - consent_jws=consent_jws, - sender_id=header.sender_id, - ) - if decision.get("decision") != "permit": - raise G2PRegistryException( - code=G2PRegistryErrorCodes.REQUEST_VALIDATION_ERROR.value[1], - message=( - f"Consent denied for reference_id '{reference_id}': " - f"{decision.get('reason_code')} - {decision.get('detail') or ''}" - ).strip(), - ) - scopes_by_ref[reference_id] = decision.get("effective_data_scopes") or [] - - return scopes_by_ref - - @staticmethod - def _extract_consent_jws(raw_item: Dict[str, Any]) -> Optional[str]: - """The partner embeds the CM consent object as a compact JWS at - ``search_criteria.authorize.consent_jws`` (self-contained, verbatim).""" - authorize = ((raw_item or {}).get("search_criteria") or {}).get("authorize") or {} - consent_jws = authorize.get("consent_jws") - return consent_jws if isinstance(consent_jws, str) else None - - @staticmethod - def _stamp_enforcement_meta(response_env: DciSearchResponseEnvelope) -> None: - """Record the enforcement posture in the response header meta so a - bypassed (testing) response is never mistaken for an authorised one.""" - try: - response_env.header.meta = { - **(response_env.header.meta or {}), - "signature_validation": "enabled" - if _config.signature_validation_enabled - else "disabled", - "consent_enforcement": "enabled" - if _config.consent_enforcement_enabled - else "disabled", - } - except Exception: # never let stamping break the response - _logger.exception("Failed to stamp enforcement meta") - - async def _sign_response(self, response_env: DciSearchResponseEnvelope) -> str: - """Sign the response envelope when signature validation is on; otherwise - return a marker so the caller can see signing was intentionally skipped.""" - if not _config.signature_validation_enabled: - return _SIGNATURE_DISABLED_MARKER - try: - return await self.keymanager_helper.generate_signature( - response_env.header, response_env.message - ) - except Exception: - _logger.exception( - "Response signing failed (is the registry signing key configured?); " - "returning unsigned marker" - ) - return _SIGNATURE_DISABLED_MARKER + error_response: DciSearchResponseEnvelope = self.request_response_helper.construct_error_response(error_exception, dci_search_request_env) + # Skip keymanager auth for testing + # error_response.signature = await self.keymanager_helper.sign_response(error_response.header, error_response.message) + + return error_response \ No newline at end of file diff --git a/apis/openg2p-registry-partner-api/src/openg2p_registry_partner_api/search/dci/helpers/__init__.py b/apis/openg2p-registry-partner-api/src/openg2p_registry_partner_api/search/dci/helpers/__init__.py index 4371bea4..5df9d41b 100644 --- a/apis/openg2p-registry-partner-api/src/openg2p_registry_partner_api/search/dci/helpers/__init__.py +++ b/apis/openg2p-registry-partner-api/src/openg2p_registry_partner_api/search/dci/helpers/__init__.py @@ -1,4 +1,3 @@ -from .consent_helper import DciConsentHelper -from .keymanager_helper import DciKeymanagerHelper, partner_reference_id +from .keymanager_helper import DciKeymanagerHelper from .query_helper import DciQueryHelper from .request_response_helper import DciRequestResponseHelper diff --git a/apis/openg2p-registry-partner-api/src/openg2p_registry_partner_api/search/dci/helpers/consent_helper.py b/apis/openg2p-registry-partner-api/src/openg2p_registry_partner_api/search/dci/helpers/consent_helper.py deleted file mode 100644 index ba2ffc22..00000000 --- a/apis/openg2p-registry-partner-api/src/openg2p_registry_partner_api/search/dci/helpers/consent_helper.py +++ /dev/null @@ -1,98 +0,0 @@ -import logging -from typing import Any, Dict, List, Optional - -import httpx -from openg2p_fastapi_common.service import BaseService - -from ....config import Settings - -_config = Settings.get_config() -_logger = logging.getLogger(_config.logging_default_logger_name) - - -class DciConsentHelper(BaseService): - """Client for the Consent Manager (PDP) ``/validate`` endpoint. - - The registry is a Policy Enforcement Point (PEP): it does NOT interpret - consent. It forwards the partner-signed consent object — a compact JWS - embedded by the partner at ``search_criteria.authorize.consent_jws`` — to - the Consent Manager, which verifies the JWS against Partner Management keys, - evaluates the partner's data-share policy, and returns a decision plus the - ``effective_data_scopes`` the registry must clamp the response to. - - Fail-closed: any transport error, non-2xx, or a missing consent JWS yields - a synthetic ``deny`` so a failure never leaks data. - """ - - @property - def _validate_url(self) -> str: - base = (_config.consent_manager_url or "").rstrip("/") - return f"{base}/consent/v1/validate" - - @staticmethod - def _deny(reason: str, detail: str = "") -> Dict[str, Any]: - return { - "decision": "deny", - "reason_code": reason, - "detail": detail, - "effective_data_scopes": [], - } - - async def validate( - self, - consent_jws: Optional[str], - sender_id: str, - requested_scopes: Optional[List[str]] = None, - subject_id: Optional[Dict[str, Any]] = None, - ) -> Dict[str, Any]: - """Call CM ``/validate`` and return its Decision dict (fail-closed). - - ``consent_jws`` is the compact JWS exactly as the partner sent it. It is - self-contained (its signed bytes travel in the payload segment), so the - registry forwards it verbatim — no reshaping needed. - """ - if not consent_jws: - _logger.warning("Consent enforcement: no consent JWS present in request") - return self._deny("missing_consent", "no consent JWS embedded in request") - - if not _config.consent_manager_url: - _logger.error("Consent enforcement enabled but consent_manager_url is not configured") - return self._deny("cm_not_configured", "consent_manager_url is empty") - - request_context: Dict[str, Any] = {} - if requested_scopes: - request_context["requested_scopes"] = requested_scopes - if subject_id: - request_context["subject_id"] = subject_id - - body: Dict[str, Any] = { - "consent_jws": consent_jws, - "partner_id": sender_id, - } - if request_context: - body["request_context"] = request_context - - try: - async with httpx.AsyncClient(timeout=_config.consent_manager_timeout) as client: - response = await client.post(self._validate_url, json=body) - except Exception as exc: # network / timeout — fail closed - _logger.exception("Consent Manager /validate call failed") - return self._deny("cm_unreachable", str(exc)) - - if response.status_code != 200: - _logger.error("Consent Manager /validate returned %s: %s", response.status_code, response.text) - return self._deny("cm_error", f"HTTP {response.status_code}") - - try: - decision: Dict[str, Any] = response.json() - except Exception as exc: - _logger.exception("Consent Manager /validate returned non-JSON") - return self._deny("cm_error", str(exc)) - - _logger.info( - "Consent decision for partner '%s': %s (%s)", - sender_id, - decision.get("decision"), - decision.get("reason_code"), - ) - return decision diff --git a/apis/openg2p-registry-partner-api/src/openg2p_registry_partner_api/search/dci/helpers/keymanager_helper.py b/apis/openg2p-registry-partner-api/src/openg2p_registry_partner_api/search/dci/helpers/keymanager_helper.py index 61d62af2..6fcaa45a 100644 --- a/apis/openg2p-registry-partner-api/src/openg2p_registry_partner_api/search/dci/helpers/keymanager_helper.py +++ b/apis/openg2p-registry-partner-api/src/openg2p_registry_partner_api/search/dci/helpers/keymanager_helper.py @@ -1,9 +1,8 @@ from typing import Dict, Any from openg2p_fastapi_common.service import BaseService from openg2p_registry_core.errors import G2PRegistryException, G2PRegistryErrorCodes -from openg2p_fastapi_common.utils.crypto import build_crypto_helper +from openg2p_fastapi_common.utils.crypto import KeymanagerCryptoHelper -from ....config import Settings from ..schemas import ( DciRequestHeader, DciResponseHeader, @@ -11,34 +10,10 @@ DciSearchRequest, ) -_config = Settings.get_config() - - -def partner_reference_id(sender_id: str) -> str: - """Map a DCI ``header.sender_id`` to the partner reference used to look up - keys — ``PARTNER_`` (upper-cased, ``-`` -> ``_``). - - This is the SAME convention as openg2p-fastapi-partner-auth's - ``JWTValidationHelper.get_partner_id_from_payload`` and g2p-bridge, so a - partner's key is looked up identically across the platform (Partner - Management, Consent Manager, Registry). - """ - return f"PARTNER_{(sender_id or '').replace('-', '_').upper()}" - - class DciKeymanagerHelper(BaseService): - """Signs/verifies the DCI envelope signature. - - Backend-agnostic: the underlying crypto helper is selected by - ``crypto_backend`` config (``partner-mgmt`` by default — partner keys from - Partner Management; ``keymanager`` for the legacy Mosip service; ``local`` - for tests). The DCI wire convention is unchanged: the signature is a - detached JWS over the ``{header, message}`` payload. - """ - def __init__(self, **kwargs): super().__init__(**kwargs) - self.crypto_helper = build_crypto_helper(backend=_config.crypto_backend) + self.keymanager_crypto_helper = KeymanagerCryptoHelper() async def generate_signature( self, @@ -49,7 +24,7 @@ async def generate_signature( "header": header.model_dump(by_alias=True), "message": message.model_dump(by_alias=True), } - signature = await self.crypto_helper.create_jwt_token( + signature = await self.keymanager_crypto_helper.create_jwt_token( payload=signature_payload, include_payload=False, include_certificate=False, @@ -58,7 +33,7 @@ async def generate_signature( km_ref_id=header.receiver_id, ) return signature - + async def validate_signature( self, signature: str, @@ -69,15 +44,14 @@ async def validate_signature( "header": header.model_dump(by_alias=True), "message": message.model_dump(by_alias=True), } - signature_valid = await self.crypto_helper.verify_jwt( + signature_valid = await self.keymanager_crypto_helper.verify_jwt( orig_jwt=signature, payload=signature_payload, - km_app_id=header.receiver_id, - km_ref_id=partner_reference_id(header.sender_id), + km_app_id=header.receiver_id, # TODO: Add app_id + km_ref_id=header.sender_id, ) if not signature_valid: raise G2PRegistryException( code=G2PRegistryErrorCodes.REQUEST_VALIDATION_ERROR.value[1], message=G2PRegistryErrorCodes.REQUEST_VALIDATION_ERROR.value[0], - ) - return True + ) \ No newline at end of file diff --git a/apis/openg2p-registry-partner-api/src/openg2p_registry_partner_api/search/dci/services/g2p_dci_service.py b/apis/openg2p-registry-partner-api/src/openg2p_registry_partner_api/search/dci/services/g2p_dci_service.py index 366f3cba..9508d44f 100644 --- a/apis/openg2p-registry-partner-api/src/openg2p_registry_partner_api/search/dci/services/g2p_dci_service.py +++ b/apis/openg2p-registry-partner-api/src/openg2p_registry_partner_api/search/dci/services/g2p_dci_service.py @@ -36,17 +36,7 @@ def __init__(self, **kwargs): super().__init__(**kwargs) self.register_service = G2PRegisterService.get_component() - async def search( - self, - signature: str, - header: DciRequestHeader, - message: DciSearchRequest, - consent_scopes_by_ref: Optional[Dict[str, Optional[List[str]]]] = None, - ) -> List[DciSearchResponseItem]: - # consent_scopes_by_ref maps reference_id -> effective_data_scopes the - # response must be clamped to (the PEP field-level enforcement). It is - # None when consent enforcement is disabled (return all fields); an - # entry's value being None likewise means "no clamp" for that item. + async def search(self, signature: str, header: DciRequestHeader, message: DciSearchRequest) -> List[DciSearchResponseItem]: dci_search_response_items: List[DciSearchResponseItem] = [] for search_request_item in message.search_request: @@ -77,27 +67,13 @@ async def search( sort_by=sort_by, ) - reg_records = [ - self._render_reg_record_with_template( - datum, template_store_id, bucket=template_bucket - ) - for datum in search_result_data - ] - - # PEP field-level enforcement: clamp each record to the effective - # data scopes the Consent Manager permitted for this reference_id. - if consent_scopes_by_ref is not None: - allowed_scopes = consent_scopes_by_ref.get(search_request_item.reference_id) - if allowed_scopes is not None: - reg_records = [ - self._clamp_record_fields(record, allowed_scopes) - for record in reg_records - ] - dci_deep_search_result_data = DciSearchResultData( reg_type = search_criteria.reg_type, reg_record_type = search_criteria.reg_record_type, - reg_records = reg_records + reg_records = [ + self._render_reg_record_with_template(datum, template_store_id, bucket=template_bucket) + for datum in search_result_data + ] ) pagination = DciSearchResultPagination( @@ -161,21 +137,6 @@ async def _expression_search( return search_results, total_count - @staticmethod - def _clamp_record_fields(record: Dict[str, Any], allowed_scopes: List[str]) -> Dict[str, Any]: - """Return a copy of a rendered registry record keeping only the fields - the Consent Manager permitted (``effective_data_scopes``). - - Scope names are matched against the record's TOP-LEVEL keys (the - template output field names — i.e. the shared scope<->field catalog). - Strict allow-list: any field not in the effective scopes is dropped, so - a narrower policy or consent can only ever remove fields, never add. - """ - if not isinstance(record, dict): - return record - allowed = set(allowed_scopes or []) - return {key: value for key, value in record.items() if key in allowed} - def _get_model_class(self, register_mnemonic: str): module = importlib.import_module("openg2p_registry_extensions.register_domain.models") class_name = f"G2PRegister{register_mnemonic}"