From 471bb00a378b8d8f845e253d7111cdb7b7b45ad0 Mon Sep 17 00:00:00 2001 From: Stavros-Anastasios Charismiadis Date: Thu, 11 Dec 2025 13:37:07 +0200 Subject: [PATCH 01/10] First draft of 2 interconnection APIs --- services/helper/config.yaml | 3 + .../openapi_helper_interconnection.yaml | 215 ++++++++++++++ .../services/interconnection/__init__.py | 0 .../services/interconnection/__main__.py | 19 ++ .../interconnection/controllers/__init__.py | 0 .../controllers/default_controller.py | 52 ++++ .../controllers/security_controller.py | 2 + .../interconnection/core/auth_manager.py | 46 +++ .../core/capifdomaindetails.py | 67 +++++ .../core/ccfinstancedetails.py | 46 +++ .../interconnection/core/consumer_messager.py | 30 ++ .../core/internal_service_ops.py | 25 ++ .../interconnection/core/publisher.py | 12 + .../interconnection/core/redis_event.py | 63 +++++ .../interconnection/core/resources.py | 10 + .../interconnection/core/responses.py | 50 ++++ .../interconnection/core/validate_user.py | 49 ++++ .../services/interconnection/encoder.py | 35 +++ .../interconnection/models/__init__.py | 6 + .../interconnection/models/base_model.py | 68 +++++ .../models/capif_domain_details.py | 63 +++++ .../models/ccf_instance_details.py | 175 ++++++++++++ .../interconnection/models/invalid_param.py | 93 ++++++ .../interconnection/models/problem_details.py | 267 ++++++++++++++++++ .../interconnection/openapi/openapi.yaml | 231 +++++++++++++++ .../services/interconnection/typing_utils.py | 30 ++ .../services/interconnection/util.py | 205 ++++++++++++++ 27 files changed, 1862 insertions(+) create mode 100644 services/helper/helper_service/openapi_helper_interconnection.yaml create mode 100644 services/helper/helper_service/services/interconnection/__init__.py create mode 100644 services/helper/helper_service/services/interconnection/__main__.py create mode 100644 services/helper/helper_service/services/interconnection/controllers/__init__.py create mode 100644 services/helper/helper_service/services/interconnection/controllers/default_controller.py create mode 100644 services/helper/helper_service/services/interconnection/controllers/security_controller.py create mode 100644 services/helper/helper_service/services/interconnection/core/auth_manager.py create mode 100644 services/helper/helper_service/services/interconnection/core/capifdomaindetails.py create mode 100644 services/helper/helper_service/services/interconnection/core/ccfinstancedetails.py create mode 100644 services/helper/helper_service/services/interconnection/core/consumer_messager.py create mode 100644 services/helper/helper_service/services/interconnection/core/internal_service_ops.py create mode 100644 services/helper/helper_service/services/interconnection/core/publisher.py create mode 100644 services/helper/helper_service/services/interconnection/core/redis_event.py create mode 100644 services/helper/helper_service/services/interconnection/core/resources.py create mode 100644 services/helper/helper_service/services/interconnection/core/responses.py create mode 100644 services/helper/helper_service/services/interconnection/core/validate_user.py create mode 100644 services/helper/helper_service/services/interconnection/encoder.py create mode 100644 services/helper/helper_service/services/interconnection/models/__init__.py create mode 100644 services/helper/helper_service/services/interconnection/models/base_model.py create mode 100644 services/helper/helper_service/services/interconnection/models/capif_domain_details.py create mode 100644 services/helper/helper_service/services/interconnection/models/ccf_instance_details.py create mode 100644 services/helper/helper_service/services/interconnection/models/invalid_param.py create mode 100644 services/helper/helper_service/services/interconnection/models/problem_details.py create mode 100644 services/helper/helper_service/services/interconnection/openapi/openapi.yaml create mode 100644 services/helper/helper_service/services/interconnection/typing_utils.py create mode 100644 services/helper/helper_service/services/interconnection/util.py diff --git a/services/helper/config.yaml b/services/helper/config.yaml index 2cf3193d..78d58ae5 100644 --- a/services/helper/config.yaml +++ b/services/helper/config.yaml @@ -44,4 +44,7 @@ package_paths: configuration_api: path: /configuration openapi_file: configuration/openapi/openapi.yaml + interconnection_api: + path: /interconnection + openapi_file: interconnection/openapi/openapi.yaml diff --git a/services/helper/helper_service/openapi_helper_interconnection.yaml b/services/helper/helper_service/openapi_helper_interconnection.yaml new file mode 100644 index 00000000..962e642a --- /dev/null +++ b/services/helper/helper_service/openapi_helper_interconnection.yaml @@ -0,0 +1,215 @@ +openapi: 3.0.3 # The version of the OpenAPI standard +info: + title: CCF Interconnection API + description: APIs for interconnnecting CCFs + version: 1.0.0 +servers: +- url: "{apiRoot}/interconnection" + variables: + apiRoot: + default: http://localhost:8080 + description: Base URL of the Helper service. +# 1. PATHS: Where you define your endpoints +paths: + /interconnect: + post: + summary: Send a new interconnection request + operationId: interconnect_request # <--- Becomes 'def interconnect_request(body):' in Python + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CapifDomainDetails' # Reference to the model below + responses: + "201": + description: Interconnection request succeeded + content: + application/json: + schema: + $ref: '#/components/schemas/CapifDomainDetails' + "400": + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ProblemDetails' + description: Bad request + "401": + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ProblemDetails' + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ProblemDetails' + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ProblemDetails' + description: Not Found + "500": + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ProblemDetails' + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ProblemDetails' + description: Service Unavailable + default: + description: Generic Error + /connect: + post: + summary: Create a new interconnection + operationId: connect_ccfs # <--- Becomes 'def connect_ccfs(body):' in Python + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CcfInstanceDetails' # Reference to the model below + responses: + "201": + description: Interconnection succeeded + content: + application/json: + schema: + $ref: '#/components/schemas/CcfInstanceDetails' + "400": + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ProblemDetails' + description: Bad request + "401": + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ProblemDetails' + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ProblemDetails' + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ProblemDetails' + description: Not Found + "500": + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ProblemDetails' + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ProblemDetails' + description: Service Unavailable + default: + description: Generic Error +# 2. COMPONENTS: Reusable data models (Classes in Python) +components: + schemas: + CapifDomainDetails: + type: object + required: + - dstProvDom + properties: + dstProvDom: + type: string + CcfInstanceDetails: + type: object + required: + - caRoot + - publicKey + - srcProvDom + - ccfId + - dstProvDom + properties: + caRoot: + type: string + publicKey: + type: string + srcProvDom: + type: string + ccfId: + type: string + dstProvDom: + type: string + ProblemDetails: + description: Represents additional information and details on an error response. + properties: + type: + description: string providing an URI formatted according to IETF RFC 3986. + title: type + type: string + title: + description: "A short, human-readable summary of the problem type. It should\ + \ not change from occurrence to occurrence of the problem. \n" + title: title + type: string + status: + description: The HTTP status code for this occurrence of the problem. + title: status + type: integer + detail: + description: A human-readable explanation specific to this occurrence of + the problem. + title: detail + type: string + instance: + description: string providing an URI formatted according to IETF RFC 3986. + title: type + type: string + cause: + description: | + A machine-readable application error cause specific to this occurrence of the problem. This IE should be present and provide application-related error information, if available. + title: cause + type: string + invalidParams: + description: | + Description of invalid parameters, for a request rejected due to invalid parameters. + items: + $ref: '#/components/schemas/InvalidParam' + minItems: 1 + title: invalidParams + type: array + supportedFeatures: + description: | + A string used to indicate the features supported by an API that is used as defined in clause 6.6 in 3GPP TS 29.500. The string shall contain a bitmask indicating supported features in hexadecimal representation Each character in the string shall take a value of "0" to "9", "a" to "f" or "A" to "F" and shall represent the support of 4 features as described in table 5.2.2-3. The most significant character representing the highest-numbered features shall appear first in the string, and the character representing features 1 to 4 shall appear last in the string. The list of features and their numbering (starting with 1) are defined separately for each API. If the string contains a lower number of characters than there are defined features for an API, all features that would be represented by characters that are not present in the string are not supported. + pattern: "^[A-Fa-f0-9]*$" + title: supportedFeatures + type: string + title: ProblemDetails + type: object + InvalidParam: + description: | + Represents the description of invalid parameters, for a request rejected due to invalid parameters. + properties: + param: + description: "Attribute's name encoded as a JSON Pointer, or header's name." + title: param + type: string + reason: + description: "A human-readable reason, e.g. \"must be a positive integer\"\ + ." + title: reason + type: string + required: + - param + title: InvalidParam + type: object \ No newline at end of file diff --git a/services/helper/helper_service/services/interconnection/__init__.py b/services/helper/helper_service/services/interconnection/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/services/helper/helper_service/services/interconnection/__main__.py b/services/helper/helper_service/services/interconnection/__main__.py new file mode 100644 index 00000000..916cc33e --- /dev/null +++ b/services/helper/helper_service/services/interconnection/__main__.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 + +import connexion + +from interconnection import encoder + + +def main(): + app = connexion.App(__name__, specification_dir='./openapi/') + app.app.json_encoder = encoder.JSONEncoder + app.add_api('openapi.yaml', + arguments={'title': 'CCF Interconnection API'}, + pythonic_params=True) + + app.run(port=8080) + + +if __name__ == '__main__': + main() diff --git a/services/helper/helper_service/services/interconnection/controllers/__init__.py b/services/helper/helper_service/services/interconnection/controllers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/services/helper/helper_service/services/interconnection/controllers/default_controller.py b/services/helper/helper_service/services/interconnection/controllers/default_controller.py new file mode 100644 index 00000000..9a2d6d89 --- /dev/null +++ b/services/helper/helper_service/services/interconnection/controllers/default_controller.py @@ -0,0 +1,52 @@ +import connexion +from typing import Dict +from typing import Tuple +from typing import Union + +from interconnection.models.capif_domain_details import CapifDomainDetails # noqa: E501 +from interconnection.models.ccf_instance_details import CcfInstanceDetails # noqa: E501 +from interconnection.models.problem_details import ProblemDetails # noqa: E501 +from interconnection import util +from ..core.ccfinstancedetails import CcfInstanceOperations +from ..core.capifdomaindetails import CapifDomainOperations + +ccf_operations = CcfInstanceOperations() +capif_domain_operations = CapifDomainOperations() + + +def connect_ccfs(body): # noqa: E501 + """Create a new interconnection + + # noqa: E501 + + :param ccf_instance_details: + :type ccf_instance_details: dict | bytes + + :rtype: Union[CcfInstanceDetails, Tuple[CcfInstanceDetails, int], Tuple[CcfInstanceDetails, int, Dict[str, str]] + """ + ccf_instance_details = body + if connexion.request.is_json: + ccf_instance_details = CcfInstanceDetails.from_dict(connexion.request.get_json()) # noqa: E501 + + res = ccf_operations.add_ccfinstance(ccf_instance_details) + + return res + + +def interconnect_request(body): # noqa: E501 + """Send a new interconnection request + + # noqa: E501 + + :param capif_domain_details: + :type capif_domain_details: dict | bytes + + :rtype: Union[CapifDomainDetails, Tuple[CapifDomainDetails, int], Tuple[CapifDomainDetails, int, Dict[str, str]] + """ + capif_domain_details = body + if connexion.request.is_json: + capif_domain_details = CapifDomainDetails.from_dict(connexion.request.get_json()) # noqa: E501 + + res = capif_domain_operations.add_capifdomain(capif_domain_details) + + return res diff --git a/services/helper/helper_service/services/interconnection/controllers/security_controller.py b/services/helper/helper_service/services/interconnection/controllers/security_controller.py new file mode 100644 index 00000000..6d294ffd --- /dev/null +++ b/services/helper/helper_service/services/interconnection/controllers/security_controller.py @@ -0,0 +1,2 @@ +from typing import List + diff --git a/services/helper/helper_service/services/interconnection/core/auth_manager.py b/services/helper/helper_service/services/interconnection/core/auth_manager.py new file mode 100644 index 00000000..d20daffd --- /dev/null +++ b/services/helper/helper_service/services/interconnection/core/auth_manager.py @@ -0,0 +1,46 @@ + +from flask import current_app + +from .resources import Resource + + +class AuthManager(Resource): + + def add_auth_service(self, service_id, apf_id): + cert_col = self.db.get_col_by_name(self.db.certs_col) + + auth_context = cert_col.find_one({"id":apf_id}) + + if "services" in auth_context["resources"]: + if service_id not in auth_context["resources"]["services"]: + auth_context["resources"]["services"].append(service_id) + else: + auth_context["resources"]["services"] = [service_id] + + current_app.logger.info(auth_context) + cert_col.find_one_and_update({"id":apf_id}, {"$set":auth_context}) + + + def remove_auth_service(self, service_id, apf_id): + + cert_col = self.db.get_col_by_name(self.db.certs_col) + + auth_context = cert_col.find_one({"id":apf_id}) + + if "services" in auth_context["resources"]: + if service_id in auth_context["resources"]["services"]: + auth_context["resources"]["services"].remove(service_id) + + cert_col.find_one_and_update({"id":apf_id}, {"$set":auth_context}) + + def remove_auth_all_service(self, apf_id): + + cert_col = self.db.get_col_by_name(self.db.certs_col) + + auth_context = cert_col.find_one({"id":apf_id}) + + if auth_context != None: + if "services" in auth_context["resources"]: + auth_context["resources"]["services"] = [] + + cert_col.find_one_and_update({"id":apf_id}, {"$set":auth_context}) diff --git a/services/helper/helper_service/services/interconnection/core/capifdomaindetails.py b/services/helper/helper_service/services/interconnection/core/capifdomaindetails.py new file mode 100644 index 00000000..5e134457 --- /dev/null +++ b/services/helper/helper_service/services/interconnection/core/capifdomaindetails.py @@ -0,0 +1,67 @@ +import os +import secrets +from datetime import datetime +import requests +import json + +from flask import current_app +# from pymongo import ReturnDocument + +from ..models.ccf_instance_details import CcfInstanceDetails +from ..util import clean_empty, clean_n_camel_case, dict_to_camel_case +# from ..vendor_specific import add_vend_spec_fields +from .auth_manager import AuthManager +from .publisher import Publisher +from .redis_event import RedisEvent +from .resources import Resource +from .responses import ( + bad_request_error, + forbidden_error, + internal_server_error, + make_response, + not_found_error, + unauthorized_error +) + +publisher_ops = Publisher() + + +service_api_not_found_message = "Service API not found" +srcProvDom = "ccfB.fogus.org" + + +class CapifDomainOperations(Resource): + + def __init__(self): + Resource.__init__(self) + self.auth_manager = AuthManager() + + def add_capifdomain(self, capifdomaindetails): + current_app.logger.debug("Interconnection: Add domain") + current_app.logger.debug(capifdomaindetails) + + url = "https://{}/helper/interconnection/connect".format(capifdomaindetails.dst_prov_dom) + + payload = json.dumps({ + "caRoot": "caRoot", + "ccfId": "ccfId", + "dstProvDom": capifdomaindetails.dst_prov_dom, + "publicKey": "publicKey", + "srcProvDom": srcProvDom + }) + headers = { + 'accept': 'application/json', + 'Content-Type': 'application/json' + } + + response = requests.request("POST", url, headers=headers, data=payload, + cert=('certs/superadmin.crt', 'certs/superadmin.key'), + verify='certs/ca_root.crt') + + current_app.logger.debug(response.text) + + capifdomaindetails_dict = capifdomaindetails.to_dict() + current_app.logger.debug(capifdomaindetails_dict) + + res = make_response(object=clean_n_camel_case(capifdomaindetails_dict), status=response.status_code) + return res diff --git a/services/helper/helper_service/services/interconnection/core/ccfinstancedetails.py b/services/helper/helper_service/services/interconnection/core/ccfinstancedetails.py new file mode 100644 index 00000000..a699ae1c --- /dev/null +++ b/services/helper/helper_service/services/interconnection/core/ccfinstancedetails.py @@ -0,0 +1,46 @@ +import os +import secrets +from datetime import datetime + +from flask import current_app +# from pymongo import ReturnDocument + +from ..models.ccf_instance_details import CcfInstanceDetails +from ..util import clean_empty, clean_n_camel_case, dict_to_camel_case +# from ..vendor_specific import add_vend_spec_fields +from .auth_manager import AuthManager +from .publisher import Publisher +from .redis_event import RedisEvent +from .resources import Resource +from .responses import ( + bad_request_error, + forbidden_error, + internal_server_error, + make_response, + not_found_error, + unauthorized_error +) + +publisher_ops = Publisher() + + +service_api_not_found_message = "Service API not found" + + +class CcfInstanceOperations(Resource): + + def __init__(self): + Resource.__init__(self) + self.auth_manager = AuthManager() + + def add_ccfinstance(self, ccfinstancedetails): + current_app.logger.debug("Interconnection: Add instance") + current_app.logger.debug(ccfinstancedetails) + ccfinstancedetails_dict = ccfinstancedetails.to_dict() + ccfinstancedetails_dict['ca_root'] = "caRoot2" + ccfinstancedetails_dict['public_key'] = "publicKey2" + ccfinstancedetails_dict['ccf_id'] = "ccfId2" + ccfinstancedetails_dict['dst_prov_dom'], ccfinstancedetails_dict['src_prov_dom'] = ccfinstancedetails_dict['src_prov_dom'], ccfinstancedetails_dict['dst_prov_dom'] + current_app.logger.debug(ccfinstancedetails_dict) + res = make_response(object=clean_n_camel_case(ccfinstancedetails_dict), status=201) + return res \ No newline at end of file diff --git a/services/helper/helper_service/services/interconnection/core/consumer_messager.py b/services/helper/helper_service/services/interconnection/core/consumer_messager.py new file mode 100644 index 00000000..3814c71b --- /dev/null +++ b/services/helper/helper_service/services/interconnection/core/consumer_messager.py @@ -0,0 +1,30 @@ +# subscriber.py +import json + +import redis +from flask import current_app + +from .internal_service_ops import InternalServiceOps + + +class Subscriber(): + + def __init__(self): + self.r = redis.Redis(host='redis', port=6379, db=0) + self.security_ops = InternalServiceOps() + self.p = self.r.pubsub() + self.p.subscribe("internal-messages") + + def listen(self): + current_app.logger.info("Listening publish messages") + for raw_message in self.p.listen(): + if raw_message["type"] == "message" and raw_message["channel"].decode('utf-8') == "internal-messages": + current_app.logger.info("New internal event received") + internal_redis_event = json.loads( + raw_message["data"].decode('utf-8')) + if internal_redis_event.get('event') == "PROVIDER-REMOVED": + apf_ids = internal_redis_event.get( + 'information', {"apf_ids": []}).get('apf_ids') + if len(apf_ids) > 0: + self.security_ops.delete_intern_service(apf_ids) + diff --git a/services/helper/helper_service/services/interconnection/core/internal_service_ops.py b/services/helper/helper_service/services/interconnection/core/internal_service_ops.py new file mode 100644 index 00000000..fa7bc67d --- /dev/null +++ b/services/helper/helper_service/services/interconnection/core/internal_service_ops.py @@ -0,0 +1,25 @@ + +from flask import current_app + +from .auth_manager import AuthManager +from .resources import Resource + + +class InternalServiceOps(Resource): + + def __init__(self): + Resource.__init__(self) + self.auth_manager = AuthManager() + + def delete_intern_service(self, apf_ids): + + current_app.logger.info("Provider removed, removing services published by APF") + mycol = self.db.get_col_by_name(self.db.service_api_descriptions) + for apf_id in apf_ids: + my_query = {'apf_id': apf_id} + mycol.delete_many(my_query) + + #We dont need remove all auth events, because when provider is removed, remove auth entry + #self.auth_manager.remove_auth_all_service(apf_id) + + current_app.logger.info("Removed service") diff --git a/services/helper/helper_service/services/interconnection/core/publisher.py b/services/helper/helper_service/services/interconnection/core/publisher.py new file mode 100644 index 00000000..db3049f8 --- /dev/null +++ b/services/helper/helper_service/services/interconnection/core/publisher.py @@ -0,0 +1,12 @@ +# import redis + + +class Publisher(): + + def __init__(self): + # self. r = redis.Redis(host='redis', port=6379, db=0) + self. r = None + + def publish_message(self, channel, message): + # self.r.publish(channel, message) + self.r = None diff --git a/services/helper/helper_service/services/interconnection/core/redis_event.py b/services/helper/helper_service/services/interconnection/core/redis_event.py new file mode 100644 index 00000000..3037ae76 --- /dev/null +++ b/services/helper/helper_service/services/interconnection/core/redis_event.py @@ -0,0 +1,63 @@ +import json + +from ..encoder import JSONEncoder +from .publisher import Publisher + +publisher_ops = Publisher() + + +class RedisEvent(): + def __init__(self, + event, + service_api_descriptions=None, + api_ids=None, + api_invoker_ids=None, + acc_ctrl_pol_list=None, + invocation_logs=None, + api_topo_hide=None) -> None: + self.EVENTS_ENUM = [ + 'SERVICE_API_AVAILABLE', + 'SERVICE_API_UNAVAILABLE', + 'SERVICE_API_UPDATE', + 'API_INVOKER_ONBOARDED', + 'API_INVOKER_OFFBOARDED', + 'SERVICE_API_INVOCATION_SUCCESS', + 'SERVICE_API_INVOCATION_FAILURE', + 'ACCESS_CONTROL_POLICY_UPDATE', + 'ACCESS_CONTROL_POLICY_UNAVAILABLE', + 'API_INVOKER_AUTHORIZATION_REVOKED', + 'API_INVOKER_UPDATED', + 'API_TOPOLOGY_HIDING_CREATED', + 'API_TOPOLOGY_HIDING_REVOKED'] + if event not in self.EVENTS_ENUM: + raise Exception( + "Event (" + event + ") is not on event enum (" + ','.join(self.EVENTS_ENUM) + ")") + self.redis_event = { + "event": event + } + # Add event filter keys to an auxiliary object + event_detail = { + "serviceAPIDescriptions": service_api_descriptions, + "apiIds": api_ids, + "apiInvokerIds": api_invoker_ids, + "accCtrlPolList": acc_ctrl_pol_list, + "invocationLogs": invocation_logs, + "apiTopoHide": api_topo_hide + } + + # Filter keys with not None values + filtered_event_detail = {k: v for k, + v in event_detail.items() if v is not None} + + # If there are valid values then add to redis event. + if filtered_event_detail: + self.redis_event["event_detail"] = filtered_event_detail + + def to_string(self): + return json.dumps(self.redis_event, cls=JSONEncoder) + + def send_event(self): + publisher_ops.publish_message("events", self.to_string()) + + def __call__(self): + return self.redis_event diff --git a/services/helper/helper_service/services/interconnection/core/resources.py b/services/helper/helper_service/services/interconnection/core/resources.py new file mode 100644 index 00000000..a204dc67 --- /dev/null +++ b/services/helper/helper_service/services/interconnection/core/resources.py @@ -0,0 +1,10 @@ +from abc import ABC + +# from db.db import MongoDatabse + + +class Resource(ABC): + + def __init__(self): + # self.db = MongoDatabse() + self.db = None diff --git a/services/helper/helper_service/services/interconnection/core/responses.py b/services/helper/helper_service/services/interconnection/core/responses.py new file mode 100644 index 00000000..1f0302f6 --- /dev/null +++ b/services/helper/helper_service/services/interconnection/core/responses.py @@ -0,0 +1,50 @@ +import json + +from flask import Response + +from ..encoder import CustomJSONEncoder +from ..models.problem_details import ProblemDetails +from ..util import serialize_clean_camel_case + +mimetype = "application/json" + + +def make_response(object, status): + res = Response(json.dumps(object, cls=CustomJSONEncoder), status=status, mimetype=mimetype) + + return res + + +def internal_server_error(detail, cause): + prob = ProblemDetails(title="Internal Server Error", status=500, detail=detail, cause=cause) + prob = serialize_clean_camel_case(prob) + + return Response(json.dumps(prob, cls=CustomJSONEncoder), status=500, mimetype=mimetype) + + +def forbidden_error(detail, cause): + prob = ProblemDetails(title="Forbidden", status=403, detail=detail, cause=cause) + prob = serialize_clean_camel_case(prob) + + return Response(json.dumps(prob, cls=CustomJSONEncoder), status=403, mimetype=mimetype) + + +def bad_request_error(detail, cause, invalid_params): + prob = ProblemDetails(title="Bad Request", status=400, detail=detail, cause=cause, invalid_params=invalid_params) + prob = serialize_clean_camel_case(prob) + + return Response(json.dumps(prob, cls=CustomJSONEncoder), status=400, mimetype=cause) + + +def not_found_error(detail, cause): + prob = ProblemDetails(title="Not Found", status=404, detail=detail, cause=cause) + prob = serialize_clean_camel_case(prob) + + return Response(json.dumps(prob, cls=CustomJSONEncoder), status=404, mimetype=mimetype) + + +def unauthorized_error(detail, cause): + prob = ProblemDetails(title="Unauthorized", status=401, detail=detail, cause=cause) + prob = serialize_clean_camel_case(prob) + + return Response(json.dumps(prob, cls=CustomJSONEncoder), status=401, mimetype=mimetype) \ No newline at end of file diff --git a/services/helper/helper_service/services/interconnection/core/validate_user.py b/services/helper/helper_service/services/interconnection/core/validate_user.py new file mode 100644 index 00000000..1782fe04 --- /dev/null +++ b/services/helper/helper_service/services/interconnection/core/validate_user.py @@ -0,0 +1,49 @@ +import json + +from flask import Response, current_app + +from ..encoder import CustomJSONEncoder +from ..models.problem_details import ProblemDetails +from ..util import serialize_clean_camel_case +from .resources import Resource +from .responses import internal_server_error + + +class ControlAccess(Resource): + + def validate_user_cert(self, apf_id, cert_signature, service_id=None): + + cert_col = self.db.get_col_by_name(self.db.certs_col) + + try: + my_query = {'id': apf_id} + cert_entry = cert_col.find_one(my_query) + + if cert_entry is not None: + is_user_owner = True + if cert_entry["cert_signature"] != cert_signature: + is_user_owner = False + elif service_id: + if "services" not in cert_entry["resources"]: + is_user_owner = False + elif cert_entry.get("resources") and cert_entry["resources"].get("services"): + if service_id not in cert_entry["resources"].get("services"): + is_user_owner = False + if is_user_owner == False: + current_app.logger.info("STEP3") + prob = ProblemDetails( + title="Unauthorized", + detail="User not authorized", + cause="You are not the owner of this resource") + current_app.logger.info("STEP4") + prob = serialize_clean_camel_case(prob) + current_app.logger.info("STEP5") + return Response( + json.dumps(prob, cls=CustomJSONEncoder), + status=401, + mimetype="application/json") + + except Exception as e: + exception = "An exception occurred in validate apf" + current_app.logger.error(exception + "::" + str(e)) + return internal_server_error(detail=exception, cause=str(e)) diff --git a/services/helper/helper_service/services/interconnection/encoder.py b/services/helper/helper_service/services/interconnection/encoder.py new file mode 100644 index 00000000..bb634278 --- /dev/null +++ b/services/helper/helper_service/services/interconnection/encoder.py @@ -0,0 +1,35 @@ +from connexion.apps.flask_app import FlaskJSONEncoder + +from interconnection.models.base_model import Model + + +class JSONEncoder(FlaskJSONEncoder): + include_nulls = False + + def default(self, o): + if isinstance(o, Model): + dikt = {} + for attr in o.openapi_types: + value = getattr(o, attr) + if value is None and not self.include_nulls: + continue + attr = o.attribute_map[attr] + dikt[attr] = value + return dikt + return FlaskJSONEncoder.default(self, o) + + +class CustomJSONEncoder(JSONEncoder): + include_nulls = False + + def default(self, o): + if isinstance(o, Model): + dikt = {} + for attr in o.openapi_types: + value = getattr(o, attr) + if value is None and not self.include_nulls: + continue + attr = o.attribute_map[attr] + dikt[attr] = value + return dikt + return JSONEncoder.default(self, o) \ No newline at end of file diff --git a/services/helper/helper_service/services/interconnection/models/__init__.py b/services/helper/helper_service/services/interconnection/models/__init__.py new file mode 100644 index 00000000..2f654f24 --- /dev/null +++ b/services/helper/helper_service/services/interconnection/models/__init__.py @@ -0,0 +1,6 @@ +# flake8: noqa +# import models into model package +from interconnection.models.capif_domain_details import CapifDomainDetails +from interconnection.models.ccf_instance_details import CcfInstanceDetails +from interconnection.models.invalid_param import InvalidParam +from interconnection.models.problem_details import ProblemDetails diff --git a/services/helper/helper_service/services/interconnection/models/base_model.py b/services/helper/helper_service/services/interconnection/models/base_model.py new file mode 100644 index 00000000..b354bac2 --- /dev/null +++ b/services/helper/helper_service/services/interconnection/models/base_model.py @@ -0,0 +1,68 @@ +import pprint + +import typing + +from interconnection import util + +T = typing.TypeVar('T') + + +class Model: + # openapiTypes: The key is attribute name and the + # value is attribute type. + openapi_types: typing.Dict[str, type] = {} + + # attributeMap: The key is attribute name and the + # value is json key in definition. + attribute_map: typing.Dict[str, str] = {} + + @classmethod + def from_dict(cls: typing.Type[T], dikt) -> T: + """Returns the dict as a model""" + return util.deserialize_model(dikt, cls) + + def to_dict(self): + """Returns the model properties as a dict + + :rtype: dict + """ + result = {} + + for attr in self.openapi_types: + value = getattr(self, attr) + if isinstance(value, list): + result[attr] = list(map( + lambda x: x.to_dict() if hasattr(x, "to_dict") else x, + value + )) + elif hasattr(value, "to_dict"): + result[attr] = value.to_dict() + elif isinstance(value, dict): + result[attr] = dict(map( + lambda item: (item[0], item[1].to_dict()) + if hasattr(item[1], "to_dict") else item, + value.items() + )) + else: + result[attr] = value + + return result + + def to_str(self): + """Returns the string representation of the model + + :rtype: str + """ + return pprint.pformat(self.to_dict()) + + def __repr__(self): + """For `print` and `pprint`""" + return self.to_str() + + def __eq__(self, other): + """Returns true if both objects are equal""" + return self.__dict__ == other.__dict__ + + def __ne__(self, other): + """Returns true if both objects are not equal""" + return not self == other diff --git a/services/helper/helper_service/services/interconnection/models/capif_domain_details.py b/services/helper/helper_service/services/interconnection/models/capif_domain_details.py new file mode 100644 index 00000000..2a2319cd --- /dev/null +++ b/services/helper/helper_service/services/interconnection/models/capif_domain_details.py @@ -0,0 +1,63 @@ +from datetime import date, datetime # noqa: F401 + +from typing import List, Dict # noqa: F401 + +from interconnection.models.base_model import Model +from interconnection import util + + +class CapifDomainDetails(Model): + """NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + + Do not edit the class manually. + """ + + def __init__(self, dst_prov_dom=None): # noqa: E501 + """CapifDomainDetails - a model defined in OpenAPI + + :param dst_prov_dom: The dst_prov_dom of this CapifDomainDetails. # noqa: E501 + :type dst_prov_dom: str + """ + self.openapi_types = { + 'dst_prov_dom': str + } + + self.attribute_map = { + 'dst_prov_dom': 'dstProvDom' + } + + self._dst_prov_dom = dst_prov_dom + + @classmethod + def from_dict(cls, dikt) -> 'CapifDomainDetails': + """Returns the dict as a model + + :param dikt: A dict. + :type: dict + :return: The CapifDomainDetails of this CapifDomainDetails. # noqa: E501 + :rtype: CapifDomainDetails + """ + return util.deserialize_model(dikt, cls) + + @property + def dst_prov_dom(self) -> str: + """Gets the dst_prov_dom of this CapifDomainDetails. + + + :return: The dst_prov_dom of this CapifDomainDetails. + :rtype: str + """ + return self._dst_prov_dom + + @dst_prov_dom.setter + def dst_prov_dom(self, dst_prov_dom: str): + """Sets the dst_prov_dom of this CapifDomainDetails. + + + :param dst_prov_dom: The dst_prov_dom of this CapifDomainDetails. + :type dst_prov_dom: str + """ + if dst_prov_dom is None: + raise ValueError("Invalid value for `dst_prov_dom`, must not be `None`") # noqa: E501 + + self._dst_prov_dom = dst_prov_dom diff --git a/services/helper/helper_service/services/interconnection/models/ccf_instance_details.py b/services/helper/helper_service/services/interconnection/models/ccf_instance_details.py new file mode 100644 index 00000000..0ce33019 --- /dev/null +++ b/services/helper/helper_service/services/interconnection/models/ccf_instance_details.py @@ -0,0 +1,175 @@ +from datetime import date, datetime # noqa: F401 + +from typing import List, Dict # noqa: F401 + +from interconnection.models.base_model import Model +from interconnection import util + + +class CcfInstanceDetails(Model): + """NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + + Do not edit the class manually. + """ + + def __init__(self, ca_root=None, public_key=None, src_prov_dom=None, ccf_id=None, dst_prov_dom=None): # noqa: E501 + """CcfInstanceDetails - a model defined in OpenAPI + + :param ca_root: The ca_root of this CcfInstanceDetails. # noqa: E501 + :type ca_root: str + :param public_key: The public_key of this CcfInstanceDetails. # noqa: E501 + :type public_key: str + :param src_prov_dom: The src_prov_dom of this CcfInstanceDetails. # noqa: E501 + :type src_prov_dom: str + :param ccf_id: The ccf_id of this CcfInstanceDetails. # noqa: E501 + :type ccf_id: str + :param dst_prov_dom: The dst_prov_dom of this CcfInstanceDetails. # noqa: E501 + :type dst_prov_dom: str + """ + self.openapi_types = { + 'ca_root': str, + 'public_key': str, + 'src_prov_dom': str, + 'ccf_id': str, + 'dst_prov_dom': str + } + + self.attribute_map = { + 'ca_root': 'caRoot', + 'public_key': 'publicKey', + 'src_prov_dom': 'srcProvDom', + 'ccf_id': 'ccfId', + 'dst_prov_dom': 'dstProvDom' + } + + self._ca_root = ca_root + self._public_key = public_key + self._src_prov_dom = src_prov_dom + self._ccf_id = ccf_id + self._dst_prov_dom = dst_prov_dom + + @classmethod + def from_dict(cls, dikt) -> 'CcfInstanceDetails': + """Returns the dict as a model + + :param dikt: A dict. + :type: dict + :return: The CcfInstanceDetails of this CcfInstanceDetails. # noqa: E501 + :rtype: CcfInstanceDetails + """ + return util.deserialize_model(dikt, cls) + + @property + def ca_root(self) -> str: + """Gets the ca_root of this CcfInstanceDetails. + + + :return: The ca_root of this CcfInstanceDetails. + :rtype: str + """ + return self._ca_root + + @ca_root.setter + def ca_root(self, ca_root: str): + """Sets the ca_root of this CcfInstanceDetails. + + + :param ca_root: The ca_root of this CcfInstanceDetails. + :type ca_root: str + """ + if ca_root is None: + raise ValueError("Invalid value for `ca_root`, must not be `None`") # noqa: E501 + + self._ca_root = ca_root + + @property + def public_key(self) -> str: + """Gets the public_key of this CcfInstanceDetails. + + + :return: The public_key of this CcfInstanceDetails. + :rtype: str + """ + return self._public_key + + @public_key.setter + def public_key(self, public_key: str): + """Sets the public_key of this CcfInstanceDetails. + + + :param public_key: The public_key of this CcfInstanceDetails. + :type public_key: str + """ + if public_key is None: + raise ValueError("Invalid value for `public_key`, must not be `None`") # noqa: E501 + + self._public_key = public_key + + @property + def src_prov_dom(self) -> str: + """Gets the src_prov_dom of this CcfInstanceDetails. + + + :return: The src_prov_dom of this CcfInstanceDetails. + :rtype: str + """ + return self._src_prov_dom + + @src_prov_dom.setter + def src_prov_dom(self, src_prov_dom: str): + """Sets the src_prov_dom of this CcfInstanceDetails. + + + :param src_prov_dom: The src_prov_dom of this CcfInstanceDetails. + :type src_prov_dom: str + """ + if src_prov_dom is None: + raise ValueError("Invalid value for `src_prov_dom`, must not be `None`") # noqa: E501 + + self._src_prov_dom = src_prov_dom + + @property + def ccf_id(self) -> str: + """Gets the ccf_id of this CcfInstanceDetails. + + + :return: The ccf_id of this CcfInstanceDetails. + :rtype: str + """ + return self._ccf_id + + @ccf_id.setter + def ccf_id(self, ccf_id: str): + """Sets the ccf_id of this CcfInstanceDetails. + + + :param ccf_id: The ccf_id of this CcfInstanceDetails. + :type ccf_id: str + """ + if ccf_id is None: + raise ValueError("Invalid value for `ccf_id`, must not be `None`") # noqa: E501 + + self._ccf_id = ccf_id + + @property + def dst_prov_dom(self) -> str: + """Gets the dst_prov_dom of this CcfInstanceDetails. + + + :return: The dst_prov_dom of this CcfInstanceDetails. + :rtype: str + """ + return self._dst_prov_dom + + @dst_prov_dom.setter + def dst_prov_dom(self, dst_prov_dom: str): + """Sets the dst_prov_dom of this CcfInstanceDetails. + + + :param dst_prov_dom: The dst_prov_dom of this CcfInstanceDetails. + :type dst_prov_dom: str + """ + if dst_prov_dom is None: + raise ValueError("Invalid value for `dst_prov_dom`, must not be `None`") # noqa: E501 + + self._dst_prov_dom = dst_prov_dom diff --git a/services/helper/helper_service/services/interconnection/models/invalid_param.py b/services/helper/helper_service/services/interconnection/models/invalid_param.py new file mode 100644 index 00000000..3776af09 --- /dev/null +++ b/services/helper/helper_service/services/interconnection/models/invalid_param.py @@ -0,0 +1,93 @@ +from datetime import date, datetime # noqa: F401 + +from typing import List, Dict # noqa: F401 + +from interconnection.models.base_model import Model +from interconnection import util + + +class InvalidParam(Model): + """NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + + Do not edit the class manually. + """ + + def __init__(self, param=None, reason=None): # noqa: E501 + """InvalidParam - a model defined in OpenAPI + + :param param: The param of this InvalidParam. # noqa: E501 + :type param: str + :param reason: The reason of this InvalidParam. # noqa: E501 + :type reason: str + """ + self.openapi_types = { + 'param': str, + 'reason': str + } + + self.attribute_map = { + 'param': 'param', + 'reason': 'reason' + } + + self._param = param + self._reason = reason + + @classmethod + def from_dict(cls, dikt) -> 'InvalidParam': + """Returns the dict as a model + + :param dikt: A dict. + :type: dict + :return: The InvalidParam of this InvalidParam. # noqa: E501 + :rtype: InvalidParam + """ + return util.deserialize_model(dikt, cls) + + @property + def param(self) -> str: + """Gets the param of this InvalidParam. + + Attribute's name encoded as a JSON Pointer, or header's name. # noqa: E501 + + :return: The param of this InvalidParam. + :rtype: str + """ + return self._param + + @param.setter + def param(self, param: str): + """Sets the param of this InvalidParam. + + Attribute's name encoded as a JSON Pointer, or header's name. # noqa: E501 + + :param param: The param of this InvalidParam. + :type param: str + """ + if param is None: + raise ValueError("Invalid value for `param`, must not be `None`") # noqa: E501 + + self._param = param + + @property + def reason(self) -> str: + """Gets the reason of this InvalidParam. + + A human-readable reason, e.g. \"must be a positive integer\". # noqa: E501 + + :return: The reason of this InvalidParam. + :rtype: str + """ + return self._reason + + @reason.setter + def reason(self, reason: str): + """Sets the reason of this InvalidParam. + + A human-readable reason, e.g. \"must be a positive integer\". # noqa: E501 + + :param reason: The reason of this InvalidParam. + :type reason: str + """ + + self._reason = reason diff --git a/services/helper/helper_service/services/interconnection/models/problem_details.py b/services/helper/helper_service/services/interconnection/models/problem_details.py new file mode 100644 index 00000000..c19f2022 --- /dev/null +++ b/services/helper/helper_service/services/interconnection/models/problem_details.py @@ -0,0 +1,267 @@ +from datetime import date, datetime # noqa: F401 + +from typing import List, Dict # noqa: F401 + +from interconnection.models.base_model import Model +from interconnection.models.invalid_param import InvalidParam +import re +from interconnection import util + +from interconnection.models.invalid_param import InvalidParam # noqa: E501 +import re # noqa: E501 + +class ProblemDetails(Model): + """NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + + Do not edit the class manually. + """ + + def __init__(self, type=None, title=None, status=None, detail=None, instance=None, cause=None, invalid_params=None, supported_features=None): # noqa: E501 + """ProblemDetails - a model defined in OpenAPI + + :param type: The type of this ProblemDetails. # noqa: E501 + :type type: str + :param title: The title of this ProblemDetails. # noqa: E501 + :type title: str + :param status: The status of this ProblemDetails. # noqa: E501 + :type status: int + :param detail: The detail of this ProblemDetails. # noqa: E501 + :type detail: str + :param instance: The instance of this ProblemDetails. # noqa: E501 + :type instance: str + :param cause: The cause of this ProblemDetails. # noqa: E501 + :type cause: str + :param invalid_params: The invalid_params of this ProblemDetails. # noqa: E501 + :type invalid_params: List[InvalidParam] + :param supported_features: The supported_features of this ProblemDetails. # noqa: E501 + :type supported_features: str + """ + self.openapi_types = { + 'type': str, + 'title': str, + 'status': int, + 'detail': str, + 'instance': str, + 'cause': str, + 'invalid_params': List[InvalidParam], + 'supported_features': str + } + + self.attribute_map = { + 'type': 'type', + 'title': 'title', + 'status': 'status', + 'detail': 'detail', + 'instance': 'instance', + 'cause': 'cause', + 'invalid_params': 'invalidParams', + 'supported_features': 'supportedFeatures' + } + + self._type = type + self._title = title + self._status = status + self._detail = detail + self._instance = instance + self._cause = cause + self._invalid_params = invalid_params + self._supported_features = supported_features + + @classmethod + def from_dict(cls, dikt) -> 'ProblemDetails': + """Returns the dict as a model + + :param dikt: A dict. + :type: dict + :return: The ProblemDetails of this ProblemDetails. # noqa: E501 + :rtype: ProblemDetails + """ + return util.deserialize_model(dikt, cls) + + @property + def type(self) -> str: + """Gets the type of this ProblemDetails. + + string providing an URI formatted according to IETF RFC 3986. # noqa: E501 + + :return: The type of this ProblemDetails. + :rtype: str + """ + return self._type + + @type.setter + def type(self, type: str): + """Sets the type of this ProblemDetails. + + string providing an URI formatted according to IETF RFC 3986. # noqa: E501 + + :param type: The type of this ProblemDetails. + :type type: str + """ + + self._type = type + + @property + def title(self) -> str: + """Gets the title of this ProblemDetails. + + A short, human-readable summary of the problem type. It should not change from occurrence to occurrence of the problem. # noqa: E501 + + :return: The title of this ProblemDetails. + :rtype: str + """ + return self._title + + @title.setter + def title(self, title: str): + """Sets the title of this ProblemDetails. + + A short, human-readable summary of the problem type. It should not change from occurrence to occurrence of the problem. # noqa: E501 + + :param title: The title of this ProblemDetails. + :type title: str + """ + + self._title = title + + @property + def status(self) -> int: + """Gets the status of this ProblemDetails. + + The HTTP status code for this occurrence of the problem. # noqa: E501 + + :return: The status of this ProblemDetails. + :rtype: int + """ + return self._status + + @status.setter + def status(self, status: int): + """Sets the status of this ProblemDetails. + + The HTTP status code for this occurrence of the problem. # noqa: E501 + + :param status: The status of this ProblemDetails. + :type status: int + """ + + self._status = status + + @property + def detail(self) -> str: + """Gets the detail of this ProblemDetails. + + A human-readable explanation specific to this occurrence of the problem. # noqa: E501 + + :return: The detail of this ProblemDetails. + :rtype: str + """ + return self._detail + + @detail.setter + def detail(self, detail: str): + """Sets the detail of this ProblemDetails. + + A human-readable explanation specific to this occurrence of the problem. # noqa: E501 + + :param detail: The detail of this ProblemDetails. + :type detail: str + """ + + self._detail = detail + + @property + def instance(self) -> str: + """Gets the instance of this ProblemDetails. + + string providing an URI formatted according to IETF RFC 3986. # noqa: E501 + + :return: The instance of this ProblemDetails. + :rtype: str + """ + return self._instance + + @instance.setter + def instance(self, instance: str): + """Sets the instance of this ProblemDetails. + + string providing an URI formatted according to IETF RFC 3986. # noqa: E501 + + :param instance: The instance of this ProblemDetails. + :type instance: str + """ + + self._instance = instance + + @property + def cause(self) -> str: + """Gets the cause of this ProblemDetails. + + A machine-readable application error cause specific to this occurrence of the problem. This IE should be present and provide application-related error information, if available. # noqa: E501 + + :return: The cause of this ProblemDetails. + :rtype: str + """ + return self._cause + + @cause.setter + def cause(self, cause: str): + """Sets the cause of this ProblemDetails. + + A machine-readable application error cause specific to this occurrence of the problem. This IE should be present and provide application-related error information, if available. # noqa: E501 + + :param cause: The cause of this ProblemDetails. + :type cause: str + """ + + self._cause = cause + + @property + def invalid_params(self) -> List[InvalidParam]: + """Gets the invalid_params of this ProblemDetails. + + Description of invalid parameters, for a request rejected due to invalid parameters. # noqa: E501 + + :return: The invalid_params of this ProblemDetails. + :rtype: List[InvalidParam] + """ + return self._invalid_params + + @invalid_params.setter + def invalid_params(self, invalid_params: List[InvalidParam]): + """Sets the invalid_params of this ProblemDetails. + + Description of invalid parameters, for a request rejected due to invalid parameters. # noqa: E501 + + :param invalid_params: The invalid_params of this ProblemDetails. + :type invalid_params: List[InvalidParam] + """ + if invalid_params is not None and len(invalid_params) < 1: + raise ValueError("Invalid value for `invalid_params`, number of items must be greater than or equal to `1`") # noqa: E501 + + self._invalid_params = invalid_params + + @property + def supported_features(self) -> str: + """Gets the supported_features of this ProblemDetails. + + A string used to indicate the features supported by an API that is used as defined in clause 6.6 in 3GPP TS 29.500. The string shall contain a bitmask indicating supported features in hexadecimal representation Each character in the string shall take a value of \"0\" to \"9\", \"a\" to \"f\" or \"A\" to \"F\" and shall represent the support of 4 features as described in table 5.2.2-3. The most significant character representing the highest-numbered features shall appear first in the string, and the character representing features 1 to 4 shall appear last in the string. The list of features and their numbering (starting with 1) are defined separately for each API. If the string contains a lower number of characters than there are defined features for an API, all features that would be represented by characters that are not present in the string are not supported. # noqa: E501 + + :return: The supported_features of this ProblemDetails. + :rtype: str + """ + return self._supported_features + + @supported_features.setter + def supported_features(self, supported_features: str): + """Sets the supported_features of this ProblemDetails. + + A string used to indicate the features supported by an API that is used as defined in clause 6.6 in 3GPP TS 29.500. The string shall contain a bitmask indicating supported features in hexadecimal representation Each character in the string shall take a value of \"0\" to \"9\", \"a\" to \"f\" or \"A\" to \"F\" and shall represent the support of 4 features as described in table 5.2.2-3. The most significant character representing the highest-numbered features shall appear first in the string, and the character representing features 1 to 4 shall appear last in the string. The list of features and their numbering (starting with 1) are defined separately for each API. If the string contains a lower number of characters than there are defined features for an API, all features that would be represented by characters that are not present in the string are not supported. # noqa: E501 + + :param supported_features: The supported_features of this ProblemDetails. + :type supported_features: str + """ + if supported_features is not None and not re.search(r'^[A-Fa-f0-9]*$', supported_features): # noqa: E501 + raise ValueError(r"Invalid value for `supported_features`, must be a follow pattern or equal to `/^[A-Fa-f0-9]*$/`") # noqa: E501 + + self._supported_features = supported_features diff --git a/services/helper/helper_service/services/interconnection/openapi/openapi.yaml b/services/helper/helper_service/services/interconnection/openapi/openapi.yaml new file mode 100644 index 00000000..7b52c88e --- /dev/null +++ b/services/helper/helper_service/services/interconnection/openapi/openapi.yaml @@ -0,0 +1,231 @@ +openapi: 3.0.3 +info: + description: APIs for interconnnecting CCFs + title: CCF Interconnection API + version: 1.0.0 +servers: +- url: "{apiRoot}/interconnection" + variables: + apiRoot: + default: http://localhost:8080 + description: Base URL of the Helper service. +paths: + /connect: + post: + operationId: connect_ccfs + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/CcfInstanceDetails" + required: true + responses: + "201": + content: + application/json: + schema: + $ref: "#/components/schemas/CcfInstanceDetails" + description: Interconnection succeeded + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/ProblemDetails" + description: Bad request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/ProblemDetails" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/ProblemDetails" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/ProblemDetails" + description: Not Found + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/ProblemDetails" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/ProblemDetails" + description: Service Unavailable + default: + description: Generic Error + summary: Create a new interconnection + x-openapi-router-controller: interconnection.controllers.default_controller + /interconnect: + post: + operationId: interconnect_request + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/CapifDomainDetails" + required: true + responses: + "201": + content: + application/json: + schema: + $ref: "#/components/schemas/CapifDomainDetails" + description: Interconnection request succeeded + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/ProblemDetails" + description: Bad request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/ProblemDetails" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/ProblemDetails" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/ProblemDetails" + description: Not Found + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/ProblemDetails" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/ProblemDetails" + description: Service Unavailable + default: + description: Generic Error + summary: Send a new interconnection request + x-openapi-router-controller: interconnection.controllers.default_controller +components: + schemas: + CapifDomainDetails: + example: + dstProvDom: dstProvDom + properties: + dstProvDom: + title: dstProvDom + type: string + required: + - dstProvDom + title: CapifDomainDetails + type: object + CcfInstanceDetails: + example: + ccfId: ccfId + dstProvDom: dstProvDom + publicKey: publicKey + srcProvDom: srcProvDom + caRoot: caRoot + properties: + caRoot: + title: caRoot + type: string + publicKey: + title: publicKey + type: string + srcProvDom: + title: srcProvDom + type: string + ccfId: + title: ccfId + type: string + dstProvDom: + title: dstProvDom + type: string + required: + - caRoot + - ccfId + - dstProvDom + - publicKey + - srcProvDom + title: CcfInstanceDetails + type: object + ProblemDetails: + description: Represents additional information and details on an error response. + properties: + type: + description: string providing an URI formatted according to IETF RFC 3986. + title: type + type: string + title: + description: "A short, human-readable summary of the problem type. It should\ + \ not change from occurrence to occurrence of the problem. \n" + title: title + type: string + status: + description: The HTTP status code for this occurrence of the problem. + title: status + type: integer + detail: + description: A human-readable explanation specific to this occurrence of + the problem. + title: detail + type: string + instance: + description: string providing an URI formatted according to IETF RFC 3986. + title: type + type: string + cause: + description: | + A machine-readable application error cause specific to this occurrence of the problem. This IE should be present and provide application-related error information, if available. + title: cause + type: string + invalidParams: + description: | + Description of invalid parameters, for a request rejected due to invalid parameters. + items: + $ref: "#/components/schemas/InvalidParam" + minItems: 1 + title: invalidParams + type: array + supportedFeatures: + description: | + A string used to indicate the features supported by an API that is used as defined in clause 6.6 in 3GPP TS 29.500. The string shall contain a bitmask indicating supported features in hexadecimal representation Each character in the string shall take a value of "0" to "9", "a" to "f" or "A" to "F" and shall represent the support of 4 features as described in table 5.2.2-3. The most significant character representing the highest-numbered features shall appear first in the string, and the character representing features 1 to 4 shall appear last in the string. The list of features and their numbering (starting with 1) are defined separately for each API. If the string contains a lower number of characters than there are defined features for an API, all features that would be represented by characters that are not present in the string are not supported. + pattern: "^[A-Fa-f0-9]*$" + title: supportedFeatures + type: string + title: ProblemDetails + type: object + InvalidParam: + description: | + Represents the description of invalid parameters, for a request rejected due to invalid parameters. + properties: + param: + description: "Attribute's name encoded as a JSON Pointer, or header's name." + title: param + type: string + reason: + description: "A human-readable reason, e.g. \"must be a positive integer\"\ + ." + title: reason + type: string + required: + - param + title: InvalidParam + type: object diff --git a/services/helper/helper_service/services/interconnection/typing_utils.py b/services/helper/helper_service/services/interconnection/typing_utils.py new file mode 100644 index 00000000..74e3c913 --- /dev/null +++ b/services/helper/helper_service/services/interconnection/typing_utils.py @@ -0,0 +1,30 @@ +import sys + +if sys.version_info < (3, 7): + import typing + + def is_generic(klass): + """ Determine whether klass is a generic class """ + return type(klass) == typing.GenericMeta + + def is_dict(klass): + """ Determine whether klass is a Dict """ + return klass.__extra__ == dict + + def is_list(klass): + """ Determine whether klass is a List """ + return klass.__extra__ == list + +else: + + def is_generic(klass): + """ Determine whether klass is a generic class """ + return hasattr(klass, '__origin__') + + def is_dict(klass): + """ Determine whether klass is a Dict """ + return klass.__origin__ == dict + + def is_list(klass): + """ Determine whether klass is a List """ + return klass.__origin__ == list diff --git a/services/helper/helper_service/services/interconnection/util.py b/services/helper/helper_service/services/interconnection/util.py new file mode 100644 index 00000000..6b11bb9d --- /dev/null +++ b/services/helper/helper_service/services/interconnection/util.py @@ -0,0 +1,205 @@ +import datetime + +import typing +from interconnection import typing_utils + + +def serialize_clean_camel_case(obj): + res = obj.to_dict() + res = clean_empty(res) + res = dict_to_camel_case(res) + + return res + + +def clean_n_camel_case(res): + res = clean_empty(res) + res = dict_to_camel_case(res) + + return res + + +def clean_empty(d): + if isinstance(d, dict): + return { + k: v + for k, v in ((k, clean_empty(v)) for k, v in d.items()) + if v is not None or (isinstance(v, list) and len(v) == 0) + } + if isinstance(d, list): + return [v for v in map(clean_empty, d) if v is not None] + return d + + +def dict_to_camel_case(my_dict): + + + result = {} + + for attr, value in my_dict.items(): + if len(attr.split('_')) != 1: + my_key = ''.join(word.title() for word in attr.split('_')) + my_key= ''.join([my_key[0].lower(), my_key[1:]]) + else: + my_key = attr + + if my_key == "serviceApiCategory": + my_key = "serviceAPICategory" + + if isinstance(value, list): + result[my_key] = list(map( + lambda x: dict_to_camel_case(x) if isinstance(x, dict) else x, value )) + + elif hasattr(value, "to_dict"): + result[my_key] = dict_to_camel_case(value) + + elif isinstance(value, dict): + value = dict_to_camel_case(value) + result[my_key] = value + else: + result[my_key] = value + + return result + + +def _deserialize(data, klass): + """Deserializes dict, list, str into an object. + + :param data: dict, list or str. + :param klass: class literal, or string of class name. + + :return: object. + """ + if data is None: + return None + + if klass in (int, float, str, bool, bytearray): + return _deserialize_primitive(data, klass) + elif klass == object: + return _deserialize_object(data) + elif klass == datetime.date: + return deserialize_date(data) + elif klass == datetime.datetime: + return deserialize_datetime(data) + elif typing_utils.is_generic(klass): + if typing_utils.is_list(klass): + return _deserialize_list(data, klass.__args__[0]) + if typing_utils.is_dict(klass): + return _deserialize_dict(data, klass.__args__[1]) + else: + return deserialize_model(data, klass) + + +def _deserialize_primitive(data, klass): + """Deserializes to primitive type. + + :param data: data to deserialize. + :param klass: class literal. + + :return: int, long, float, str, bool. + :rtype: int | long | float | str | bool + """ + try: + value = klass(data) + except UnicodeEncodeError: + value = data + except TypeError: + value = data + return value + + +def _deserialize_object(value): + """Return an original value. + + :return: object. + """ + return value + + +def deserialize_date(string): + """Deserializes string to date. + + :param string: str. + :type string: str + :return: date. + :rtype: date + """ + if string is None: + return None + + try: + from dateutil.parser import parse + return parse(string).date() + except ImportError: + return string + + +def deserialize_datetime(string): + """Deserializes string to datetime. + + The string should be in iso8601 datetime format. + + :param string: str. + :type string: str + :return: datetime. + :rtype: datetime + """ + if string is None: + return None + + try: + from dateutil.parser import parse + return parse(string) + except ImportError: + return string + + +def deserialize_model(data, klass): + """Deserializes list or dict to model. + + :param data: dict, list. + :type data: dict | list + :param klass: class literal. + :return: model object. + """ + instance = klass() + + if not instance.openapi_types: + return data + + for attr, attr_type in instance.openapi_types.items(): + if data is not None \ + and instance.attribute_map[attr] in data \ + and isinstance(data, (list, dict)): + value = data[instance.attribute_map[attr]] + setattr(instance, attr, _deserialize(value, attr_type)) + + return instance + + +def _deserialize_list(data, boxed_type): + """Deserializes a list and its elements. + + :param data: list to deserialize. + :type data: list + :param boxed_type: class literal. + + :return: deserialized list. + :rtype: list + """ + return [_deserialize(sub_data, boxed_type) + for sub_data in data] + + +def _deserialize_dict(data, boxed_type): + """Deserializes a dict and its elements. + + :param data: dict to deserialize. + :type data: dict + :param boxed_type: class literal. + + :return: deserialized dict. + :rtype: dict + """ + return {k: _deserialize(v, boxed_type) + for k, v in data.items() } -- GitLab From 761dde4a3a7dbfafa8101f4b9ff728c1cad40306 Mon Sep 17 00:00:00 2001 From: Stavros-Anastasios Charismiadis Date: Tue, 16 Dec 2025 13:55:36 +0200 Subject: [PATCH 02/10] Read vault hostname from initial deployment value for all CAPIF APIs, add argument in run.sh for vault host, give helper access to nginx server certificates via volume --- .../api_invoker_management/config.py | 78 +++++++++++++++--- .../config.yaml | 12 +-- .../prepare_invoker.sh | 2 +- .../api_provider_management/config.py | 81 ++++++++++++++++--- .../config.yaml | 12 +-- .../prepare_provider.sh | 2 +- .../prepare_capif_acl.sh | 2 +- .../prepare_audit.sh | 2 +- .../prepare_discover.sh | 2 +- .../prepare_events.sh | 2 +- .../prepare_logging.sh | 2 +- .../prepare_publish.sh | 2 +- .../prepare_routing_info.sh | 2 +- .../prepare_security.sh | 2 +- services/docker-compose-capif.yml | 11 +-- services/docker-compose-register.yml | 2 +- services/helper/config.yaml | 2 +- services/helper/helper_service/config.py | 65 +++++++++++++-- .../core/capifdomaindetails.py | 27 +++++-- .../core/ccfinstancedetails.py | 20 ++++- services/register/config.yaml | 13 +-- services/register/register_prepare.sh | 1 + services/register/register_service/config.py | 81 +++++++++++++++---- services/run.sh | 28 ++++--- 24 files changed, 352 insertions(+), 101 deletions(-) diff --git a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/config.py b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/config.py index abfa4082..f4c87cae 100644 --- a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/config.py +++ b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/config.py @@ -1,23 +1,75 @@ import os import yaml +import re + + +# pattern for global vars: look for ${word} +pattern = re.compile('.*?\${(\w+)}.*?') +loader = yaml.SafeLoader + + +def constructor_env_variables(loader, node): + """ + Extracts the environment variable from the node's value + :param yaml.Loader loader: the yaml loader + :param node: the current node in the yaml + :return: the parsed string that contains the value of the environment + variable + """ + value = loader.construct_scalar(node) + match = pattern.findall(value) # to find all env variables in line + if match: + full_value = value + for g in match: + full_value = full_value.replace( + f'${{{g}}}', os.environ.get(g, g) + ) + return full_value + return value + +def parse_config(path=None, data=None, tag='!ENV'): + """ + Load a yaml configuration file and resolve any environment variables + The environment variables must have !ENV before them and be in this format + to be parsed: ${VAR_NAME}. + E.g.: + database: + host: !ENV ${HOST} + port: !ENV ${PORT} + app: + log_path: !ENV '/var/${LOG_PATH}' + something_else: !ENV '${AWESOME_ENV_VAR}/var/${A_SECOND_AWESOME_VAR}' + :param str path: the path to the yaml file + :param str data: the yaml data itself as a stream + :param str tag: the tag to look for + :return: the dict configuration + :rtype: dict[str, T] + """ + + # the tag will be used to mark where to start searching for the pattern + # e.g. somekey: !ENV somestring${MYENVVAR}blah blah blah + loader.add_implicit_resolver(tag, pattern, None) + loader.add_constructor(tag, constructor_env_variables) + + if path: + with open(path) as conf_data: + return yaml.load(conf_data, Loader=loader) + elif data: + return yaml.load(data, Loader=loader) + else: + raise ValueError('Either a path or data should be defined as input') #Config class to get config class Config: - def __init__(self): - self.cached = 0 - self.file="../config.yaml" - self.my_config = {} - - stamp = os.stat(self.file).st_mtime - if stamp != self.cached: - self.cached = stamp - f = open(self.file) - self.my_config = yaml.safe_load(f) - f.close() + def __init__(self): + self.cached = 0 + self.file="../config.yaml" + self.my_config = {} - def get_config(self): - return self.my_config + self.my_config = parse_config(path=self.file) + def get_config(self): + return self.my_config diff --git a/services/TS29222_CAPIF_API_Invoker_Management_API/config.yaml b/services/TS29222_CAPIF_API_Invoker_Management_API/config.yaml index 3107e411..c3e9f07a 100644 --- a/services/TS29222_CAPIF_API_Invoker_Management_API/config.yaml +++ b/services/TS29222_CAPIF_API_Invoker_Management_API/config.yaml @@ -9,12 +9,12 @@ mongo: { 'port': "27017" } -ca_factory: { - "url": "vault", - "port": "8200", - "token": "dev-only-token", - "verify": False -} +ca_factory: + url: !ENV ${VAULT_HOSTNAME} + port: "8200" + token: "dev-only-token" + verify: False + monitoring: { "fluent_bit_host": fluent-bit, diff --git a/services/TS29222_CAPIF_API_Invoker_Management_API/prepare_invoker.sh b/services/TS29222_CAPIF_API_Invoker_Management_API/prepare_invoker.sh index 0e2accbb..d8bcf6ca 100644 --- a/services/TS29222_CAPIF_API_Invoker_Management_API/prepare_invoker.sh +++ b/services/TS29222_CAPIF_API_Invoker_Management_API/prepare_invoker.sh @@ -26,7 +26,7 @@ while [ $ATTEMPT -lt $MAX_RETRIES ]; do if [ -n "$RESPONSE" ] && [ "$RESPONSE" != "null" ]; then echo "$RESPONSE" > /usr/src/app/api_invoker_management/pubkey.pem echo "Public key successfully saved." - gunicorn -k uvicorn.workers.UvicornH11Worker --bind 0.0.0.0:8080 \ + gunicorn -k uvicorn.workers.UvicornH11Worker --timeout 120 --bind 0.0.0.0:8080 \ --chdir /usr/src/app/api_invoker_management wsgi:app exit 0 # Exit successfully else diff --git a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/config.py b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/config.py index 404434fe..f4c87cae 100644 --- a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/config.py +++ b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/config.py @@ -1,20 +1,75 @@ import os import yaml +import re + + +# pattern for global vars: look for ${word} +pattern = re.compile('.*?\${(\w+)}.*?') +loader = yaml.SafeLoader + + +def constructor_env_variables(loader, node): + """ + Extracts the environment variable from the node's value + :param yaml.Loader loader: the yaml loader + :param node: the current node in the yaml + :return: the parsed string that contains the value of the environment + variable + """ + value = loader.construct_scalar(node) + match = pattern.findall(value) # to find all env variables in line + if match: + full_value = value + for g in match: + full_value = full_value.replace( + f'${{{g}}}', os.environ.get(g, g) + ) + return full_value + return value + +def parse_config(path=None, data=None, tag='!ENV'): + """ + Load a yaml configuration file and resolve any environment variables + The environment variables must have !ENV before them and be in this format + to be parsed: ${VAR_NAME}. + E.g.: + database: + host: !ENV ${HOST} + port: !ENV ${PORT} + app: + log_path: !ENV '/var/${LOG_PATH}' + something_else: !ENV '${AWESOME_ENV_VAR}/var/${A_SECOND_AWESOME_VAR}' + :param str path: the path to the yaml file + :param str data: the yaml data itself as a stream + :param str tag: the tag to look for + :return: the dict configuration + :rtype: dict[str, T] + """ + + # the tag will be used to mark where to start searching for the pattern + # e.g. somekey: !ENV somestring${MYENVVAR}blah blah blah + loader.add_implicit_resolver(tag, pattern, None) + loader.add_constructor(tag, constructor_env_variables) + + if path: + with open(path) as conf_data: + return yaml.load(conf_data, Loader=loader) + elif data: + return yaml.load(data, Loader=loader) + else: + raise ValueError('Either a path or data should be defined as input') #Config class to get config class Config: - def __init__(self): - self.cached = 0 - self.file="../config.yaml" - self.my_config = {} - stamp = os.stat(self.file).st_mtime - if stamp != self.cached: - self.cached = stamp - f = open(self.file) - self.my_config = yaml.safe_load(f) - f.close() - - def get_config(self): - return self.my_config + def __init__(self): + self.cached = 0 + self.file="../config.yaml" + self.my_config = {} + + self.my_config = parse_config(path=self.file) + + def get_config(self): + return self.my_config + diff --git a/services/TS29222_CAPIF_API_Provider_Management_API/config.yaml b/services/TS29222_CAPIF_API_Provider_Management_API/config.yaml index ce684f36..e2bb0de0 100644 --- a/services/TS29222_CAPIF_API_Provider_Management_API/config.yaml +++ b/services/TS29222_CAPIF_API_Provider_Management_API/config.yaml @@ -9,12 +9,12 @@ mongo: { } -ca_factory: { - "url": "vault", - "port": "8200", - "token": "dev-only-token", - "verify": False -} +ca_factory: + url: !ENV ${VAULT_HOSTNAME} + port: "8200" + token: "dev-only-token" + verify: False + monitoring: { diff --git a/services/TS29222_CAPIF_API_Provider_Management_API/prepare_provider.sh b/services/TS29222_CAPIF_API_Provider_Management_API/prepare_provider.sh index edefb582..297f6975 100644 --- a/services/TS29222_CAPIF_API_Provider_Management_API/prepare_provider.sh +++ b/services/TS29222_CAPIF_API_Provider_Management_API/prepare_provider.sh @@ -26,7 +26,7 @@ while [ $ATTEMPT -lt $MAX_RETRIES ]; do if [ -n "$RESPONSE" ] && [ "$RESPONSE" != "null" ]; then echo "$RESPONSE" > /usr/src/app/api_provider_management/pubkey.pem echo "Public key successfully saved." - gunicorn -k uvicorn.workers.UvicornH11Worker --bind 0.0.0.0:8080 \ + gunicorn -k uvicorn.workers.UvicornH11Worker --timeout 120 --bind 0.0.0.0:8080 \ --chdir /usr/src/app/api_provider_management wsgi:app exit 0 # Exit successfully else diff --git a/services/TS29222_CAPIF_Access_Control_Policy_API/prepare_capif_acl.sh b/services/TS29222_CAPIF_Access_Control_Policy_API/prepare_capif_acl.sh index 42f12afe..41d326c5 100644 --- a/services/TS29222_CAPIF_Access_Control_Policy_API/prepare_capif_acl.sh +++ b/services/TS29222_CAPIF_Access_Control_Policy_API/prepare_capif_acl.sh @@ -1,4 +1,4 @@ #!/bin/bash -gunicorn -k uvicorn.workers.UvicornH11Worker --bind 0.0.0.0:8080 \ +gunicorn -k uvicorn.workers.UvicornH11Worker --timeout 120 --bind 0.0.0.0:8080 \ --chdir /usr/src/app/capif_acl wsgi:app \ No newline at end of file diff --git a/services/TS29222_CAPIF_Auditing_API/prepare_audit.sh b/services/TS29222_CAPIF_Auditing_API/prepare_audit.sh index 6049f2e9..75fee0b1 100644 --- a/services/TS29222_CAPIF_Auditing_API/prepare_audit.sh +++ b/services/TS29222_CAPIF_Auditing_API/prepare_audit.sh @@ -1,6 +1,6 @@ #!/bin/bash -gunicorn -k uvicorn.workers.UvicornH11Worker --bind 0.0.0.0:8080 \ +gunicorn -k uvicorn.workers.UvicornH11Worker --timeout 120 --bind 0.0.0.0:8080 \ --chdir /usr/src/app/logs wsgi:app diff --git a/services/TS29222_CAPIF_Discover_Service_API/prepare_discover.sh b/services/TS29222_CAPIF_Discover_Service_API/prepare_discover.sh index dd3f3612..43bcd30c 100644 --- a/services/TS29222_CAPIF_Discover_Service_API/prepare_discover.sh +++ b/services/TS29222_CAPIF_Discover_Service_API/prepare_discover.sh @@ -1,6 +1,6 @@ #!/bin/bash -gunicorn -k uvicorn.workers.UvicornH11Worker --bind 0.0.0.0:8080 \ +gunicorn -k uvicorn.workers.UvicornH11Worker --timeout 120 --bind 0.0.0.0:8080 \ --chdir /usr/src/app/service_apis wsgi:app diff --git a/services/TS29222_CAPIF_Events_API/prepare_events.sh b/services/TS29222_CAPIF_Events_API/prepare_events.sh index bf16c7f3..e3bf0423 100644 --- a/services/TS29222_CAPIF_Events_API/prepare_events.sh +++ b/services/TS29222_CAPIF_Events_API/prepare_events.sh @@ -1,4 +1,4 @@ #!/bin/bash -gunicorn -k uvicorn.workers.UvicornH11Worker --bind 0.0.0.0:8080 \ +gunicorn -k uvicorn.workers.UvicornH11Worker --timeout 120 --bind 0.0.0.0:8080 \ --chdir /usr/src/app/capif_events wsgi:app diff --git a/services/TS29222_CAPIF_Logging_API_Invocation_API/prepare_logging.sh b/services/TS29222_CAPIF_Logging_API_Invocation_API/prepare_logging.sh index 25c0c0ec..29b210da 100644 --- a/services/TS29222_CAPIF_Logging_API_Invocation_API/prepare_logging.sh +++ b/services/TS29222_CAPIF_Logging_API_Invocation_API/prepare_logging.sh @@ -1,4 +1,4 @@ #!/bin/bash -gunicorn -k uvicorn.workers.UvicornH11Worker --bind 0.0.0.0:8080 \ +gunicorn -k uvicorn.workers.UvicornH11Worker --timeout 120 --bind 0.0.0.0:8080 \ --chdir /usr/src/app/api_invocation_logs wsgi:app diff --git a/services/TS29222_CAPIF_Publish_Service_API/prepare_publish.sh b/services/TS29222_CAPIF_Publish_Service_API/prepare_publish.sh index 8526fe88..6e019e1c 100644 --- a/services/TS29222_CAPIF_Publish_Service_API/prepare_publish.sh +++ b/services/TS29222_CAPIF_Publish_Service_API/prepare_publish.sh @@ -1,4 +1,4 @@ #!/bin/bash -gunicorn -k uvicorn.workers.UvicornH11Worker --bind 0.0.0.0:8080 \ +gunicorn -k uvicorn.workers.UvicornH11Worker --timeout 120 --bind 0.0.0.0:8080 \ --chdir /usr/src/app/published_apis wsgi:app diff --git a/services/TS29222_CAPIF_Routing_Info_API/prepare_routing_info.sh b/services/TS29222_CAPIF_Routing_Info_API/prepare_routing_info.sh index aa37163f..2c8c8d10 100644 --- a/services/TS29222_CAPIF_Routing_Info_API/prepare_routing_info.sh +++ b/services/TS29222_CAPIF_Routing_Info_API/prepare_routing_info.sh @@ -1,4 +1,4 @@ #!/bin/bash -gunicorn -k uvicorn.workers.UvicornH11Worker --bind 0.0.0.0:8080 \ +gunicorn -k uvicorn.workers.UvicornH11Worker --timeout 120 --bind 0.0.0.0:8080 \ --chdir /usr/src/app/capif_routing_info wsgi:app diff --git a/services/TS29222_CAPIF_Security_API/prepare_security.sh b/services/TS29222_CAPIF_Security_API/prepare_security.sh index c14609ad..191d727b 100644 --- a/services/TS29222_CAPIF_Security_API/prepare_security.sh +++ b/services/TS29222_CAPIF_Security_API/prepare_security.sh @@ -76,5 +76,5 @@ if [ "$SUCCES_OPERATION" = false ]; then exit 1 # Exit with failure fi -gunicorn -k uvicorn.workers.UvicornH11Worker --bind 0.0.0.0:8080 \ +gunicorn -k uvicorn.workers.UvicornH11Worker --timeout 120 --bind 0.0.0.0:8080 \ --chdir $CERTS_FOLDER wsgi:app diff --git a/services/docker-compose-capif.yml b/services/docker-compose-capif.yml index 01a34a6c..67af0251 100644 --- a/services/docker-compose-capif.yml +++ b/services/docker-compose-capif.yml @@ -21,6 +21,7 @@ services: volumes: - ${SERVICES_DIR}/helper/config.yaml:/usr/src/app/config.yaml - ${SERVICES_DIR}/helper/helper_service/certs:/usr/src/app/helper_service/certs + - ${SERVICES_DIR}/nginx/certs:/usr/src/app/helper_service/server_certs:ro extra_hosts: - host.docker.internal:host-gateway - fluent-bit:host-gateway @@ -29,7 +30,7 @@ services: environment: - CAPIF_HOSTNAME=${CAPIF_HOSTNAME} - CONTAINER_NAME=helper - - VAULT_HOSTNAME=vault + - VAULT_HOSTNAME=${CAPIF_VAULT} - VAULT_ACCESS_TOKEN=dev-only-token - VAULT_PORT=8200 - LOG_LEVEL=${LOG_LEVEL} @@ -76,7 +77,7 @@ services: - CAPIF_HOSTNAME=${CAPIF_HOSTNAME} - CONTAINER_NAME=api-invoker-management - MONITORING=${MONITORING} - - VAULT_HOSTNAME=vault + - VAULT_HOSTNAME=${CAPIF_VAULT} - VAULT_ACCESS_TOKEN=dev-only-token - VAULT_PORT=8200 - LOG_LEVEL=${LOG_LEVEL} @@ -104,7 +105,7 @@ services: - CAPIF_HOSTNAME=${CAPIF_HOSTNAME} - CONTAINER_NAME=api-provider-management - MONITORING=${MONITORING} - - VAULT_HOSTNAME=vault + - VAULT_HOSTNAME=${CAPIF_VAULT} - VAULT_ACCESS_TOKEN=dev-only-token - VAULT_PORT=8200 - LOG_LEVEL=${LOG_LEVEL} @@ -263,7 +264,7 @@ services: - CAPIF_HOSTNAME=${CAPIF_HOSTNAME} - CONTAINER_NAME=api-security - MONITORING=${MONITORING} - - VAULT_HOSTNAME=vault + - VAULT_HOSTNAME=${CAPIF_VAULT} - VAULT_ACCESS_TOKEN=dev-only-token - VAULT_PORT=8200 - LOG_LEVEL=${LOG_LEVEL} @@ -312,7 +313,7 @@ services: image: labs.etsi.org:5050/ocf/capif/nginx-ocf-patched:1.27.1 environment: - CAPIF_HOSTNAME=${CAPIF_HOSTNAME} - - VAULT_HOSTNAME=vault + - VAULT_HOSTNAME=${CAPIF_VAULT} - VAULT_ACCESS_TOKEN=dev-only-token - VAULT_PORT=8200 - LOG_LEVEL=${LOG_LEVEL} diff --git a/services/docker-compose-register.yml b/services/docker-compose-register.yml index 66607e55..5f12ed8c 100644 --- a/services/docker-compose-register.yml +++ b/services/docker-compose-register.yml @@ -10,7 +10,7 @@ services: - ${SERVICES_DIR}/register/config.yaml:/usr/src/app/config.yaml environment: - CAPIF_PRIV_KEY=${CAPIF_PRIV_KEY} - - VAULT_HOSTNAME=vault + - VAULT_HOSTNAME=${CAPIF_VAULT} - VAULT_ACCESS_TOKEN=dev-only-token - VAULT_PORT=8200 - LOG_LEVEL=${LOG_LEVEL} diff --git a/services/helper/config.yaml b/services/helper/config.yaml index 78d58ae5..a162bac2 100644 --- a/services/helper/config.yaml +++ b/services/helper/config.yaml @@ -13,7 +13,7 @@ mongo: ca_factory: - url: vault + url: !ENV ${VAULT_HOSTNAME} port: 8200 token: dev-only-token verify: False diff --git a/services/helper/helper_service/config.py b/services/helper/helper_service/config.py index d9f4ad1c..f4c87cae 100644 --- a/services/helper/helper_service/config.py +++ b/services/helper/helper_service/config.py @@ -1,6 +1,64 @@ import os import yaml +import re + + +# pattern for global vars: look for ${word} +pattern = re.compile('.*?\${(\w+)}.*?') +loader = yaml.SafeLoader + + +def constructor_env_variables(loader, node): + """ + Extracts the environment variable from the node's value + :param yaml.Loader loader: the yaml loader + :param node: the current node in the yaml + :return: the parsed string that contains the value of the environment + variable + """ + value = loader.construct_scalar(node) + match = pattern.findall(value) # to find all env variables in line + if match: + full_value = value + for g in match: + full_value = full_value.replace( + f'${{{g}}}', os.environ.get(g, g) + ) + return full_value + return value + +def parse_config(path=None, data=None, tag='!ENV'): + """ + Load a yaml configuration file and resolve any environment variables + The environment variables must have !ENV before them and be in this format + to be parsed: ${VAR_NAME}. + E.g.: + database: + host: !ENV ${HOST} + port: !ENV ${PORT} + app: + log_path: !ENV '/var/${LOG_PATH}' + something_else: !ENV '${AWESOME_ENV_VAR}/var/${A_SECOND_AWESOME_VAR}' + :param str path: the path to the yaml file + :param str data: the yaml data itself as a stream + :param str tag: the tag to look for + :return: the dict configuration + :rtype: dict[str, T] + """ + + # the tag will be used to mark where to start searching for the pattern + # e.g. somekey: !ENV somestring${MYENVVAR}blah blah blah + loader.add_implicit_resolver(tag, pattern, None) + loader.add_constructor(tag, constructor_env_variables) + + if path: + with open(path) as conf_data: + return yaml.load(conf_data, Loader=loader) + elif data: + return yaml.load(data, Loader=loader) + else: + raise ValueError('Either a path or data should be defined as input') #Config class to get config @@ -10,12 +68,7 @@ class Config: self.file="../config.yaml" self.my_config = {} - stamp = os.stat(self.file).st_mtime - if stamp != self.cached: - self.cached = stamp - f = open(self.file) - self.my_config = yaml.safe_load(f) - f.close() + self.my_config = parse_config(path=self.file) def get_config(self): return self.my_config diff --git a/services/helper/helper_service/services/interconnection/core/capifdomaindetails.py b/services/helper/helper_service/services/interconnection/core/capifdomaindetails.py index 5e134457..746b02d7 100644 --- a/services/helper/helper_service/services/interconnection/core/capifdomaindetails.py +++ b/services/helper/helper_service/services/interconnection/core/capifdomaindetails.py @@ -7,6 +7,7 @@ import json from flask import current_app # from pymongo import ReturnDocument +from db.db import get_mongo from ..models.ccf_instance_details import CcfInstanceDetails from ..util import clean_empty, clean_n_camel_case, dict_to_camel_case # from ..vendor_specific import add_vend_spec_fields @@ -35,6 +36,7 @@ class CapifDomainOperations(Resource): def __init__(self): Resource.__init__(self) self.auth_manager = AuthManager() + self.db = get_mongo() def add_capifdomain(self, capifdomaindetails): current_app.logger.debug("Interconnection: Add domain") @@ -42,12 +44,25 @@ class CapifDomainOperations(Resource): url = "https://{}/helper/interconnection/connect".format(capifdomaindetails.dst_prov_dom) + with open('server_certs/ca.crt', 'rb') as ca_cert: + server_ca = ca_cert.read() + ca_cert.close() + + with open('server_certs/server.crt', 'rb') as server_cert: + server_pub = server_cert.read() + server_cert.close() + + config_col = self.db.get_col_by_name(self.db.capif_configuration) + config = config_col.find_one({}, {"_id": 0}) + # current_app.logger.debug(config) + # current_app.logger.debug(config['ccf_id']) + payload = json.dumps({ - "caRoot": "caRoot", - "ccfId": "ccfId", + "caRoot": server_ca.decode("utf-8"), + "ccfId": config['ccf_id'], "dstProvDom": capifdomaindetails.dst_prov_dom, - "publicKey": "publicKey", - "srcProvDom": srcProvDom + "publicKey": server_pub.decode("utf-8"), + "srcProvDom": os.getenv("CAPIF_HOSTNAME") }) headers = { 'accept': 'application/json', @@ -55,8 +70,8 @@ class CapifDomainOperations(Resource): } response = requests.request("POST", url, headers=headers, data=payload, - cert=('certs/superadmin.crt', 'certs/superadmin.key'), - verify='certs/ca_root.crt') + cert=('server_certs/server.crt', 'server_certs/server.key'), + verify='server_certs/ca.crt') current_app.logger.debug(response.text) diff --git a/services/helper/helper_service/services/interconnection/core/ccfinstancedetails.py b/services/helper/helper_service/services/interconnection/core/ccfinstancedetails.py index a699ae1c..d260f0a9 100644 --- a/services/helper/helper_service/services/interconnection/core/ccfinstancedetails.py +++ b/services/helper/helper_service/services/interconnection/core/ccfinstancedetails.py @@ -5,6 +5,7 @@ from datetime import datetime from flask import current_app # from pymongo import ReturnDocument +from db.db import get_mongo from ..models.ccf_instance_details import CcfInstanceDetails from ..util import clean_empty, clean_n_camel_case, dict_to_camel_case # from ..vendor_specific import add_vend_spec_fields @@ -32,14 +33,27 @@ class CcfInstanceOperations(Resource): def __init__(self): Resource.__init__(self) self.auth_manager = AuthManager() + self.db = get_mongo() def add_ccfinstance(self, ccfinstancedetails): current_app.logger.debug("Interconnection: Add instance") current_app.logger.debug(ccfinstancedetails) ccfinstancedetails_dict = ccfinstancedetails.to_dict() - ccfinstancedetails_dict['ca_root'] = "caRoot2" - ccfinstancedetails_dict['public_key'] = "publicKey2" - ccfinstancedetails_dict['ccf_id'] = "ccfId2" + + with open('server_certs/ca.crt', 'rb') as ca_cert: + server_ca = ca_cert.read() + ca_cert.close() + + with open('server_certs/server.crt', 'rb') as server_cert: + server_pub = server_cert.read() + server_cert.close() + + config_col = self.db.get_col_by_name(self.db.capif_configuration) + config = config_col.find_one({}, {"_id": 0}) + + ccfinstancedetails_dict['ca_root'] = server_ca.decode("utf-8") + ccfinstancedetails_dict['public_key'] = server_pub.decode("utf-8") + ccfinstancedetails_dict['ccf_id'] = config['ccf_id'] ccfinstancedetails_dict['dst_prov_dom'], ccfinstancedetails_dict['src_prov_dom'] = ccfinstancedetails_dict['src_prov_dom'], ccfinstancedetails_dict['dst_prov_dom'] current_app.logger.debug(ccfinstancedetails_dict) res = make_response(object=clean_n_camel_case(ccfinstancedetails_dict), status=201) diff --git a/services/register/config.yaml b/services/register/config.yaml index 85fb232c..efe62888 100644 --- a/services/register/config.yaml +++ b/services/register/config.yaml @@ -8,12 +8,13 @@ mongo: { 'host': 'mongo_register', 'port': '27017' } -ca_factory: { - "url": "vault", - "port": "8200", - "token": "dev-only-token", - "verify": False -} + +ca_factory: + url: !ENV ${VAULT_HOSTNAME} + port: "8200" + token: "dev-only-token" + verify: False + ccf: { "url": "capifcore", diff --git a/services/register/register_prepare.sh b/services/register/register_prepare.sh index 05234114..385011d9 100644 --- a/services/register/register_prepare.sh +++ b/services/register/register_prepare.sh @@ -59,5 +59,6 @@ echo "Starting Register service with signed certificate." gunicorn --certfile=/usr/src/app/register_service/certs/register_cert.crt \ --keyfile=/usr/src/app/register_service/certs/register_key.key \ --ca-certs=/usr/src/app/register_service/certs/ca_root.crt \ + --timeout 120 \ --bind 0.0.0.0:8080 \ --chdir /usr/src/app/register_service wsgi:app \ No newline at end of file diff --git a/services/register/register_service/config.py b/services/register/register_service/config.py index 2ac31772..f4c87cae 100644 --- a/services/register/register_service/config.py +++ b/services/register/register_service/config.py @@ -1,22 +1,75 @@ import os import yaml +import re + + +# pattern for global vars: look for ${word} +pattern = re.compile('.*?\${(\w+)}.*?') +loader = yaml.SafeLoader + + +def constructor_env_variables(loader, node): + """ + Extracts the environment variable from the node's value + :param yaml.Loader loader: the yaml loader + :param node: the current node in the yaml + :return: the parsed string that contains the value of the environment + variable + """ + value = loader.construct_scalar(node) + match = pattern.findall(value) # to find all env variables in line + if match: + full_value = value + for g in match: + full_value = full_value.replace( + f'${{{g}}}', os.environ.get(g, g) + ) + return full_value + return value + +def parse_config(path=None, data=None, tag='!ENV'): + """ + Load a yaml configuration file and resolve any environment variables + The environment variables must have !ENV before them and be in this format + to be parsed: ${VAR_NAME}. + E.g.: + database: + host: !ENV ${HOST} + port: !ENV ${PORT} + app: + log_path: !ENV '/var/${LOG_PATH}' + something_else: !ENV '${AWESOME_ENV_VAR}/var/${A_SECOND_AWESOME_VAR}' + :param str path: the path to the yaml file + :param str data: the yaml data itself as a stream + :param str tag: the tag to look for + :return: the dict configuration + :rtype: dict[str, T] + """ + + # the tag will be used to mark where to start searching for the pattern + # e.g. somekey: !ENV somestring${MYENVVAR}blah blah blah + loader.add_implicit_resolver(tag, pattern, None) + loader.add_constructor(tag, constructor_env_variables) + + if path: + with open(path) as conf_data: + return yaml.load(conf_data, Loader=loader) + elif data: + return yaml.load(data, Loader=loader) + else: + raise ValueError('Either a path or data should be defined as input') #Config class to get config class Config: - def __init__(self): - self.cached = 0 - self.file="../config.yaml" - self.my_config = {} - - stamp = os.stat(self.file).st_mtime - if stamp != self.cached: - self.cached = stamp - f = open(self.file) - self.my_config = yaml.safe_load(f) - f.close() - - def get_config(self): - return self.my_config + def __init__(self): + self.cached = 0 + self.file="../config.yaml" + self.my_config = {} + + self.my_config = parse_config(path=self.file) + + def get_config(self): + return self.my_config diff --git a/services/run.sh b/services/run.sh index 5acc9afe..d82f6b25 100755 --- a/services/run.sh +++ b/services/run.sh @@ -4,6 +4,7 @@ source $(dirname "$(readlink -f "$0")")/variables.sh help() { echo "Usage: $1 " echo " -c : Setup different hostname for capif" + echo " -a : Setup different hostname for vault" echo " -R : Setup different hostname for register service" echo " -s : Run Mock server. Default true" echo " -m : Run monitoring service" @@ -36,11 +37,14 @@ then fi # Read params -while getopts ":c:l:ms:hrv:f:g:b:" opt; do +while getopts ":c:a:l:ms:hrv:f:g:b:" opt; do case $opt in c) CAPIF_HOSTNAME="$OPTARG" ;; + a) + CAPIF_VAULT="$OPTARG" + ;; R) CAPIF_REGISTER="$OPTARG" ;; @@ -82,7 +86,7 @@ while getopts ":c:l:ms:hrv:f:g:b:" opt; do esac done -echo Nginx hostname will be $CAPIF_HOSTNAME, Register Hostname $CAPIF_REGISTER, deploy $DEPLOY, monitoring $MONITORING_STATE +echo Nginx hostname will be $CAPIF_HOSTNAME, Vault $CAPIF_VAULT, Register Hostname $CAPIF_REGISTER, deploy $DEPLOY, monitoring $MONITORING_STATE if [ "$BUILD_DOCKER_IMAGES" == "true" ] ; then echo '***Building Docker images set as true***' @@ -109,15 +113,17 @@ fi docker network create capif-network -# Deploy Vault service -REGISTRY_BASE_URL=$REGISTRY_BASE_URL OCF_VERSION=$OCF_VERSION CAPIF_HOSTNAME=$CAPIF_HOSTNAME docker compose -f "$SERVICES_DIR/docker-compose-vault.yml" up --detach $BUILD $CACHED_INFO - -status=$? -if [ $status -eq 0 ]; then - echo "*** Vault Service Runing ***" -else - echo "*** Vault failed to start ***" - exit $status +## Deploy Vault service +if [ "$CAPIF_VAULT" == "vault" ] ; then + REGISTRY_BASE_URL=$REGISTRY_BASE_URL OCF_VERSION=$OCF_VERSION CAPIF_HOSTNAME=$CAPIF_HOSTNAME docker compose -f "$SERVICES_DIR/docker-compose-vault.yml" up --detach $BUILD $CACHED_INFO + + status=$? + if [ $status -eq 0 ]; then + echo "*** Vault Service Runing ***" + else + echo "*** Vault failed to start ***" + exit $status + fi fi # Deploy Capif services -- GitLab From a9e1815a955e6a18ec93ac4d47b43423afeaaade Mon Sep 17 00:00:00 2001 From: Stavros-Anastasios Charismiadis Date: Wed, 17 Dec 2025 11:38:08 +0200 Subject: [PATCH 03/10] add database interaction for interconnected ccfs (not fully tested) --- services/helper/config.yaml | 1 + .../interconnection/core/capifdomaindetails.py | 18 ++++++++++++------ .../interconnection/core/ccfinstancedetails.py | 10 +++++++++- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/services/helper/config.yaml b/services/helper/config.yaml index a162bac2..f331cb38 100644 --- a/services/helper/config.yaml +++ b/services/helper/config.yaml @@ -8,6 +8,7 @@ mongo: col_security: security col_event: eventsdetails col_capif_configuration: capif_configuration + col_interconnected: interconnected host: mongo port: 27017 diff --git a/services/helper/helper_service/services/interconnection/core/capifdomaindetails.py b/services/helper/helper_service/services/interconnection/core/capifdomaindetails.py index 746b02d7..e65f532b 100644 --- a/services/helper/helper_service/services/interconnection/core/capifdomaindetails.py +++ b/services/helper/helper_service/services/interconnection/core/capifdomaindetails.py @@ -73,10 +73,16 @@ class CapifDomainOperations(Resource): cert=('server_certs/server.crt', 'server_certs/server.key'), verify='server_certs/ca.crt') - current_app.logger.debug(response.text) - - capifdomaindetails_dict = capifdomaindetails.to_dict() - current_app.logger.debug(capifdomaindetails_dict) - - res = make_response(object=clean_n_camel_case(capifdomaindetails_dict), status=response.status_code) + inter_ccf = response.json() + current_app.logger.debug(inter_ccf) + + if response.status_code == 201: + interconnected_col = self.db.get_col_by_name(self.db.col_interconnected) + interconnected_ccf = interconnected_col.find_one({"ccf_id": inter_ccf['ccf_id']}) + if interconnected_ccf: + current_app.logger.debug("CCF already interconnected : {}".format(inter_ccf['ccf_id'])) + return make_response("CCF already interconnected"), 409 + interconnected_col.insert_one(inter_ccf) + + res = make_response(object=clean_n_camel_case(inter_ccf), status=response.status_code) return res diff --git a/services/helper/helper_service/services/interconnection/core/ccfinstancedetails.py b/services/helper/helper_service/services/interconnection/core/ccfinstancedetails.py index d260f0a9..c2dff97b 100644 --- a/services/helper/helper_service/services/interconnection/core/ccfinstancedetails.py +++ b/services/helper/helper_service/services/interconnection/core/ccfinstancedetails.py @@ -38,7 +38,6 @@ class CcfInstanceOperations(Resource): def add_ccfinstance(self, ccfinstancedetails): current_app.logger.debug("Interconnection: Add instance") current_app.logger.debug(ccfinstancedetails) - ccfinstancedetails_dict = ccfinstancedetails.to_dict() with open('server_certs/ca.crt', 'rb') as ca_cert: server_ca = ca_cert.read() @@ -51,6 +50,15 @@ class CcfInstanceOperations(Resource): config_col = self.db.get_col_by_name(self.db.capif_configuration) config = config_col.find_one({}, {"_id": 0}) + interconnected_col = self.db.get_col_by_name(self.db.col_interconnected) + interconnected_ccf = interconnected_col.find_one({"ccf_id": ccfinstancedetails.ccf_id}) + if interconnected_ccf: + current_app.logger.debug("CCF already interconnected : {}".format(ccfinstancedetails.ccf_id)) + return make_response("CCF already interconnected"), 409 + + ccfinstancedetails_dict = ccfinstancedetails.to_dict() + interconnected_col.insert_one(ccfinstancedetails_dict) + ccfinstancedetails_dict['ca_root'] = server_ca.decode("utf-8") ccfinstancedetails_dict['public_key'] = server_pub.decode("utf-8") ccfinstancedetails_dict['ccf_id'] = config['ccf_id'] -- GitLab From 7aa6215fcdb19e3e21dba8d44fd492070dd6f688 Mon Sep 17 00:00:00 2001 From: Stavros-Anastasios Charismiadis Date: Wed, 17 Dec 2025 11:57:53 +0200 Subject: [PATCH 04/10] Fix database connection (add interconnection db in mongodb object) --- services/helper/helper_service/db/db.py | 1 + .../services/interconnection/core/capifdomaindetails.py | 2 +- .../services/interconnection/core/ccfinstancedetails.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/services/helper/helper_service/db/db.py b/services/helper/helper_service/db/db.py index 757933d3..f8c7e014 100644 --- a/services/helper/helper_service/db/db.py +++ b/services/helper/helper_service/db/db.py @@ -38,6 +38,7 @@ class MongoDatabse(): self.security_context_col = self.config['mongo']['col_security'] self.events = self.config['mongo']['col_event'] self.capif_configuration = self.config['mongo']['col_capif_configuration'] + self.interconnected = self.config['mongo']['col_interconnected'] self.initialize_capif_configuration() diff --git a/services/helper/helper_service/services/interconnection/core/capifdomaindetails.py b/services/helper/helper_service/services/interconnection/core/capifdomaindetails.py index e65f532b..c7e91047 100644 --- a/services/helper/helper_service/services/interconnection/core/capifdomaindetails.py +++ b/services/helper/helper_service/services/interconnection/core/capifdomaindetails.py @@ -77,7 +77,7 @@ class CapifDomainOperations(Resource): current_app.logger.debug(inter_ccf) if response.status_code == 201: - interconnected_col = self.db.get_col_by_name(self.db.col_interconnected) + interconnected_col = self.db.get_col_by_name(self.db.interconnected) interconnected_ccf = interconnected_col.find_one({"ccf_id": inter_ccf['ccf_id']}) if interconnected_ccf: current_app.logger.debug("CCF already interconnected : {}".format(inter_ccf['ccf_id'])) diff --git a/services/helper/helper_service/services/interconnection/core/ccfinstancedetails.py b/services/helper/helper_service/services/interconnection/core/ccfinstancedetails.py index c2dff97b..78f9c335 100644 --- a/services/helper/helper_service/services/interconnection/core/ccfinstancedetails.py +++ b/services/helper/helper_service/services/interconnection/core/ccfinstancedetails.py @@ -50,7 +50,7 @@ class CcfInstanceOperations(Resource): config_col = self.db.get_col_by_name(self.db.capif_configuration) config = config_col.find_one({}, {"_id": 0}) - interconnected_col = self.db.get_col_by_name(self.db.col_interconnected) + interconnected_col = self.db.get_col_by_name(self.db.interconnected) interconnected_ccf = interconnected_col.find_one({"ccf_id": ccfinstancedetails.ccf_id}) if interconnected_ccf: current_app.logger.debug("CCF already interconnected : {}".format(ccfinstancedetails.ccf_id)) -- GitLab From f67d1b468c5ac05604f888608df8dc88cc2c5f7a Mon Sep 17 00:00:00 2001 From: Stavros-Anastasios Charismiadis Date: Wed, 17 Dec 2025 12:23:56 +0200 Subject: [PATCH 05/10] Fix responses --- .../services/interconnection/core/capifdomaindetails.py | 2 +- .../services/interconnection/core/ccfinstancedetails.py | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/services/helper/helper_service/services/interconnection/core/capifdomaindetails.py b/services/helper/helper_service/services/interconnection/core/capifdomaindetails.py index c7e91047..eb234866 100644 --- a/services/helper/helper_service/services/interconnection/core/capifdomaindetails.py +++ b/services/helper/helper_service/services/interconnection/core/capifdomaindetails.py @@ -81,7 +81,7 @@ class CapifDomainOperations(Resource): interconnected_ccf = interconnected_col.find_one({"ccf_id": inter_ccf['ccf_id']}) if interconnected_ccf: current_app.logger.debug("CCF already interconnected : {}".format(inter_ccf['ccf_id'])) - return make_response("CCF already interconnected"), 409 + return make_response("CCF already interconnected", 409) interconnected_col.insert_one(inter_ccf) res = make_response(object=clean_n_camel_case(inter_ccf), status=response.status_code) diff --git a/services/helper/helper_service/services/interconnection/core/ccfinstancedetails.py b/services/helper/helper_service/services/interconnection/core/ccfinstancedetails.py index 78f9c335..b314ad30 100644 --- a/services/helper/helper_service/services/interconnection/core/ccfinstancedetails.py +++ b/services/helper/helper_service/services/interconnection/core/ccfinstancedetails.py @@ -7,7 +7,7 @@ from flask import current_app from db.db import get_mongo from ..models.ccf_instance_details import CcfInstanceDetails -from ..util import clean_empty, clean_n_camel_case, dict_to_camel_case +from ..util import clean_empty, clean_n_camel_case, dict_to_camel_case, serialize_clean_camel_case # from ..vendor_specific import add_vend_spec_fields from .auth_manager import AuthManager from .publisher import Publisher @@ -54,7 +54,7 @@ class CcfInstanceOperations(Resource): interconnected_ccf = interconnected_col.find_one({"ccf_id": ccfinstancedetails.ccf_id}) if interconnected_ccf: current_app.logger.debug("CCF already interconnected : {}".format(ccfinstancedetails.ccf_id)) - return make_response("CCF already interconnected"), 409 + return make_response("CCF already interconnected", 409) ccfinstancedetails_dict = ccfinstancedetails.to_dict() interconnected_col.insert_one(ccfinstancedetails_dict) @@ -64,5 +64,7 @@ class CcfInstanceOperations(Resource): ccfinstancedetails_dict['ccf_id'] = config['ccf_id'] ccfinstancedetails_dict['dst_prov_dom'], ccfinstancedetails_dict['src_prov_dom'] = ccfinstancedetails_dict['src_prov_dom'], ccfinstancedetails_dict['dst_prov_dom'] current_app.logger.debug(ccfinstancedetails_dict) - res = make_response(object=clean_n_camel_case(ccfinstancedetails_dict), status=201) + + ccfinstancedetails_new = CcfInstanceDetails().from_dict(dict_to_camel_case(ccfinstancedetails_dict)) + res = make_response(object=serialize_clean_camel_case(ccfinstancedetails_new), status=201) return res \ No newline at end of file -- GitLab From 44d792d8c84003c7f4595f365f3557f6a0fbd45f Mon Sep 17 00:00:00 2001 From: Stavros-Anastasios Charismiadis Date: Wed, 17 Dec 2025 13:55:04 +0200 Subject: [PATCH 06/10] More fixes on responses --- .../services/interconnection/core/capifdomaindetails.py | 5 +++-- .../services/interconnection/core/ccfinstancedetails.py | 5 +++-- .../helper/helper_service/services/interconnection/util.py | 7 +++++++ 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/services/helper/helper_service/services/interconnection/core/capifdomaindetails.py b/services/helper/helper_service/services/interconnection/core/capifdomaindetails.py index eb234866..ad25f0f4 100644 --- a/services/helper/helper_service/services/interconnection/core/capifdomaindetails.py +++ b/services/helper/helper_service/services/interconnection/core/capifdomaindetails.py @@ -9,7 +9,7 @@ from flask import current_app from db.db import get_mongo from ..models.ccf_instance_details import CcfInstanceDetails -from ..util import clean_empty, clean_n_camel_case, dict_to_camel_case +from ..util import clean_empty, clean_n_camel_case, dict_to_camel_case, serialize_clean_camel_case # from ..vendor_specific import add_vend_spec_fields from .auth_manager import AuthManager from .publisher import Publisher @@ -84,5 +84,6 @@ class CapifDomainOperations(Resource): return make_response("CCF already interconnected", 409) interconnected_col.insert_one(inter_ccf) - res = make_response(object=clean_n_camel_case(inter_ccf), status=response.status_code) + ccfinstancedetails_new = CcfInstanceDetails().from_dict(dict_to_camel_case(inter_ccf)) + res = make_response(object=ccfinstancedetails_new, status=response.status_code) return res diff --git a/services/helper/helper_service/services/interconnection/core/ccfinstancedetails.py b/services/helper/helper_service/services/interconnection/core/ccfinstancedetails.py index b314ad30..26f27aaf 100644 --- a/services/helper/helper_service/services/interconnection/core/ccfinstancedetails.py +++ b/services/helper/helper_service/services/interconnection/core/ccfinstancedetails.py @@ -7,7 +7,7 @@ from flask import current_app from db.db import get_mongo from ..models.ccf_instance_details import CcfInstanceDetails -from ..util import clean_empty, clean_n_camel_case, dict_to_camel_case, serialize_clean_camel_case +from ..util import clean_empty, clean_n_camel_case, dict_to_camel_case, serialize_clean # from ..vendor_specific import add_vend_spec_fields from .auth_manager import AuthManager from .publisher import Publisher @@ -66,5 +66,6 @@ class CcfInstanceOperations(Resource): current_app.logger.debug(ccfinstancedetails_dict) ccfinstancedetails_new = CcfInstanceDetails().from_dict(dict_to_camel_case(ccfinstancedetails_dict)) - res = make_response(object=serialize_clean_camel_case(ccfinstancedetails_new), status=201) + current_app.logger.debug(ccfinstancedetails_new) + res = make_response(object=serialize_clean(ccfinstancedetails_new), status=201) return res \ No newline at end of file diff --git a/services/helper/helper_service/services/interconnection/util.py b/services/helper/helper_service/services/interconnection/util.py index 6b11bb9d..005fd9c8 100644 --- a/services/helper/helper_service/services/interconnection/util.py +++ b/services/helper/helper_service/services/interconnection/util.py @@ -12,6 +12,13 @@ def serialize_clean_camel_case(obj): return res +def serialize_clean(obj): + res = obj.to_dict() + res = clean_empty(res) + + return res + + def clean_n_camel_case(res): res = clean_empty(res) res = dict_to_camel_case(res) -- GitLab From 632f0fc0684ec5ffb98032eec99fc1c231d9925a Mon Sep 17 00:00:00 2001 From: Stavros-Anastasios Charismiadis Date: Wed, 17 Dec 2025 15:22:47 +0200 Subject: [PATCH 07/10] Check if domain already interconnected --- .../interconnection/core/capifdomaindetails.py | 18 ++++++++---------- .../interconnection/core/ccfinstancedetails.py | 14 +++++++------- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/services/helper/helper_service/services/interconnection/core/capifdomaindetails.py b/services/helper/helper_service/services/interconnection/core/capifdomaindetails.py index ad25f0f4..f16db15b 100644 --- a/services/helper/helper_service/services/interconnection/core/capifdomaindetails.py +++ b/services/helper/helper_service/services/interconnection/core/capifdomaindetails.py @@ -42,6 +42,12 @@ class CapifDomainOperations(Resource): current_app.logger.debug("Interconnection: Add domain") current_app.logger.debug(capifdomaindetails) + interconnected_col = self.db.get_col_by_name(self.db.interconnected) + interconnected_ccf = interconnected_col.find_one({"dst_prov_dom": capifdomaindetails.dst_prov_dom}) + if interconnected_ccf: + current_app.logger.debug("CAPIF domain already interconnected : {}".format(capifdomaindetails.dst_prov_dom)) + return make_response("CAPIF domain already interconnected", 409) + url = "https://{}/helper/interconnection/connect".format(capifdomaindetails.dst_prov_dom) with open('server_certs/ca.crt', 'rb') as ca_cert: @@ -54,15 +60,12 @@ class CapifDomainOperations(Resource): config_col = self.db.get_col_by_name(self.db.capif_configuration) config = config_col.find_one({}, {"_id": 0}) - # current_app.logger.debug(config) - # current_app.logger.debug(config['ccf_id']) payload = json.dumps({ "caRoot": server_ca.decode("utf-8"), "ccfId": config['ccf_id'], - "dstProvDom": capifdomaindetails.dst_prov_dom, - "publicKey": server_pub.decode("utf-8"), - "srcProvDom": os.getenv("CAPIF_HOSTNAME") + "dstProvDom": os.getenv("CAPIF_HOSTNAME"), + "publicKey": server_pub.decode("utf-8") }) headers = { 'accept': 'application/json', @@ -77,11 +80,6 @@ class CapifDomainOperations(Resource): current_app.logger.debug(inter_ccf) if response.status_code == 201: - interconnected_col = self.db.get_col_by_name(self.db.interconnected) - interconnected_ccf = interconnected_col.find_one({"ccf_id": inter_ccf['ccf_id']}) - if interconnected_ccf: - current_app.logger.debug("CCF already interconnected : {}".format(inter_ccf['ccf_id'])) - return make_response("CCF already interconnected", 409) interconnected_col.insert_one(inter_ccf) ccfinstancedetails_new = CcfInstanceDetails().from_dict(dict_to_camel_case(inter_ccf)) diff --git a/services/helper/helper_service/services/interconnection/core/ccfinstancedetails.py b/services/helper/helper_service/services/interconnection/core/ccfinstancedetails.py index 26f27aaf..6c2987dc 100644 --- a/services/helper/helper_service/services/interconnection/core/ccfinstancedetails.py +++ b/services/helper/helper_service/services/interconnection/core/ccfinstancedetails.py @@ -39,6 +39,12 @@ class CcfInstanceOperations(Resource): current_app.logger.debug("Interconnection: Add instance") current_app.logger.debug(ccfinstancedetails) + interconnected_col = self.db.get_col_by_name(self.db.interconnected) + interconnected_ccf = interconnected_col.find_one({"ccf_id": ccfinstancedetails.ccf_id}) + if interconnected_ccf: + current_app.logger.debug("CCF already interconnected : {}".format(ccfinstancedetails.ccf_id)) + return make_response("CCF already interconnected", 409) + with open('server_certs/ca.crt', 'rb') as ca_cert: server_ca = ca_cert.read() ca_cert.close() @@ -50,19 +56,13 @@ class CcfInstanceOperations(Resource): config_col = self.db.get_col_by_name(self.db.capif_configuration) config = config_col.find_one({}, {"_id": 0}) - interconnected_col = self.db.get_col_by_name(self.db.interconnected) - interconnected_ccf = interconnected_col.find_one({"ccf_id": ccfinstancedetails.ccf_id}) - if interconnected_ccf: - current_app.logger.debug("CCF already interconnected : {}".format(ccfinstancedetails.ccf_id)) - return make_response("CCF already interconnected", 409) - ccfinstancedetails_dict = ccfinstancedetails.to_dict() interconnected_col.insert_one(ccfinstancedetails_dict) ccfinstancedetails_dict['ca_root'] = server_ca.decode("utf-8") ccfinstancedetails_dict['public_key'] = server_pub.decode("utf-8") ccfinstancedetails_dict['ccf_id'] = config['ccf_id'] - ccfinstancedetails_dict['dst_prov_dom'], ccfinstancedetails_dict['src_prov_dom'] = ccfinstancedetails_dict['src_prov_dom'], ccfinstancedetails_dict['dst_prov_dom'] + ccfinstancedetails_dict['dst_prov_dom'] = os.getenv("CAPIF_HOSTNAME") current_app.logger.debug(ccfinstancedetails_dict) ccfinstancedetails_new = CcfInstanceDetails().from_dict(dict_to_camel_case(ccfinstancedetails_dict)) -- GitLab From f8dda545308d6cad5f030587d48037e8cb8bf55e Mon Sep 17 00:00:00 2001 From: Stavros-Anastasios Charismiadis Date: Wed, 17 Dec 2025 15:58:20 +0200 Subject: [PATCH 08/10] Remove source provider domain field --- .../openapi_helper_interconnection.yaml | 3 -- .../core/capifdomaindetails.py | 1 - .../models/ccf_instance_details.py | 30 +------------------ .../interconnection/openapi/openapi.yaml | 5 ---- 4 files changed, 1 insertion(+), 38 deletions(-) diff --git a/services/helper/helper_service/openapi_helper_interconnection.yaml b/services/helper/helper_service/openapi_helper_interconnection.yaml index 962e642a..d038ec15 100644 --- a/services/helper/helper_service/openapi_helper_interconnection.yaml +++ b/services/helper/helper_service/openapi_helper_interconnection.yaml @@ -136,7 +136,6 @@ components: required: - caRoot - publicKey - - srcProvDom - ccfId - dstProvDom properties: @@ -144,8 +143,6 @@ components: type: string publicKey: type: string - srcProvDom: - type: string ccfId: type: string dstProvDom: diff --git a/services/helper/helper_service/services/interconnection/core/capifdomaindetails.py b/services/helper/helper_service/services/interconnection/core/capifdomaindetails.py index f16db15b..62813de4 100644 --- a/services/helper/helper_service/services/interconnection/core/capifdomaindetails.py +++ b/services/helper/helper_service/services/interconnection/core/capifdomaindetails.py @@ -28,7 +28,6 @@ publisher_ops = Publisher() service_api_not_found_message = "Service API not found" -srcProvDom = "ccfB.fogus.org" class CapifDomainOperations(Resource): diff --git a/services/helper/helper_service/services/interconnection/models/ccf_instance_details.py b/services/helper/helper_service/services/interconnection/models/ccf_instance_details.py index 0ce33019..fa23a751 100644 --- a/services/helper/helper_service/services/interconnection/models/ccf_instance_details.py +++ b/services/helper/helper_service/services/interconnection/models/ccf_instance_details.py @@ -12,15 +12,13 @@ class CcfInstanceDetails(Model): Do not edit the class manually. """ - def __init__(self, ca_root=None, public_key=None, src_prov_dom=None, ccf_id=None, dst_prov_dom=None): # noqa: E501 + def __init__(self, ca_root=None, public_key=None, ccf_id=None, dst_prov_dom=None): # noqa: E501 """CcfInstanceDetails - a model defined in OpenAPI :param ca_root: The ca_root of this CcfInstanceDetails. # noqa: E501 :type ca_root: str :param public_key: The public_key of this CcfInstanceDetails. # noqa: E501 :type public_key: str - :param src_prov_dom: The src_prov_dom of this CcfInstanceDetails. # noqa: E501 - :type src_prov_dom: str :param ccf_id: The ccf_id of this CcfInstanceDetails. # noqa: E501 :type ccf_id: str :param dst_prov_dom: The dst_prov_dom of this CcfInstanceDetails. # noqa: E501 @@ -29,7 +27,6 @@ class CcfInstanceDetails(Model): self.openapi_types = { 'ca_root': str, 'public_key': str, - 'src_prov_dom': str, 'ccf_id': str, 'dst_prov_dom': str } @@ -37,14 +34,12 @@ class CcfInstanceDetails(Model): self.attribute_map = { 'ca_root': 'caRoot', 'public_key': 'publicKey', - 'src_prov_dom': 'srcProvDom', 'ccf_id': 'ccfId', 'dst_prov_dom': 'dstProvDom' } self._ca_root = ca_root self._public_key = public_key - self._src_prov_dom = src_prov_dom self._ccf_id = ccf_id self._dst_prov_dom = dst_prov_dom @@ -105,29 +100,6 @@ class CcfInstanceDetails(Model): self._public_key = public_key - @property - def src_prov_dom(self) -> str: - """Gets the src_prov_dom of this CcfInstanceDetails. - - - :return: The src_prov_dom of this CcfInstanceDetails. - :rtype: str - """ - return self._src_prov_dom - - @src_prov_dom.setter - def src_prov_dom(self, src_prov_dom: str): - """Sets the src_prov_dom of this CcfInstanceDetails. - - - :param src_prov_dom: The src_prov_dom of this CcfInstanceDetails. - :type src_prov_dom: str - """ - if src_prov_dom is None: - raise ValueError("Invalid value for `src_prov_dom`, must not be `None`") # noqa: E501 - - self._src_prov_dom = src_prov_dom - @property def ccf_id(self) -> str: """Gets the ccf_id of this CcfInstanceDetails. diff --git a/services/helper/helper_service/services/interconnection/openapi/openapi.yaml b/services/helper/helper_service/services/interconnection/openapi/openapi.yaml index 7b52c88e..41dbf42c 100644 --- a/services/helper/helper_service/services/interconnection/openapi/openapi.yaml +++ b/services/helper/helper_service/services/interconnection/openapi/openapi.yaml @@ -140,7 +140,6 @@ components: ccfId: ccfId dstProvDom: dstProvDom publicKey: publicKey - srcProvDom: srcProvDom caRoot: caRoot properties: caRoot: @@ -149,9 +148,6 @@ components: publicKey: title: publicKey type: string - srcProvDom: - title: srcProvDom - type: string ccfId: title: ccfId type: string @@ -163,7 +159,6 @@ components: - ccfId - dstProvDom - publicKey - - srcProvDom title: CcfInstanceDetails type: object ProblemDetails: -- GitLab From 2e62893d3396eb9970c04388fdb498a49329c0cc Mon Sep 17 00:00:00 2001 From: Stavros-Anastasios Charismiadis Date: Thu, 8 Jan 2026 15:59:29 +0200 Subject: [PATCH 09/10] writing fix --- services/show_logs.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/show_logs.sh b/services/show_logs.sh index 7a4c807b..d71ae680 100755 --- a/services/show_logs.sh +++ b/services/show_logs.sh @@ -34,7 +34,7 @@ FOLLOW="" DUID=$(id -u) DGID=$(id -g) -# Read params +# Read parameters while getopts "cvrahmfs" opt; do case $opt in c) -- GitLab From 77f71dfd02c47288260eede83f7e50ca6199561c Mon Sep 17 00:00:00 2001 From: Stavros-Anastasios Charismiadis Date: Thu, 12 Feb 2026 17:19:01 +0200 Subject: [PATCH 10/10] Manage two-way interconnection (each CCF needed its own crt and key on nginx) --- services/helper/helper_service/app.py | 5 +- services/nginx/nginx_prepare.sh | 165 ++++++++++++++++---------- services/run.sh | 8 +- 3 files changed, 113 insertions(+), 65 deletions(-) diff --git a/services/helper/helper_service/app.py b/services/helper/helper_service/app.py index 88d918b9..e4f9ed4f 100644 --- a/services/helper/helper_service/app.py +++ b/services/helper/helper_service/app.py @@ -46,6 +46,7 @@ req.get_subject().OU = 'helper' req.get_subject().L = 'Madrid' req.get_subject().ST = 'Madrid' req.get_subject().C = 'ES' +# req.get_subject().CN = "superadmin{}".format(os.getenv("CAPIF_HOSTNAME")) req.get_subject().emailAddress = 'helper@tid.es' req.set_pubkey(key) req.sign(key, 'sha256') @@ -66,7 +67,9 @@ data = { 'format':'pem_bundle', 'ttl': ttl_superadmin_cert, 'csr': csr_request, - 'common_name': "superadmin" + # 'common_name': "superadmin{}".format(os.getenv("CAPIF_HOSTNAME")), + 'common_name': "superadmin", + 'alt_names': "{}".format(os.getenv("CAPIF_HOSTNAME")) } response = requests.request("POST", url, headers=headers, data=data, verify = config["ca_factory"].get("verify", False)) diff --git a/services/nginx/nginx_prepare.sh b/services/nginx/nginx_prepare.sh index 91884863..e38770b9 100644 --- a/services/nginx/nginx_prepare.sh +++ b/services/nginx/nginx_prepare.sh @@ -44,69 +44,114 @@ if [ "$SUCCES_OPERATION" = false ]; then exit 1 # Exit with failure fi -# Setup inital value to ATTEMPT and SUCCESS_OPERATION -ATTEMPT=0 -SUCCES_OPERATION=false - -while [ $ATTEMPT -lt $MAX_RETRIES ]; do - # Increment ATTEMPT using eval - eval "ATTEMPT=\$((ATTEMPT + 1))" - echo "Attempt $ATTEMPT of $MAX_RETRIES" - - # Make the request to Vault and store the response in a variable - RESPONSE=$(curl -s -k --connect-timeout 5 --max-time 10 \ - --header "X-Vault-Token: $VAULT_TOKEN" \ - --request GET "$VAULT_ADDR/v1/secret/data/server_cert" | jq -r '.data.data.cert') - - echo "$RESPONSE" - # Check if the response is "null" or empty - if [ -n "$RESPONSE" ] && [ "$RESPONSE" != "null" ]; then - echo "$RESPONSE" > $CERTS_FOLDER/server.crt - echo "Server Certificate successfully saved." - ATTEMPT=0 - SUCCES_OPERATION=true - break - else - echo "Invalid response ('null' or empty), retrying in $RETRY_DELAY seconds..." - sleep $RETRY_DELAY - fi -done - -if [ "$SUCCES_OPERATION" = false ]; then - echo "Error: Failed to retrieve a valid response after $MAX_RETRIES attempts." - exit 1 # Exit with failure +if [ "$VAULT_HOSTNAME" != "vault" ] ; then + # Setup variables (Replace these or ensure they are in your environment) + TTL="8760h" + + echo "--- Generating Private Key and CSR ---" + # Generate Private Key + openssl genrsa -out "$CERTS_FOLDER/server.key" 2048 + + # Generate CSR + # Note: We pass the Subject information here. + # OpenSSL's -subj format: /C=/ST=/L=/O=/OU=/CN=/emailAddress= + SUBJ="/C=ES/ST=Madrid/L=Madrid/O=OCF helper/OU=helper/CN=${CAPIF_HOSTNAME}/emailAddress=helper@tid.es" + + openssl req -new \ + -key "$CERTS_FOLDER/server.key" \ + -out "$CERTS_FOLDER/server.csr" \ + -subj "$SUBJ" + + echo "--- Requesting Certificate from Vault ---" + + # Prepare the CSR (Safe for JSON) + ESC_CSR=$(awk '{printf "%s\\n", $0}' "$CERTS_FOLDER/server.csr") + + # Request and Extract Certificate in one pipeline + # This prevents the "parse error" by passing the raw stream to jq + curl -s --request POST \ + --header "X-Vault-Token: $VAULT_TOKEN" \ + --header "Content-Type: application/json" \ + --data "{ + \"format\": \"pem_bundle\", + \"ttl\": \"$TTL\", + \"csr\": \"$ESC_CSR\", + \"common_name\": \"superadmin${CAPIF_HOSTNAME}\", + \"alt_names\": \"${CAPIF_HOSTNAME}\" + }" \ + "$VAULT_ADDR/v1/pki_int/sign/my-ca" | jq -r '.data.certificate' > "$CERTS_FOLDER/server.crt" + + # Validation Check + if [ ! -s "$CERTS_FOLDER/server.crt" ] || [ "$(cat $CERTS_FOLDER/server.crt)" == "null" ]; then + echo "ERROR: Failed to generate server.crt. Check Vault Token and Connectivity." + exit 1 + fi +else + # Setup initial value to ATTEMPT and SUCCESS_OPERATION + ATTEMPT=0 + SUCCES_OPERATION=false + + while [ $ATTEMPT -lt $MAX_RETRIES ]; do + # Increment ATTEMPT using eval + eval "ATTEMPT=\$((ATTEMPT + 1))" + echo "Attempt $ATTEMPT of $MAX_RETRIES" + + # Make the request to Vault and store the response in a variable + RESPONSE=$(curl -s -k --connect-timeout 5 --max-time 10 \ + --header "X-Vault-Token: $VAULT_TOKEN" \ + --request GET "$VAULT_ADDR/v1/secret/data/server_cert" | jq -r '.data.data.cert') + + echo "$RESPONSE" + + # Check if the response is "null" or empty + if [ -n "$RESPONSE" ] && [ "$RESPONSE" != "null" ]; then + echo "$RESPONSE" > $CERTS_FOLDER/server.crt + echo "Server Certificate successfully saved." + ATTEMPT=0 + SUCCES_OPERATION=true + break + else + echo "Invalid response ('null' or empty), retrying in $RETRY_DELAY seconds..." + sleep $RETRY_DELAY + fi + done + + if [ "$SUCCES_OPERATION" = false ]; then + echo "Error: Failed to retrieve a valid response after $MAX_RETRIES attempts." + exit 1 # Exit with failure + fi + + # Setup inital value to ATTEMPT and SUCCESS_OPERATION + ATTEMPT=0 + SUCCES_OPERATION=false + + while [ $ATTEMPT -lt $MAX_RETRIES ]; do + # Increment ATTEMPT using eval + eval "ATTEMPT=\$((ATTEMPT + 1))" + echo "Attempt $ATTEMPT of $MAX_RETRIES" + + # Make the request to Vault and store the response in a variable + RESPONSE=$(curl -s -k --connect-timeout 5 --max-time 10 \ + --header "X-Vault-Token: $VAULT_TOKEN" \ + --request GET "$VAULT_ADDR/v1/secret/data/server_cert/private" | jq -r '.data.data.key') + + echo "$RESPONSE" + + # Check if the response is "null" or empty + if [ -n "$RESPONSE" ] && [ "$RESPONSE" != "null" ]; then + echo "$RESPONSE" > $CERTS_FOLDER/server.key + echo "Server Key successfully saved." + ATTEMPT=0 + SUCCES_OPERATION=true + break + else + echo "Invalid response ('null' or empty), retrying in $RETRY_DELAY seconds..." + sleep $RETRY_DELAY + fi + done fi -# Setup inital value to ATTEMPT and SUCCESS_OPERATION -ATTEMPT=0 -SUCCES_OPERATION=false - -while [ $ATTEMPT -lt $MAX_RETRIES ]; do - # Increment ATTEMPT using eval - eval "ATTEMPT=\$((ATTEMPT + 1))" - echo "Attempt $ATTEMPT of $MAX_RETRIES" - - # Make the request to Vault and store the response in a variable - RESPONSE=$(curl -s -k --connect-timeout 5 --max-time 10 \ - --header "X-Vault-Token: $VAULT_TOKEN" \ - --request GET "$VAULT_ADDR/v1/secret/data/server_cert/private" | jq -r '.data.data.key') - - echo "$RESPONSE" - - # Check if the response is "null" or empty - if [ -n "$RESPONSE" ] && [ "$RESPONSE" != "null" ]; then - echo "$RESPONSE" > $CERTS_FOLDER/server.key - echo "Server Key successfully saved." - ATTEMPT=0 - SUCCES_OPERATION=true - break - else - echo "Invalid response ('null' or empty), retrying in $RETRY_DELAY seconds..." - sleep $RETRY_DELAY - fi -done - if [ "$SUCCES_OPERATION" = false ]; then echo "Error: Failed to retrieve a valid response after $MAX_RETRIES attempts." exit 1 # Exit with failure diff --git a/services/run.sh b/services/run.sh index d82f6b25..68710509 100755 --- a/services/run.sh +++ b/services/run.sh @@ -22,11 +22,11 @@ help() { docker_version=$(docker compose version --short | cut -d',' -f1) IFS='.' read -ra version_components <<< "$docker_version" -if [ "${version_components[0]}" -ge 2 ] && [ "${version_components[1]}" -ge 10 ]; then - echo "Docker compose version it greater than 2.10" +if [ "${version_components[0]}" -gt 2 ] || { [ "${version_components[0]}" -eq 2 ] && [ "${version_components[1]}" -ge 10 ]; }; then + echo "Docker compose version is greater than or equal to 2.10" else - echo "Docker compose version is not valid. Should be greater than 2.10" - exit 1 + echo "Docker compose version is not valid. Should be >= 2.10" + exit 1 fi # Check if yq is installed -- GitLab