Loading capif_backend/backoffice_service/controllers/me_controller.py +8 −10 Changes for capif_backend/backoffice_service/controllers/me_controller.py: 8 added lines, 10 removed lines. Original line number Diff line number Diff line Loading @@ -2,7 +2,8 @@ from flask import Blueprint, jsonify, request from flask_jwt_extended import jwt_required from ..core.operations import Operations from ..utils.utils import get_capif_token, get_capif_urls from ..utils.utils import get_capif_token, get_capif_urls, get_capif_uuid me_routes = Blueprint("me_routes", __name__) operations = Operations() # create operations object -> operations.py Loading @@ -14,6 +15,10 @@ def onboard_invoker(): if not capif_token: return jsonify({"error": "This account has no CAPIF credentials."}), 403 user_uuid = get_capif_uuid() if not user_uuid: return jsonify({"error": "Could not determine the CAPIF user id for this account."}), 500 payload = request.get_json(silent=True) or {} invoker_info = payload.get("apiInvokerInformation") notification_destination = payload.get("notificationDestination") Loading @@ -22,13 +27,6 @@ def onboard_invoker(): ccf_onboarding_url = get_capif_urls()["ccf_onboarding_url"] data, status = operations.onboard_invoker( capif_token, ccf_onboarding_url, invoker_info, notification_destination capif_token, user_uuid, ccf_onboarding_url, invoker_info, notification_destination ) if status != 201: return jsonify({"error": "CCF onboarding failed.", "ccf_status": status, "ccf_response": data}), 502 return jsonify({ "apiInvokerId": data["apiInvokerId"], "apiInvokerCertificate": data["onboardingInformation"]["apiInvokerCertificate"], }), 201 No newline at end of file return jsonify(data), status No newline at end of file capif_backend/backoffice_service/core/operations.py +46 −14 Changes for capif_backend/backoffice_service/core/operations.py: 46 added lines, 14 removed lines. Original line number Diff line number Diff line Loading @@ -11,7 +11,7 @@ from urllib.parse import urlencode # from ..configs.config import Config from ..config import Config from ..utils.certs import generate_keypair_and_csr from ..utils.vault import store_invoker class Operations: Loading Loading @@ -118,7 +118,6 @@ class Operations: except requests.exceptions.RequestException as e: return jsonify({"error": f"Error when retrieving user list: {str(e)}"}), 500 def find_user_by_uuid(self, uuid): try: # Call retrieve_users_pages_order to get all users Loading @@ -142,7 +141,6 @@ class Operations: except Exception as e: return jsonify({"error": f"An error occurred: {str(e)}"}), 500 def get_number_of_users(self): def login2(): try: Loading Loading @@ -258,7 +256,6 @@ class Operations: except requests.exceptions.RequestException as e: return jsonify({"error": f"Error when creating user: {str(e)}"}), 500 def get_invokers(self): url = "https://{}/helper/api/getInvokers".format(os.getenv('CAPIF_HOSTNAME')) Loading @@ -281,7 +278,6 @@ class Operations: except requests.exceptions.RequestException as e: return jsonify({"error": str(e)}), 500 def delete_invokers(self, invoker_ids): try: responses = [] Loading Loading @@ -320,7 +316,6 @@ class Operations: error_message += " | Response content: " + response.text raise Exception("Error deleting invokers: " + error_message) def get_num_invokers(self): url = "https://{}/helper/api/getInvokers".format(os.getenv('CAPIF_HOSTNAME')) Loading @@ -341,7 +336,6 @@ class Operations: # Handle request errors and return the error message return jsonify({"error": str(e)}), 500 def get_invokers_pages_order1(self, page, page_size, sort_order): url = "https://{}/helper/api/getInvokers".format(os.getenv('CAPIF_HOSTNAME')) Loading Loading @@ -439,7 +433,6 @@ class Operations: print(f"Error: {str(e)}") # Log de error return {"error": str(e)} def get_security_context_of_a_invoker(self, invoker_id): # Construir la URL con el invoker_id como query parameter url = "https://{}/helper/api/getSecurity".format(os.getenv('CAPIF_HOSTNAME')) Loading Loading @@ -489,16 +482,18 @@ class Operations: print(f"Error: {str(e)}") # Log de error return {"error": str(e)} def onboard_invoker(self, capif_token, onboarding_url, invoker_info, notification_destination): """Onboard an API invoker in CCF on behalf of the logged-in user.""" csr_request, _private_key = generate_keypair_and_csr() def onboard_invoker(self, capif_token, user_uuid, onboarding_url, invoker_info, notification_destination): """Onboard an API invoker in CCF on behalf of the logged-in user, and store its private key and certificate in Vault""" csr_request, private_key = generate_keypair_and_csr() body = { "apiInvokerInformation": invoker_info, "notificationDestination": notification_destination, "requestTestNotification": False, "onboardingInformation": {"apiInvokerPublicKey": csr_request.decode("utf-8")}, "supportedFeatures": "fffffff", "websockNotifConfig": {"requestWebsocketUri": True, "websocketUri": "websocketUri"}, "supportedFeatures": "fffffff", # TODO: placeholder from capif-client, needs real value "websockNotifConfig": {"requestWebsocketUri": True, "websocketUri": "websocketUri"}, # TODO: placeholder } url = "https://{}/{}".format(os.getenv('CAPIF_HOSTNAME'), onboarding_url) Loading @@ -519,8 +514,45 @@ class Operations: except ValueError: data = {"error": response.text} return data, response.status_code if response.status_code != 201: return {"error": "CCF onboarding failed.", "ccf_status": response.status_code, "ccf_response": data}, 502 invoker_id = data.get("apiInvokerId") certificate = (data.get("onboardingInformation") or {}).get("apiInvokerCertificate") if not invoker_id or not certificate: print(f"ERROR: unexpected CCF onboarding response shape: {list(data.keys())}") if invoker_id: self._rollback_invoker(invoker_id) return {"error": "CCF returned an unexpected onboarding response."}, 502 if not store_invoker(user_uuid, invoker_id, private_key.decode("utf-8"), certificate): if self._rollback_invoker(invoker_id): print(f"WARNING: invoker {invoker_id} rolled back in CCF after Vault write failure") else: print(f"ERROR: invoker {invoker_id} exists in CCF without stored credentials; admin cleanup needed") return {"error": "Onboarding failed: the invoker credentials could not be stored."}, 500 return {"apiInvokerId": invoker_id}, 201 def _rollback_invoker(self, invoker_id): """Delete an invoker in CCF. Returns True on success.""" url = "https://{}/api-invoker-management/v1/onboardedInvokers/{}".format( os.getenv('CAPIF_HOSTNAME'), invoker_id) try: response = requests.delete( url, cert=("newcerts/superadmin.crt", "newcerts/superadmin.key"), verify="newcerts/ca_root.crt", timeout=30, ) except requests.exceptions.RequestException as e: print(f"ERROR: rollback of invoker {invoker_id} failed: {e}") return False if response.status_code != 204: print(f"ERROR: rollback of invoker {invoker_id} failed: {response.status_code} {response.text}") return False return True def get_invokers_of_a_user(self, uuid): # Construir la URL base Loading capif_backend/backoffice_service/utils/vault.py +33 −0 Changes for capif_backend/backoffice_service/utils/vault.py: 33 added lines, 0 removed lines. Original line number Diff line number Diff line import os import requests def vault_addr(): """Base URL of Vault. VAULT_ADDR, otherwise falls back to http://VAULT_HOSTNAME:VAULT_PORT as before.""" Loading @@ -9,3 +10,35 @@ def vault_addr(): def vault_headers(): return {'X-Vault-Token': os.getenv('VAULT_TOKEN')} def vault_verify(): """TLS verification for Vault requests: a CA bundle path from VAULT_CACERT if set, otherwise default verification. Has no effect on http:// addresses.""" return os.getenv('VAULT_CACERT') or True def store_invoker(user_uuid, invoker_id, private_key, certificate): """Store an invoker's private key and certificate in Vault.""" path = f"secret/data/backoffice/users/{user_uuid}/invokers/{invoker_id}" payload = {"data": { "private_key": private_key, "certificate": certificate, "api_invoker_id": invoker_id, }} try: response = requests.post( f"{vault_addr()}/v1/{path}", headers=vault_headers(), json=payload, verify=vault_verify(), timeout=10, ) except requests.exceptions.RequestException as e: print(f"ERROR: Vault write failed for invoker {invoker_id}: {e}") return False if not response.ok: print(f"ERROR: Vault write failed for invoker {invoker_id}: {response.status_code} {response.text}") return False return True No newline at end of file Loading
capif_backend/backoffice_service/controllers/me_controller.py +8 −10 Changes for capif_backend/backoffice_service/controllers/me_controller.py: 8 added lines, 10 removed lines. Original line number Diff line number Diff line Loading @@ -2,7 +2,8 @@ from flask import Blueprint, jsonify, request from flask_jwt_extended import jwt_required from ..core.operations import Operations from ..utils.utils import get_capif_token, get_capif_urls from ..utils.utils import get_capif_token, get_capif_urls, get_capif_uuid me_routes = Blueprint("me_routes", __name__) operations = Operations() # create operations object -> operations.py Loading @@ -14,6 +15,10 @@ def onboard_invoker(): if not capif_token: return jsonify({"error": "This account has no CAPIF credentials."}), 403 user_uuid = get_capif_uuid() if not user_uuid: return jsonify({"error": "Could not determine the CAPIF user id for this account."}), 500 payload = request.get_json(silent=True) or {} invoker_info = payload.get("apiInvokerInformation") notification_destination = payload.get("notificationDestination") Loading @@ -22,13 +27,6 @@ def onboard_invoker(): ccf_onboarding_url = get_capif_urls()["ccf_onboarding_url"] data, status = operations.onboard_invoker( capif_token, ccf_onboarding_url, invoker_info, notification_destination capif_token, user_uuid, ccf_onboarding_url, invoker_info, notification_destination ) if status != 201: return jsonify({"error": "CCF onboarding failed.", "ccf_status": status, "ccf_response": data}), 502 return jsonify({ "apiInvokerId": data["apiInvokerId"], "apiInvokerCertificate": data["onboardingInformation"]["apiInvokerCertificate"], }), 201 No newline at end of file return jsonify(data), status No newline at end of file
capif_backend/backoffice_service/core/operations.py +46 −14 Changes for capif_backend/backoffice_service/core/operations.py: 46 added lines, 14 removed lines. Original line number Diff line number Diff line Loading @@ -11,7 +11,7 @@ from urllib.parse import urlencode # from ..configs.config import Config from ..config import Config from ..utils.certs import generate_keypair_and_csr from ..utils.vault import store_invoker class Operations: Loading Loading @@ -118,7 +118,6 @@ class Operations: except requests.exceptions.RequestException as e: return jsonify({"error": f"Error when retrieving user list: {str(e)}"}), 500 def find_user_by_uuid(self, uuid): try: # Call retrieve_users_pages_order to get all users Loading @@ -142,7 +141,6 @@ class Operations: except Exception as e: return jsonify({"error": f"An error occurred: {str(e)}"}), 500 def get_number_of_users(self): def login2(): try: Loading Loading @@ -258,7 +256,6 @@ class Operations: except requests.exceptions.RequestException as e: return jsonify({"error": f"Error when creating user: {str(e)}"}), 500 def get_invokers(self): url = "https://{}/helper/api/getInvokers".format(os.getenv('CAPIF_HOSTNAME')) Loading @@ -281,7 +278,6 @@ class Operations: except requests.exceptions.RequestException as e: return jsonify({"error": str(e)}), 500 def delete_invokers(self, invoker_ids): try: responses = [] Loading Loading @@ -320,7 +316,6 @@ class Operations: error_message += " | Response content: " + response.text raise Exception("Error deleting invokers: " + error_message) def get_num_invokers(self): url = "https://{}/helper/api/getInvokers".format(os.getenv('CAPIF_HOSTNAME')) Loading @@ -341,7 +336,6 @@ class Operations: # Handle request errors and return the error message return jsonify({"error": str(e)}), 500 def get_invokers_pages_order1(self, page, page_size, sort_order): url = "https://{}/helper/api/getInvokers".format(os.getenv('CAPIF_HOSTNAME')) Loading Loading @@ -439,7 +433,6 @@ class Operations: print(f"Error: {str(e)}") # Log de error return {"error": str(e)} def get_security_context_of_a_invoker(self, invoker_id): # Construir la URL con el invoker_id como query parameter url = "https://{}/helper/api/getSecurity".format(os.getenv('CAPIF_HOSTNAME')) Loading Loading @@ -489,16 +482,18 @@ class Operations: print(f"Error: {str(e)}") # Log de error return {"error": str(e)} def onboard_invoker(self, capif_token, onboarding_url, invoker_info, notification_destination): """Onboard an API invoker in CCF on behalf of the logged-in user.""" csr_request, _private_key = generate_keypair_and_csr() def onboard_invoker(self, capif_token, user_uuid, onboarding_url, invoker_info, notification_destination): """Onboard an API invoker in CCF on behalf of the logged-in user, and store its private key and certificate in Vault""" csr_request, private_key = generate_keypair_and_csr() body = { "apiInvokerInformation": invoker_info, "notificationDestination": notification_destination, "requestTestNotification": False, "onboardingInformation": {"apiInvokerPublicKey": csr_request.decode("utf-8")}, "supportedFeatures": "fffffff", "websockNotifConfig": {"requestWebsocketUri": True, "websocketUri": "websocketUri"}, "supportedFeatures": "fffffff", # TODO: placeholder from capif-client, needs real value "websockNotifConfig": {"requestWebsocketUri": True, "websocketUri": "websocketUri"}, # TODO: placeholder } url = "https://{}/{}".format(os.getenv('CAPIF_HOSTNAME'), onboarding_url) Loading @@ -519,8 +514,45 @@ class Operations: except ValueError: data = {"error": response.text} return data, response.status_code if response.status_code != 201: return {"error": "CCF onboarding failed.", "ccf_status": response.status_code, "ccf_response": data}, 502 invoker_id = data.get("apiInvokerId") certificate = (data.get("onboardingInformation") or {}).get("apiInvokerCertificate") if not invoker_id or not certificate: print(f"ERROR: unexpected CCF onboarding response shape: {list(data.keys())}") if invoker_id: self._rollback_invoker(invoker_id) return {"error": "CCF returned an unexpected onboarding response."}, 502 if not store_invoker(user_uuid, invoker_id, private_key.decode("utf-8"), certificate): if self._rollback_invoker(invoker_id): print(f"WARNING: invoker {invoker_id} rolled back in CCF after Vault write failure") else: print(f"ERROR: invoker {invoker_id} exists in CCF without stored credentials; admin cleanup needed") return {"error": "Onboarding failed: the invoker credentials could not be stored."}, 500 return {"apiInvokerId": invoker_id}, 201 def _rollback_invoker(self, invoker_id): """Delete an invoker in CCF. Returns True on success.""" url = "https://{}/api-invoker-management/v1/onboardedInvokers/{}".format( os.getenv('CAPIF_HOSTNAME'), invoker_id) try: response = requests.delete( url, cert=("newcerts/superadmin.crt", "newcerts/superadmin.key"), verify="newcerts/ca_root.crt", timeout=30, ) except requests.exceptions.RequestException as e: print(f"ERROR: rollback of invoker {invoker_id} failed: {e}") return False if response.status_code != 204: print(f"ERROR: rollback of invoker {invoker_id} failed: {response.status_code} {response.text}") return False return True def get_invokers_of_a_user(self, uuid): # Construir la URL base Loading
capif_backend/backoffice_service/utils/vault.py +33 −0 Changes for capif_backend/backoffice_service/utils/vault.py: 33 added lines, 0 removed lines. Original line number Diff line number Diff line import os import requests def vault_addr(): """Base URL of Vault. VAULT_ADDR, otherwise falls back to http://VAULT_HOSTNAME:VAULT_PORT as before.""" Loading @@ -9,3 +10,35 @@ def vault_addr(): def vault_headers(): return {'X-Vault-Token': os.getenv('VAULT_TOKEN')} def vault_verify(): """TLS verification for Vault requests: a CA bundle path from VAULT_CACERT if set, otherwise default verification. Has no effect on http:// addresses.""" return os.getenv('VAULT_CACERT') or True def store_invoker(user_uuid, invoker_id, private_key, certificate): """Store an invoker's private key and certificate in Vault.""" path = f"secret/data/backoffice/users/{user_uuid}/invokers/{invoker_id}" payload = {"data": { "private_key": private_key, "certificate": certificate, "api_invoker_id": invoker_id, }} try: response = requests.post( f"{vault_addr()}/v1/{path}", headers=vault_headers(), json=payload, verify=vault_verify(), timeout=10, ) except requests.exceptions.RequestException as e: print(f"ERROR: Vault write failed for invoker {invoker_id}: {e}") return False if not response.ok: print(f"ERROR: Vault write failed for invoker {invoker_id}: {response.status_code} {response.text}") return False return True No newline at end of file