From 90ef9c7d392bd9b961f7d4bc8f01452909e2c0b3 Mon Sep 17 00:00:00 2001 From: Stavros-Anastasios Charismiadis Date: Thu, 16 Jul 2026 14:28:54 +0300 Subject: [PATCH 01/10] Change name of interconnection APIs, add templates for sync and delete --- .../openapi_helper_interconnection.yaml | 165 ++++++++++++++++- .../controllers/default_controller.py | 51 +++++- .../core/capifdomaindetails.py | 10 +- .../core/ccfinstancedetails.py | 6 +- ...CF_Interconnection.postman_collection.json | 50 +++--- .../interconnection/openapi/openapi.yaml | 168 +++++++++++++++++- services/nginx/endpoints/endpoints.conf | 5 +- services/nginx/policies/helper-mtls.conf | 10 +- 8 files changed, 425 insertions(+), 40 deletions(-) diff --git a/services/helper/helper_service/openapi_helper_interconnection.yaml b/services/helper/helper_service/openapi_helper_interconnection.yaml index d038ec1..be61376 100644 --- a/services/helper/helper_service/openapi_helper_interconnection.yaml +++ b/services/helper/helper_service/openapi_helper_interconnection.yaml @@ -11,7 +11,7 @@ servers: description: Base URL of the Helper service. # 1. PATHS: Where you define your endpoints paths: - /interconnect: + /request: post: summary: Send a new interconnection request operationId: interconnect_request # <--- Becomes 'def interconnect_request(body):' in Python @@ -66,10 +66,117 @@ paths: description: Service Unavailable default: description: Generic Error - /connect: + /sync: + post: + summary: Send an interconnection sync request from one CCF to another + operationId: interconnect_sync # <--- Becomes 'def interconnect_sync(body):' in Python + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CapifDomainDetails' + responses: + "201": + description: Interconnection sync 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 + /request/{ccf_id}: + delete: + summary: Remove an interconnection request + operationId: delete_interconnect_request # <--- Becomes 'def delete_interconnect_request(ccf_id):' in Python + parameters: + - description: CCF identifier of the interconnection request to remove + in: path + name: ccf_id + required: true + schema: + type: string + responses: + "204": + description: Interconnection request removed + "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 + /establish: post: summary: Create a new interconnection - operationId: connect_ccfs # <--- Becomes 'def connect_ccfs(body):' in Python + operationId: connect_ccf requestBody: required: true content: @@ -121,6 +228,58 @@ paths: description: Service Unavailable default: description: Generic Error + /establish/{ccf_id}: + delete: + summary: Remove an established interconnection + operationId: delete_connected_ccf + parameters: + - description: CCF identifier of the interconnection to remove + in: path + name: ccf_id + required: true + schema: + type: string + responses: + "204": + description: Interconnection removed + "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: diff --git a/services/helper/helper_service/services/interconnection/controllers/default_controller.py b/services/helper/helper_service/services/interconnection/controllers/default_controller.py index 9a2d6d8..43f1da9 100644 --- a/services/helper/helper_service/services/interconnection/controllers/default_controller.py +++ b/services/helper/helper_service/services/interconnection/controllers/default_controller.py @@ -14,7 +14,7 @@ ccf_operations = CcfInstanceOperations() capif_domain_operations = CapifDomainOperations() -def connect_ccfs(body): # noqa: E501 +def connect_ccf(body): # noqa: E501 """Create a new interconnection # noqa: E501 @@ -33,6 +33,21 @@ def connect_ccfs(body): # noqa: E501 return res +def delete_connected_ccf(ccf_id): # noqa: E501 + """Remove an established interconnection + + # noqa: E501 + + :param ccf_id: CCF identifier of the interconnection to remove + :type ccf_id: str + + :rtype: Union[None, Tuple[None, int], Tuple[None, int, Dict[str, str]]] + """ + res = ccf_operations.delete_ccfinstance(ccf_id) + + return res + + def interconnect_request(body): # noqa: E501 """Send a new interconnection request @@ -50,3 +65,37 @@ def interconnect_request(body): # noqa: E501 res = capif_domain_operations.add_capifdomain(capif_domain_details) return res + + +def interconnect_sync(body): # noqa: E501 + """Send an interconnection sync request from one CCF to another + + # 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.sync_capifdomain(capif_domain_details) + + return res + + +def delete_interconnect_request(ccf_id): # noqa: E501 + """Remove an interconnection request + + # noqa: E501 + + :param ccf_id: CCF identifier of the interconnection request to remove + :type ccf_id: str + + :rtype: Union[None, Tuple[None, int], Tuple[None, int, Dict[str, str]]] + """ + res = capif_domain_operations.delete_capifdomain(ccf_id) + + return res diff --git a/services/helper/helper_service/services/interconnection/core/capifdomaindetails.py b/services/helper/helper_service/services/interconnection/core/capifdomaindetails.py index 08263f5..8ff2d59 100644 --- a/services/helper/helper_service/services/interconnection/core/capifdomaindetails.py +++ b/services/helper/helper_service/services/interconnection/core/capifdomaindetails.py @@ -47,7 +47,7 @@ class CapifDomainOperations(Resource): 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) + url = "https://{}/helper/interconnection/establish".format(capifdomaindetails.dst_prov_dom) with open('certs/ca.crt', 'rb') as ca_cert: server_ca = ca_cert.read() @@ -101,3 +101,11 @@ class CapifDomainOperations(Resource): return Response(json.dumps(inter_ccf), status=response.status_code, mimetype=remote_mimetype) return Response(response.text, status=response.status_code, mimetype=remote_mimetype) + + def sync_capifdomain(self, capifdomaindetails): + """Sync interconnection state with a peer CCF.""" + pass + + def delete_capifdomain(self, ccf_id): + """Remove an interconnection request for a CCF.""" + pass diff --git a/services/helper/helper_service/services/interconnection/core/ccfinstancedetails.py b/services/helper/helper_service/services/interconnection/core/ccfinstancedetails.py index 3013003..b989675 100644 --- a/services/helper/helper_service/services/interconnection/core/ccfinstancedetails.py +++ b/services/helper/helper_service/services/interconnection/core/ccfinstancedetails.py @@ -73,4 +73,8 @@ class CcfInstanceOperations(Resource): ccfinstancedetails_new = CcfInstanceDetails().from_dict(dict_to_camel_case(ccfinstancedetails_dict)) current_app.logger.debug(ccfinstancedetails_new) res = make_response(object=serialize_clean(ccfinstancedetails_new), status=status) - return res \ No newline at end of file + return res + + def delete_ccfinstance(self, ccf_id): + """Remove an established interconnection with a peer CCF.""" + pass \ No newline at end of file diff --git a/services/helper/helper_service/services/interconnection/openapi/CCF_Interconnection.postman_collection.json b/services/helper/helper_service/services/interconnection/openapi/CCF_Interconnection.postman_collection.json index 4845c9b..828bf67 100644 --- a/services/helper/helper_service/services/interconnection/openapi/CCF_Interconnection.postman_collection.json +++ b/services/helper/helper_service/services/interconnection/openapi/CCF_Interconnection.postman_collection.json @@ -7,7 +7,7 @@ }, "item": [ { - "name": "Create Interconnection - POST /connect", + "name": "Create Interconnection - POST /establish", "request": { "method": "POST", "header": [ @@ -21,12 +21,12 @@ "raw": "{\n \"caRoot\": \"-----BEGIN CERTIFICATE-----...-----END CERTIFICATE-----\",\n \"publicKey\": \"-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----\",\n \"ccfId\": \"ccf-001\",\n \"dstProvDom\": \"provider-b.example.com\"\n}" }, "url": { - "raw": "{{baseUrl}}/connect", + "raw": "{{baseUrl}}/establish", "host": [ "{{baseUrl}}" ], "path": [ - "connect" + "establish" ] }, "description": "Create a new interconnection between CCFs." @@ -47,12 +47,12 @@ "raw": "{\n \"caRoot\": \"-----BEGIN CERTIFICATE-----...-----END CERTIFICATE-----\",\n \"publicKey\": \"-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----\",\n \"ccfId\": \"ccf-001\",\n \"dstProvDom\": \"provider-b.example.com\"\n}" }, "url": { - "raw": "{{baseUrl}}/connect", + "raw": "{{baseUrl}}/establish", "host": [ "{{baseUrl}}" ], "path": [ - "connect" + "establish" ] } }, @@ -78,12 +78,12 @@ } ], "url": { - "raw": "{{baseUrl}}/connect", + "raw": "{{baseUrl}}/establish", "host": [ "{{baseUrl}}" ], "path": [ - "connect" + "establish" ] } }, @@ -96,7 +96,7 @@ "value": "application/problem+json" } ], - "body": "{\n \"type\": \"https://example.com/problems/invalid-request\",\n \"title\": \"Bad Request\",\n \"status\": 400,\n \"detail\": \"Request body contains invalid or missing parameters.\",\n \"instance\": \"/interconnection/connect\",\n \"cause\": \"MANDATORY_IE_MISSING\",\n \"invalidParams\": [\n {\n \"param\": \"/ccfId\",\n \"reason\": \"is required\"\n },\n {\n \"param\": \"/publicKey\",\n \"reason\": \"must be a valid PEM public key\"\n }\n ],\n \"supportedFeatures\": \"1F\"\n}" + "body": "{\n \"type\": \"https://example.com/problems/invalid-request\",\n \"title\": \"Bad Request\",\n \"status\": 400,\n \"detail\": \"Request body contains invalid or missing parameters.\",\n \"instance\": \"/interconnection/establish\",\n \"cause\": \"MANDATORY_IE_MISSING\",\n \"invalidParams\": [\n {\n \"param\": \"/ccfId\",\n \"reason\": \"is required\"\n },\n {\n \"param\": \"/publicKey\",\n \"reason\": \"must be a valid PEM public key\"\n }\n ],\n \"supportedFeatures\": \"1F\"\n}" }, { "name": "401 Unauthorized", @@ -109,12 +109,12 @@ } ], "url": { - "raw": "{{baseUrl}}/connect", + "raw": "{{baseUrl}}/establish", "host": [ "{{baseUrl}}" ], "path": [ - "connect" + "establish" ] } }, @@ -127,7 +127,7 @@ "value": "application/problem+json" } ], - "body": "{\n \"type\": \"https://example.com/problems/unauthorized\",\n \"title\": \"Unauthorized\",\n \"status\": 401,\n \"detail\": \"Missing or invalid authentication credentials.\",\n \"instance\": \"/interconnection/connect\",\n \"cause\": \"INVALID_TOKEN\"\n}" + "body": "{\n \"type\": \"https://example.com/problems/unauthorized\",\n \"title\": \"Unauthorized\",\n \"status\": 401,\n \"detail\": \"Missing or invalid authentication credentials.\",\n \"instance\": \"/interconnection/establish\",\n \"cause\": \"INVALID_TOKEN\"\n}" }, { "name": "403 Forbidden", @@ -140,12 +140,12 @@ } ], "url": { - "raw": "{{baseUrl}}/connect", + "raw": "{{baseUrl}}/establish", "host": [ "{{baseUrl}}" ], "path": [ - "connect" + "establish" ] } }, @@ -158,7 +158,7 @@ "value": "application/problem+json" } ], - "body": "{\n \"type\": \"https://example.com/problems/forbidden\",\n \"title\": \"Forbidden\",\n \"status\": 403,\n \"detail\": \"The client is not allowed to perform this operation.\",\n \"instance\": \"/interconnection/connect\",\n \"cause\": \"INSUFFICIENT_SCOPE\"\n}" + "body": "{\n \"type\": \"https://example.com/problems/forbidden\",\n \"title\": \"Forbidden\",\n \"status\": 403,\n \"detail\": \"The client is not allowed to perform this operation.\",\n \"instance\": \"/interconnection/establish\",\n \"cause\": \"INSUFFICIENT_SCOPE\"\n}" }, { "name": "404 Not Found", @@ -171,12 +171,12 @@ } ], "url": { - "raw": "{{baseUrl}}/connect", + "raw": "{{baseUrl}}/establish", "host": [ "{{baseUrl}}" ], "path": [ - "connect" + "establish" ] } }, @@ -189,12 +189,12 @@ "value": "application/problem+json" } ], - "body": "{\n \"type\": \"https://example.com/problems/not-found\",\n \"title\": \"Not Found\",\n \"status\": 404,\n \"detail\": \"The target resource was not found.\",\n \"instance\": \"/interconnection/connect\",\n \"cause\": \"RESOURCE_NOT_FOUND\"\n}" + "body": "{\n \"type\": \"https://example.com/problems/not-found\",\n \"title\": \"Not Found\",\n \"status\": 404,\n \"detail\": \"The target resource was not found.\",\n \"instance\": \"/interconnection/establish\",\n \"cause\": \"RESOURCE_NOT_FOUND\"\n}" } ] }, { - "name": "Interconnect Request - POST /interconnect", + "name": "Interconnection Request - POST /request", "request": { "method": "POST", "header": [ @@ -208,12 +208,12 @@ "raw": "{\n \"dstProvDom\": \"provider-b.example.com\"\n}" }, "url": { - "raw": "{{baseUrl}}/interconnect", + "raw": "{{baseUrl}}/request", "host": [ "{{baseUrl}}" ], "path": [ - "interconnect" + "request" ] }, "description": "Send a new interconnection request to another CAPIF domain." @@ -234,12 +234,12 @@ "raw": "{\n \"dstProvDom\": \"provider-b.example.com\"\n}" }, "url": { - "raw": "{{baseUrl}}/interconnect", + "raw": "{{baseUrl}}/request", "host": [ "{{baseUrl}}" ], "path": [ - "interconnect" + "request" ] } }, @@ -265,12 +265,12 @@ } ], "url": { - "raw": "{{baseUrl}}/interconnect", + "raw": "{{baseUrl}}/request", "host": [ "{{baseUrl}}" ], "path": [ - "interconnect" + "request" ] } }, @@ -283,7 +283,7 @@ "value": "application/problem+json" } ], - "body": "{\n \"type\": \"https://example.com/problems/invalid-request\",\n \"title\": \"Bad Request\",\n \"status\": 400,\n \"detail\": \"dstProvDom is missing or invalid.\",\n \"instance\": \"/interconnection/interconnect\",\n \"cause\": \"MANDATORY_IE_MISSING\",\n \"invalidParams\": [\n {\n \"param\": \"/dstProvDom\",\n \"reason\": \"is required\"\n }\n ]\n}" + "body": "{\n \"type\": \"https://example.com/problems/invalid-request\",\n \"title\": \"Bad Request\",\n \"status\": 400,\n \"detail\": \"dstProvDom is missing or invalid.\",\n \"instance\": \"/interconnection/request\",\n \"cause\": \"MANDATORY_IE_MISSING\",\n \"invalidParams\": [\n {\n \"param\": \"/dstProvDom\",\n \"reason\": \"is required\"\n }\n ]\n}" } ] } diff --git a/services/helper/helper_service/services/interconnection/openapi/openapi.yaml b/services/helper/helper_service/services/interconnection/openapi/openapi.yaml index 41dbf42..f53aef3 100644 --- a/services/helper/helper_service/services/interconnection/openapi/openapi.yaml +++ b/services/helper/helper_service/services/interconnection/openapi/openapi.yaml @@ -10,9 +10,9 @@ servers: default: http://localhost:8080 description: Base URL of the Helper service. paths: - /connect: + /establish: post: - operationId: connect_ccfs + operationId: connect_ccf requestBody: content: application/json: @@ -66,7 +66,60 @@ paths: description: Generic Error summary: Create a new interconnection x-openapi-router-controller: interconnection.controllers.default_controller - /interconnect: + /establish/{ccf_id}: + delete: + operationId: delete_connected_ccf + parameters: + - description: CCF identifier of the interconnection to remove + in: path + name: ccf_id + required: true + schema: + type: string + responses: + "204": + description: Interconnection removed + "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: Remove an established interconnection + x-openapi-router-controller: interconnection.controllers.default_controller + /request: post: operationId: interconnect_request requestBody: @@ -122,6 +175,115 @@ paths: description: Generic Error summary: Send a new interconnection request x-openapi-router-controller: interconnection.controllers.default_controller + /sync: + post: + operationId: interconnect_sync + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/CapifDomainDetails" + required: true + responses: + "201": + content: + application/json: + schema: + $ref: "#/components/schemas/CapifDomainDetails" + description: Interconnection sync 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 an interconnection sync request from one CCF to another + x-openapi-router-controller: interconnection.controllers.default_controller + /request/{ccf_id}: + delete: + operationId: delete_interconnect_request + parameters: + - description: CCF identifier of the interconnection request to remove + in: path + name: ccf_id + required: true + schema: + type: string + responses: + "204": + description: Interconnection request removed + "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: Remove an interconnection request + x-openapi-router-controller: interconnection.controllers.default_controller components: schemas: CapifDomainDetails: diff --git a/services/nginx/endpoints/endpoints.conf b/services/nginx/endpoints/endpoints.conf index ec6e78c..c225b46 100644 --- a/services/nginx/endpoints/endpoints.conf +++ b/services/nginx/endpoints/endpoints.conf @@ -9,8 +9,9 @@ map $uri $endpoint { # Regex matches for endpoints that can be grouped by common patterns # Helper related endpoints - ~^/helper/interconnection/interconnect helper_interconnect; - ~^/helper/interconnection/connect helper_connect; + ~^/helper/interconnection/request helper_request; + ~^/helper/interconnection/sync helper_sync; + ~^/helper/interconnection/establish helper_establish; ~^/helper/ helper_base_tree; # Invoker management related endpoints diff --git a/services/nginx/policies/helper-mtls.conf b/services/nginx/policies/helper-mtls.conf index 98a5c56..af0cd53 100644 --- a/services/nginx/policies/helper-mtls.conf +++ b/services/nginx/policies/helper-mtls.conf @@ -1,10 +1,12 @@ map "$endpoint:$method:$role" $helper_mtls_policy { default "DENY"; - # /interconnect: only an operator with a superadmin cert may start interconnection - ~^helper_interconnect:.*:superadmin$ ALLOW; - # /connect: the peer CCF authenticates with its server certificate. + # /request: only an operator with a superadmin cert may start interconnection + ~^helper_request:.*:superadmin$ ALLOW; + # /sync: a peer CCF authenticates with its server certificate + ~^helper_sync:.*:ccf$ ALLOW; + # /establish: the peer CCF authenticates with its server certificate. # A server cert's CN is a hostname, so it resolves to role "unknown". - ~^helper_connect:.*:ccf$ ALLOW; + ~^helper_establish:.*:ccf$ ALLOW; # Everything else under /helper keeps the previous behaviour ~^helper_base_tree:.*:superadmin$ ALLOW; ~^helper_base_tree:.*:amf$ ALLOW; -- GitLab From 69a81d2f7190ae9eb4f5a0272bc8f4447fd185f8 Mon Sep 17 00:00:00 2001 From: Stavros-Anastasios Charismiadis Date: Fri, 17 Jul 2026 15:52:34 +0300 Subject: [PATCH 02/10] Implement basic logic of delete (without remove of interconnected APIs) --- services/docker-compose-capif.yml | 1 + .../core/capifdomaindetails.py | 38 ++++++++++++++++++- .../core/ccfinstancedetails.py | 15 +++++++- 3 files changed, 50 insertions(+), 4 deletions(-) diff --git a/services/docker-compose-capif.yml b/services/docker-compose-capif.yml index 50e2c5d..129659a 100644 --- a/services/docker-compose-capif.yml +++ b/services/docker-compose-capif.yml @@ -32,6 +32,7 @@ services: - ${CAPIF_INTERCONNECTION_HOSTNAME:-capifcore-b}:host-gateway environment: - CAPIF_HOSTNAME=${CAPIF_HOSTNAME} + - CAPIF_HTTPS_PORT=${CAPIF_HTTPS_PORT} - CONTAINER_NAME=${HELPER_CONTAINER_NAME} - VAULT_HOSTNAME=${CAPIF_VAULT} - VAULT_ACCESS_TOKEN=${CAPIF_VAULT_TOKEN} diff --git a/services/helper/helper_service/services/interconnection/core/capifdomaindetails.py b/services/helper/helper_service/services/interconnection/core/capifdomaindetails.py index 8ff2d59..e4bad65 100644 --- a/services/helper/helper_service/services/interconnection/core/capifdomaindetails.py +++ b/services/helper/helper_service/services/interconnection/core/capifdomaindetails.py @@ -63,7 +63,7 @@ class CapifDomainOperations(Resource): payload = json.dumps({ "caRoot": server_ca.decode("utf-8"), "ccfId": config['ccf_id'], - "dstProvDom": os.getenv("CAPIF_HOSTNAME"), + "dstProvDom": "{}:{}".format(os.getenv("CAPIF_HOSTNAME"), os.getenv("CAPIF_HTTPS_PORT")), "publicKey": server_pub.decode("utf-8") }) headers = { @@ -108,4 +108,38 @@ class CapifDomainOperations(Resource): def delete_capifdomain(self, ccf_id): """Remove an interconnection request for a CCF.""" - pass + current_app.logger.debug("Interconnection: Delete domain") + current_app.logger.debug(ccf_id) + + interconnected_col = self.db.get_col_by_name(self.db.interconnected) + interconnected_ccf = interconnected_col.find_one({"ccf_id": ccf_id}) + if interconnected_ccf: + current_app.logger.debug("CAPIF domain with ccf_id {} interconnected".format(ccf_id)) + # Retrieve the destination provider domain from the interconnected collection + dst_prov_dom = interconnected_ccf.get("dst_prov_dom") + # Retrieve the CAPIF configuration from the database to get the CCF ID of the originator CCF + config_col = self.db.get_col_by_name(self.db.capif_configuration) + config = config_col.find_one({}, {"_id": 0}) + url = "https://{}/helper/interconnection/establish/{}".format(dst_prov_dom, config['ccf_id']) + + headers = { + 'accept': 'application/problem+json' + } + + try: + response = requests.request("DELETE", url, headers=headers, + cert=('certs/server.crt', 'certs/server.key'), + verify='certs/ca.crt') + if response.status_code in (204, 404): + interconnected_col.delete_one({"ccf_id": ccf_id}) + return make_response("CAPIF domain with ccf_id {} deleted".format(ccf_id), 204) + else: + return Response(response.text, status=response.status_code, mimetype="application/json") + except requests.exceptions.RequestException as exc: + return not_found_error( + detail="CAPIF domain with ccf_id {} not found".format(ccf_id), + cause="NOT_FOUND", + ) + + return not_found_error("CAPIF domain with ccf_id {} not found".format(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 b989675..0324953 100644 --- a/services/helper/helper_service/services/interconnection/core/ccfinstancedetails.py +++ b/services/helper/helper_service/services/interconnection/core/ccfinstancedetails.py @@ -67,7 +67,7 @@ class CcfInstanceOperations(Resource): 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'] = os.getenv("CAPIF_HOSTNAME") + ccfinstancedetails_dict['dst_prov_dom'] = "{}:{}".format(os.getenv("CAPIF_HOSTNAME"), os.getenv("CAPIF_HTTPS_PORT")) current_app.logger.debug(ccfinstancedetails_dict) ccfinstancedetails_new = CcfInstanceDetails().from_dict(dict_to_camel_case(ccfinstancedetails_dict)) @@ -77,4 +77,15 @@ class CcfInstanceOperations(Resource): def delete_ccfinstance(self, ccf_id): """Remove an established interconnection with a peer CCF.""" - pass \ No newline at end of file + current_app.logger.debug("Interconnection: Remove instance") + current_app.logger.debug(ccf_id) + + interconnected_col = self.db.get_col_by_name(self.db.interconnected) + interconnected_ccf = interconnected_col.find_one({"ccf_id": ccf_id}) + if interconnected_ccf: + interconnected_col.delete_one({"ccf_id": ccf_id}) + res = make_response(object = "CCF instance with ccf_id {} deleted".format(ccf_id), status=204) + else: + res = not_found_error("CCF instance with ccf_id {} not found".format(ccf_id)) + + return res -- GitLab From 0e5cd2fb8a193cb974db8b3149ac2af35b154d0f Mon Sep 17 00:00:00 2001 From: Stavros-Anastasios Charismiadis Date: Tue, 21 Jul 2026 11:57:52 +0300 Subject: [PATCH 03/10] change API urls in robot tests --- .../features/Helper/Interconnection API/interconnection.robot | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/features/Helper/Interconnection API/interconnection.robot b/tests/features/Helper/Interconnection API/interconnection.robot index 49aefda..7887576 100644 --- a/tests/features/Helper/Interconnection API/interconnection.robot +++ b/tests/features/Helper/Interconnection API/interconnection.robot @@ -28,7 +28,7 @@ Interconnection 2 CCF ${body}= Create Interconnection Request Body ${DESTINATION_PROVIDER_DOMAIN} ${resp}= Post Request Capif - ... /helper/interconnection/interconnect + ... /helper/interconnection/request ... server=${CAPIF_HTTPS_URL} ... verify=ca.crt ... username=${SUPERADMIN_USERNAME} @@ -54,7 +54,7 @@ Interconnection 2 CCF X ... ${DESTINATION_PROVIDER_DOMAIN} ... ${ccf_crt} ${resp}= Post Request Capif - ... /helper/interconnection/connect + ... /helper/interconnection/establish ... server=${CAPIF_HTTPS_URL} ... verify=ca.crt ... username=${CCF_USERNAME} -- GitLab From 72ce4e59a3f42aa8484071ce2955c409db2712c2 Mon Sep 17 00:00:00 2001 From: Stavros-Anastasios Charismiadis Date: Tue, 28 Jul 2026 14:01:23 +0300 Subject: [PATCH 04/10] Add the removal of corresponding APIs when delete interconnection of two CCFs --- .../core/serviceapidescriptions.py | 46 ++++++++++++++++--- services/docker-compose-capif.yml | 1 + .../core/capifdomaindetails.py | 22 +++++++++ .../core/ccfinstancedetails.py | 22 +++++++++ 4 files changed, 85 insertions(+), 6 deletions(-) diff --git a/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/serviceapidescriptions.py b/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/serviceapidescriptions.py index ba71ce6..51073dc 100644 --- a/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/serviceapidescriptions.py +++ b/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/serviceapidescriptions.py @@ -202,7 +202,6 @@ class PublishServiceOperations(Resource): config_col = self.db.get_col_by_name(self.db.capif_configuration) config = config_col.find_one({}, {"_id": 0}) ccf_id = config['ccf_id'] - serviceapidescription_dict['ccf_id'] = ccf_id serviceapidescription_dict['shareable_info'] = {"is_shareable": False} url = 'https://{}/published-apis/v1/{}/service-apis'.format(dom, ccf_id) @@ -218,10 +217,40 @@ class PublishServiceOperations(Resource): response = requests.request("POST", url, headers=headers, data=json.dumps(clean_n_camel_case(serviceapidescription_dict), cls=encoder.CustomJSONEncoder), cert=('certs/server.crt', 'certs/server.key'), verify='certs/ca.crt') - if not (response.status_code == 201 or response.status_code == 200): + if (response.status_code == 201 or response.status_code == 200): + if rec.get("pub_api_path") is None: + current_app.logger.debug("pub_api_path not in keys") + rec.update({"pub_api_path": {"ccf_ids": [interconnected_ccf.get("ccf_id")]}}) + else: + current_app.logger.debug("pub_api_path in keys") + ccf_list = rec.get("pub_api_path").get("ccf_ids") + current_app.logger.debug("fetch") + ccf_list.append(interconnected_ccf.get("ccf_id")) + current_app.logger.debug("append") + rec.update({"pub_api_path": {"ccf_ids": ccf_list}}) + current_app.logger.debug("update") + else: rec['shareable_info']['capif_prov_doms'].remove(dom) - - + if "CCF" in apf_id: + rec['pub_api_path']['ccf_ids'].remove(apf_id) + + current_app.logger.debug("Check pub_api_path") + if "CCF" in apf_id: + current_app.logger.debug("After CCF in apf_id") + current_app.logger.debug("rec keys: {}".format(rec.keys())) + if rec.get("pub_api_path") is None: + current_app.logger.debug("pub_api_path not in keys") + rec.update({"pub_api_path": {"ccf_ids": [apf_id]}}) + else: + current_app.logger.debug("pub_api_path in keys") + ccf_list = rec.get("pub_api_path").get("ccf_ids") + current_app.logger.debug("fetch") + ccf_list.append(apf_id) + current_app.logger.debug("append") + rec.update({"pub_api_path": {"ccf_ids": ccf_list}}) + current_app.logger.debug("update") + + current_app.logger.debug("inserting record") mycol.insert_one(rec) if "APF" in apf_id: @@ -229,15 +258,20 @@ class PublishServiceOperations(Resource): current_app.logger.debug("Service inserted in database") + published_service = { + key: value for key, value in rec.items() + if key not in ("_id", "apf_id", "onboarding_date") + } + res = make_response(object=clean_n_camel_case( - serviceapidescription_dict), status=201) + published_service), status=201) res.headers['Location'] = f"https://{os.getenv('CAPIF_HOSTNAME')}/published-apis/v1/{str(apf_id)}/service-apis/{str(api_id)}" if res.status_code == 201: current_app.logger.info("Service published") event_to_send = self.service_api_availability_event( clean_n_camel_case( - serviceapidescription_dict)) + published_service)) RedisEvent(event_to_send, service_api_descriptions=[clean_n_camel_case( serviceapidescription.to_dict())], diff --git a/services/docker-compose-capif.yml b/services/docker-compose-capif.yml index 129659a..c10683d 100644 --- a/services/docker-compose-capif.yml +++ b/services/docker-compose-capif.yml @@ -273,6 +273,7 @@ services: - fluent-bit:host-gateway - otel-collector:host-gateway - ${CAPIF_VAULT}:host-gateway + - ${CAPIF_INTERCONNECTION_HOSTNAME:-capifcore-b}:host-gateway environment: - CAPIF_HOSTNAME=${CAPIF_HOSTNAME} - CONTAINER_NAME=${PUBLISHED_APIS_CONTAINER_NAME} diff --git a/services/helper/helper_service/services/interconnection/core/capifdomaindetails.py b/services/helper/helper_service/services/interconnection/core/capifdomaindetails.py index e4bad65..e3e72b5 100644 --- a/services/helper/helper_service/services/interconnection/core/capifdomaindetails.py +++ b/services/helper/helper_service/services/interconnection/core/capifdomaindetails.py @@ -102,6 +102,25 @@ class CapifDomainOperations(Resource): return Response(response.text, status=response.status_code, mimetype=remote_mimetype) + def remove_ccf_from_pub_api_path(self, services_col, ccf_id): + """Drop a CCF from the publication path of every service API that references it.""" + current_app.logger.debug("Removing ccf_id {} from pub_api_path.ccf_ids".format(ccf_id)) + + result = services_col.update_many( + {"pub_api_path.ccf_ids": ccf_id}, + {"$pull": {"pub_api_path.ccf_ids": ccf_id}} + ) + current_app.logger.debug("Service APIs updated: {}".format(result.modified_count)) + + # ccf_ids must contain at least one entry, so null out pub_api_path once it is emptied + cleaned = services_col.update_many( + {"pub_api_path.ccf_ids": {"$size": 0}}, + {"$set": {"pub_api_path": None}} + ) + current_app.logger.debug("Service APIs with empty pub_api_path nulled: {}".format(cleaned.modified_count)) + + return result.modified_count + def sync_capifdomain(self, capifdomaindetails): """Sync interconnection state with a peer CCF.""" pass @@ -131,6 +150,9 @@ class CapifDomainOperations(Resource): cert=('certs/server.crt', 'certs/server.key'), verify='certs/ca.crt') if response.status_code in (204, 404): + services_col = self.db.get_col_by_name(self.db.services_col) + services_col.delete_many({"apf_id": ccf_id}) + self.remove_ccf_from_pub_api_path(services_col, ccf_id) interconnected_col.delete_one({"ccf_id": ccf_id}) return make_response("CAPIF domain with ccf_id {} deleted".format(ccf_id), 204) else: diff --git a/services/helper/helper_service/services/interconnection/core/ccfinstancedetails.py b/services/helper/helper_service/services/interconnection/core/ccfinstancedetails.py index 0324953..8372d3b 100644 --- a/services/helper/helper_service/services/interconnection/core/ccfinstancedetails.py +++ b/services/helper/helper_service/services/interconnection/core/ccfinstancedetails.py @@ -75,6 +75,25 @@ class CcfInstanceOperations(Resource): res = make_response(object=serialize_clean(ccfinstancedetails_new), status=status) return res + def remove_ccf_from_pub_api_path(self, services_col, ccf_id): + """Drop a CCF from the publication path of every service API that references it.""" + current_app.logger.debug("Removing ccf_id {} from pub_api_path.ccf_ids".format(ccf_id)) + + result = services_col.update_many( + {"pub_api_path.ccf_ids": ccf_id}, + {"$pull": {"pub_api_path.ccf_ids": ccf_id}} + ) + current_app.logger.debug("Service APIs updated: {}".format(result.modified_count)) + + # ccf_ids must contain at least one entry, so null out pub_api_path once it is emptied + cleaned = services_col.update_many( + {"pub_api_path.ccf_ids": {"$size": 0}}, + {"$set": {"pub_api_path": None}} + ) + current_app.logger.debug("Service APIs with empty pub_api_path nulled: {}".format(cleaned.modified_count)) + + return result.modified_count + def delete_ccfinstance(self, ccf_id): """Remove an established interconnection with a peer CCF.""" current_app.logger.debug("Interconnection: Remove instance") @@ -83,6 +102,9 @@ class CcfInstanceOperations(Resource): interconnected_col = self.db.get_col_by_name(self.db.interconnected) interconnected_ccf = interconnected_col.find_one({"ccf_id": ccf_id}) if interconnected_ccf: + services_col = self.db.get_col_by_name(self.db.services_col) + services_col.delete_many({"apf_id": ccf_id}) + self.remove_ccf_from_pub_api_path(services_col, ccf_id) interconnected_col.delete_one({"ccf_id": ccf_id}) res = make_response(object = "CCF instance with ccf_id {} deleted".format(ccf_id), status=204) else: -- GitLab From 694a58b8735c5679e35614569645ce1df51e5361 Mon Sep 17 00:00:00 2001 From: Stavros-Anastasios Charismiadis Date: Tue, 28 Jul 2026 15:47:23 +0300 Subject: [PATCH 05/10] Remove unnecessary logs --- .../core/serviceapidescriptions.py | 23 ------------------- .../core/capifdomaindetails.py | 10 -------- .../core/ccfinstancedetails.py | 7 ------ 3 files changed, 40 deletions(-) diff --git a/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/serviceapidescriptions.py b/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/serviceapidescriptions.py index 51073dc..48eb78a 100644 --- a/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/serviceapidescriptions.py +++ b/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/serviceapidescriptions.py @@ -197,8 +197,6 @@ class PublishServiceOperations(Resource): interconnected_ccf = interconnected_col.find_one({"dst_prov_dom": dom}) # 4. If yes, make a publish request with CCF certificate if interconnected_ccf: - current_app.logger.debug("CCF interconnected and API can be shared: {}".format(dom)) - config_col = self.db.get_col_by_name(self.db.capif_configuration) config = config_col.find_one({}, {"_id": 0}) ccf_id = config['ccf_id'] @@ -206,58 +204,38 @@ class PublishServiceOperations(Resource): url = 'https://{}/published-apis/v1/{}/service-apis'.format(dom, ccf_id) - current_app.logger.debug("{}".format(url)) - headers = { 'accept': 'application/json', 'Content-Type': 'application/json' } - current_app.logger.debug("Add variables to request") - response = requests.request("POST", url, headers=headers, data=json.dumps(clean_n_camel_case(serviceapidescription_dict), cls=encoder.CustomJSONEncoder), cert=('certs/server.crt', 'certs/server.key'), verify='certs/ca.crt') if (response.status_code == 201 or response.status_code == 200): if rec.get("pub_api_path") is None: - current_app.logger.debug("pub_api_path not in keys") rec.update({"pub_api_path": {"ccf_ids": [interconnected_ccf.get("ccf_id")]}}) else: - current_app.logger.debug("pub_api_path in keys") ccf_list = rec.get("pub_api_path").get("ccf_ids") - current_app.logger.debug("fetch") ccf_list.append(interconnected_ccf.get("ccf_id")) - current_app.logger.debug("append") rec.update({"pub_api_path": {"ccf_ids": ccf_list}}) - current_app.logger.debug("update") else: rec['shareable_info']['capif_prov_doms'].remove(dom) if "CCF" in apf_id: rec['pub_api_path']['ccf_ids'].remove(apf_id) - current_app.logger.debug("Check pub_api_path") if "CCF" in apf_id: - current_app.logger.debug("After CCF in apf_id") - current_app.logger.debug("rec keys: {}".format(rec.keys())) if rec.get("pub_api_path") is None: - current_app.logger.debug("pub_api_path not in keys") rec.update({"pub_api_path": {"ccf_ids": [apf_id]}}) else: - current_app.logger.debug("pub_api_path in keys") ccf_list = rec.get("pub_api_path").get("ccf_ids") - current_app.logger.debug("fetch") ccf_list.append(apf_id) - current_app.logger.debug("append") rec.update({"pub_api_path": {"ccf_ids": ccf_list}}) - current_app.logger.debug("update") - current_app.logger.debug("inserting record") mycol.insert_one(rec) if "APF" in apf_id: self.auth_manager.add_auth_service(api_id, apf_id) - current_app.logger.debug("Service inserted in database") - published_service = { key: value for key, value in rec.items() if key not in ("_id", "apf_id", "onboarding_date") @@ -268,7 +246,6 @@ class PublishServiceOperations(Resource): res.headers['Location'] = f"https://{os.getenv('CAPIF_HOSTNAME')}/published-apis/v1/{str(apf_id)}/service-apis/{str(api_id)}" if res.status_code == 201: - current_app.logger.info("Service published") event_to_send = self.service_api_availability_event( clean_n_camel_case( published_service)) diff --git a/services/helper/helper_service/services/interconnection/core/capifdomaindetails.py b/services/helper/helper_service/services/interconnection/core/capifdomaindetails.py index e3e72b5..139f669 100644 --- a/services/helper/helper_service/services/interconnection/core/capifdomaindetails.py +++ b/services/helper/helper_service/services/interconnection/core/capifdomaindetails.py @@ -39,12 +39,10 @@ class CapifDomainOperations(Resource): def add_capifdomain(self, capifdomaindetails): 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/establish".format(capifdomaindetails.dst_prov_dom) @@ -77,7 +75,6 @@ class CapifDomainOperations(Resource): verify='certs/ca.crt') except requests.exceptions.RequestException as exc: detail = "Error calling interconnection endpoint: {}".format(str(exc)) - current_app.logger.error(detail) return internal_server_error(detail=detail, cause="REQUEST_FAILURE") try: @@ -85,8 +82,6 @@ class CapifDomainOperations(Resource): except ValueError: inter_ccf = response.text - current_app.logger.debug(inter_ccf) - if response.status_code in (200, 201): if isinstance(inter_ccf, dict): interconnected_col.insert_one(inter_ccf) @@ -104,20 +99,17 @@ class CapifDomainOperations(Resource): def remove_ccf_from_pub_api_path(self, services_col, ccf_id): """Drop a CCF from the publication path of every service API that references it.""" - current_app.logger.debug("Removing ccf_id {} from pub_api_path.ccf_ids".format(ccf_id)) result = services_col.update_many( {"pub_api_path.ccf_ids": ccf_id}, {"$pull": {"pub_api_path.ccf_ids": ccf_id}} ) - current_app.logger.debug("Service APIs updated: {}".format(result.modified_count)) # ccf_ids must contain at least one entry, so null out pub_api_path once it is emptied cleaned = services_col.update_many( {"pub_api_path.ccf_ids": {"$size": 0}}, {"$set": {"pub_api_path": None}} ) - current_app.logger.debug("Service APIs with empty pub_api_path nulled: {}".format(cleaned.modified_count)) return result.modified_count @@ -128,12 +120,10 @@ class CapifDomainOperations(Resource): def delete_capifdomain(self, ccf_id): """Remove an interconnection request for a CCF.""" current_app.logger.debug("Interconnection: Delete domain") - current_app.logger.debug(ccf_id) interconnected_col = self.db.get_col_by_name(self.db.interconnected) interconnected_ccf = interconnected_col.find_one({"ccf_id": ccf_id}) if interconnected_ccf: - current_app.logger.debug("CAPIF domain with ccf_id {} interconnected".format(ccf_id)) # Retrieve the destination provider domain from the interconnected collection dst_prov_dom = interconnected_ccf.get("dst_prov_dom") # Retrieve the CAPIF configuration from the database to get the CCF ID of the originator CCF diff --git a/services/helper/helper_service/services/interconnection/core/ccfinstancedetails.py b/services/helper/helper_service/services/interconnection/core/ccfinstancedetails.py index 8372d3b..868e592 100644 --- a/services/helper/helper_service/services/interconnection/core/ccfinstancedetails.py +++ b/services/helper/helper_service/services/interconnection/core/ccfinstancedetails.py @@ -37,7 +37,6 @@ class CcfInstanceOperations(Resource): def add_ccfinstance(self, ccfinstancedetails): current_app.logger.debug("Interconnection: Add instance") - current_app.logger.debug(ccfinstancedetails) with open('certs/ca.crt', 'rb') as ca_cert: server_ca = ca_cert.read() @@ -68,36 +67,30 @@ class CcfInstanceOperations(Resource): ccfinstancedetails_dict['public_key'] = server_pub.decode("utf-8") ccfinstancedetails_dict['ccf_id'] = config['ccf_id'] ccfinstancedetails_dict['dst_prov_dom'] = "{}:{}".format(os.getenv("CAPIF_HOSTNAME"), os.getenv("CAPIF_HTTPS_PORT")) - current_app.logger.debug(ccfinstancedetails_dict) ccfinstancedetails_new = CcfInstanceDetails().from_dict(dict_to_camel_case(ccfinstancedetails_dict)) - current_app.logger.debug(ccfinstancedetails_new) res = make_response(object=serialize_clean(ccfinstancedetails_new), status=status) return res def remove_ccf_from_pub_api_path(self, services_col, ccf_id): """Drop a CCF from the publication path of every service API that references it.""" - current_app.logger.debug("Removing ccf_id {} from pub_api_path.ccf_ids".format(ccf_id)) result = services_col.update_many( {"pub_api_path.ccf_ids": ccf_id}, {"$pull": {"pub_api_path.ccf_ids": ccf_id}} ) - current_app.logger.debug("Service APIs updated: {}".format(result.modified_count)) # ccf_ids must contain at least one entry, so null out pub_api_path once it is emptied cleaned = services_col.update_many( {"pub_api_path.ccf_ids": {"$size": 0}}, {"$set": {"pub_api_path": None}} ) - current_app.logger.debug("Service APIs with empty pub_api_path nulled: {}".format(cleaned.modified_count)) return result.modified_count def delete_ccfinstance(self, ccf_id): """Remove an established interconnection with a peer CCF.""" current_app.logger.debug("Interconnection: Remove instance") - current_app.logger.debug(ccf_id) interconnected_col = self.db.get_col_by_name(self.db.interconnected) interconnected_ccf = interconnected_col.find_one({"ccf_id": ccf_id}) -- GitLab From 8a9bb7ac61a41240a6418fb860920b030a3bee43 Mon Sep 17 00:00:00 2001 From: Stavros-Anastasios Charismiadis Date: Tue, 28 Jul 2026 17:30:45 +0300 Subject: [PATCH 06/10] Add sync API, sync 2 CCFs after interconnection established --- .../controllers/default_controller.py | 2 +- .../core/capifdomaindetails.py | 7 +- .../core/ccfinstancedetails.py | 126 +++++++++++++++++- 3 files changed, 128 insertions(+), 7 deletions(-) diff --git a/services/helper/helper_service/services/interconnection/controllers/default_controller.py b/services/helper/helper_service/services/interconnection/controllers/default_controller.py index 43f1da9..9335a76 100644 --- a/services/helper/helper_service/services/interconnection/controllers/default_controller.py +++ b/services/helper/helper_service/services/interconnection/controllers/default_controller.py @@ -81,7 +81,7 @@ def interconnect_sync(body): # noqa: E501 if connexion.request.is_json: capif_domain_details = CapifDomainDetails.from_dict(connexion.request.get_json()) # noqa: E501 - res = capif_domain_operations.sync_capifdomain(capif_domain_details) + res = ccf_operations.sync_ccfinstance(capif_domain_details) return res diff --git a/services/helper/helper_service/services/interconnection/core/capifdomaindetails.py b/services/helper/helper_service/services/interconnection/core/capifdomaindetails.py index 139f669..152fbc0 100644 --- a/services/helper/helper_service/services/interconnection/core/capifdomaindetails.py +++ b/services/helper/helper_service/services/interconnection/core/capifdomaindetails.py @@ -12,6 +12,7 @@ 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 ..vendor_specific import add_vend_spec_fields from .auth_manager import AuthManager +from .ccfinstancedetails import CcfInstanceOperations from .publisher import Publisher from .redis_event import RedisEvent from .resources import Resource @@ -36,6 +37,7 @@ class CapifDomainOperations(Resource): Resource.__init__(self) self.auth_manager = AuthManager() self.db = get_mongo() + self.ccf_operations = CcfInstanceOperations() def add_capifdomain(self, capifdomaindetails): current_app.logger.debug("Interconnection: Add domain") @@ -85,6 +87,7 @@ class CapifDomainOperations(Resource): if response.status_code in (200, 201): if isinstance(inter_ccf, dict): interconnected_col.insert_one(inter_ccf) + self.ccf_operations.sync_with_ccf(capifdomaindetails.dst_prov_dom) ccfinstancedetails_new = CcfInstanceDetails().from_dict(dict_to_camel_case(inter_ccf)) return make_response(object=ccfinstancedetails_new, status=response.status_code) @@ -113,10 +116,6 @@ class CapifDomainOperations(Resource): return result.modified_count - def sync_capifdomain(self, capifdomaindetails): - """Sync interconnection state with a peer CCF.""" - pass - def delete_capifdomain(self, ccf_id): """Remove an interconnection request for a CCF.""" current_app.logger.debug("Interconnection: Delete domain") diff --git a/services/helper/helper_service/services/interconnection/core/ccfinstancedetails.py b/services/helper/helper_service/services/interconnection/core/ccfinstancedetails.py index 868e592..fb66401 100644 --- a/services/helper/helper_service/services/interconnection/core/ccfinstancedetails.py +++ b/services/helper/helper_service/services/interconnection/core/ccfinstancedetails.py @@ -1,13 +1,17 @@ import os import secrets from datetime import datetime +import requests +import json -from flask import current_app +from flask import current_app, Response # from pymongo import ReturnDocument from db.db import get_mongo +from ..encoder import CustomJSONEncoder +from ..models.capif_domain_details import CapifDomainDetails from ..models.ccf_instance_details import CcfInstanceDetails -from ..util import clean_empty, clean_n_camel_case, dict_to_camel_case, serialize_clean +from ..util import clean_empty, clean_n_camel_case, dict_to_camel_case, serialize_clean, serialize_clean_camel_case # from ..vendor_specific import add_vend_spec_fields from .auth_manager import AuthManager from .publisher import Publisher @@ -72,6 +76,124 @@ class CcfInstanceOperations(Resource): res = make_response(object=serialize_clean(ccfinstancedetails_new), status=status) return res + def local_ccf_identity(self): + """Return the CCF identifier and the provider domain of this CAPIF instance.""" + config_col = self.db.get_col_by_name(self.db.capif_configuration) + config = config_col.find_one({}, {"_id": 0}) + dst_prov_dom = "{}:{}".format(os.getenv("CAPIF_HOSTNAME"), os.getenv("CAPIF_HTTPS_PORT")) + + return config['ccf_id'], dst_prov_dom + + def add_ccf_to_pub_api_path(self, services_col, service_api, ccf_id): + """Record that a service API is from now on also published through the given CCF.""" + if service_api.get("pub_api_path") is None: + update = {"$set": {"pub_api_path": {"ccf_ids": [ccf_id]}}} + else: + update = {"$addToSet": {"pub_api_path.ccf_ids": ccf_id}} + + services_col.update_one({"_id": service_api["_id"]}, update) + + def publish_shared_apis(self, peer_dom, peer_ccf_id, local_ccf_id): + """Publish to a peer CCF every local service API that is shareable with its domain.""" + services_col = self.db.get_col_by_name(self.db.services_col) + shareable_apis = services_col.find({ + "shareable_info.is_shareable": True, + "shareable_info.capif_prov_doms": peer_dom, + "apf_id": {"$ne": peer_ccf_id}, + "pub_api_path.ccf_ids": {"$ne": peer_ccf_id} + }) + + url = "https://{}/published-apis/v1/{}/service-apis".format(peer_dom, local_ccf_id) + headers = { + 'accept': 'application/json', + 'Content-Type': 'application/json' + } + + published = 0 + for service_api in shareable_apis: + payload = { + key: value for key, value in service_api.items() + if key not in ("_id", "apf_id", "onboarding_date") + } + # The peer stores the API as its own, it must not share it any further + payload['shareable_info'] = {"is_shareable": False} + + try: + response = requests.request("POST", url, headers=headers, + data=json.dumps(clean_n_camel_case(payload), cls=CustomJSONEncoder), + cert=('certs/server.crt', 'certs/server.key'), + verify='certs/ca.crt') + except requests.exceptions.RequestException as exc: + current_app.logger.error("Interconnection: publish to {} failed: {}".format(peer_dom, str(exc))) + continue + + if response.status_code not in (200, 201): + current_app.logger.warning("Interconnection: {} rejected service api {} with status {}".format( + peer_dom, service_api.get("api_name"), response.status_code)) + continue + + self.add_ccf_to_pub_api_path(services_col, service_api, peer_ccf_id) + published += 1 + + current_app.logger.debug("Interconnection: {} service apis shared with {}".format(published, peer_dom)) + + return published + + def sync_ccfinstance(self, capifdomaindetails): + """Share the local service APIs with the peer CCF asking for a sync.""" + current_app.logger.debug("Interconnection: Sync instance") + + peer_dom = capifdomaindetails.dst_prov_dom + + interconnected_col = self.db.get_col_by_name(self.db.interconnected) + interconnected_ccf = interconnected_col.find_one({"dst_prov_dom": peer_dom}) + if interconnected_ccf is None: + return not_found_error( + detail="CAPIF domain {} is not interconnected".format(peer_dom), + cause="NOT_FOUND") + + local_ccf_id, local_dom = self.local_ccf_identity() + self.publish_shared_apis(peer_dom, interconnected_ccf.get("ccf_id"), local_ccf_id) + + capifdomaindetails_new = CapifDomainDetails(dst_prov_dom=local_dom) + + return make_response(object=serialize_clean_camel_case(capifdomaindetails_new), status=201) + + def sync_with_ccf(self, peer_dom): + """Ask a peer CCF for a sync and share the local service APIs back once it answers.""" + current_app.logger.debug("Interconnection: Sync with {}".format(peer_dom)) + + interconnected_col = self.db.get_col_by_name(self.db.interconnected) + interconnected_ccf = interconnected_col.find_one({"dst_prov_dom": peer_dom}) + if interconnected_ccf is None: + current_app.logger.warning("Interconnection: {} is not interconnected, sync skipped".format(peer_dom)) + return 0 + + local_ccf_id, local_dom = self.local_ccf_identity() + + url = "https://{}/helper/interconnection/sync".format(peer_dom) + headers = { + 'accept': 'application/json', + 'Content-Type': 'application/json' + } + payload = json.dumps(serialize_clean_camel_case(CapifDomainDetails(dst_prov_dom=local_dom))) + + try: + response = requests.request("POST", url, headers=headers, data=payload, + cert=('certs/server.crt', 'certs/server.key'), + verify='certs/ca.crt') + except requests.exceptions.RequestException as exc: + current_app.logger.error("Interconnection: sync request to {} failed: {}".format(peer_dom, str(exc))) + return 0 + + if response.status_code not in (200, 201): + current_app.logger.warning("Interconnection: {} answered sync with status {}".format( + peer_dom, response.status_code)) + return 0 + + # The peer shared its own APIs while serving the request, now mirror the procedure + return self.publish_shared_apis(peer_dom, interconnected_ccf.get("ccf_id"), local_ccf_id) + def remove_ccf_from_pub_api_path(self, services_col, ccf_id): """Drop a CCF from the publication path of every service API that references it.""" -- GitLab From bc3ebb4c369b07547f97a00d7c647e202070222d Mon Sep 17 00:00:00 2001 From: Jorge Moratinos Salcines Date: Mon, 3 Aug 2026 11:16:43 +0200 Subject: [PATCH 07/10] Fixed local deployment and local test --- services/register/config.yaml | 42 +++++++++---------- .../Interconnection API/interconnection.robot | 26 ++++++++++-- 2 files changed, 43 insertions(+), 25 deletions(-) diff --git a/services/register/config.yaml b/services/register/config.yaml index efe6288..fb2d19f 100644 --- a/services/register/config.yaml +++ b/services/register/config.yaml @@ -1,13 +1,12 @@ -mongo: { - 'user': 'root', - 'password': 'example', - 'db': 'capif_users', - 'col': 'user', - 'col_capif_configuration': "capif_configuration", - 'admins': 'admins', - 'host': 'mongo_register', - 'port': '27017' -} +mongo: + user: "root" + password: "example" + db: "capif_users" + col: "user" + col_capif_configuration: "capif_configuration" + admins: "admins" + host: "mongo_register" + port: "27017" ca_factory: url: !ENV ${VAULT_HOSTNAME} @@ -16,20 +15,19 @@ ca_factory: verify: False -ccf: { - "url": "capifcore", - "helper_remove_user": "/helper/deleteEntities/" -} +ccf: + url: !ENV ${CAPIF_HOSTNAME} + helper_remove_user: "/helper/deleteEntities/" -register: { - "register_uuid": '6ba7b810-9dad-11d1-80b4-00c04fd430c8', - "refresh_expiration": 30, #days - "token_expiration": 10, #mins - "admin_users": {admin_user: "admin", - admin_pass: "password123"} -} +register: + register_uuid: '6ba7b810-9dad-11d1-80b4-00c04fd430c8' + refresh_expiration: 30 #days + token_expiration: 10 #mins + admin_users: + admin_user: "admin" + admin_pass: "password123" -capif_configuration: +capif_configuration: config_description: Default Register Configuration config_name: default config_version: "1.0" diff --git a/tests/features/Helper/Interconnection API/interconnection.robot b/tests/features/Helper/Interconnection API/interconnection.robot index 7887576..06f27e3 100644 --- a/tests/features/Helper/Interconnection API/interconnection.robot +++ b/tests/features/Helper/Interconnection API/interconnection.robot @@ -24,7 +24,7 @@ Interconnection 2 CCF ${DESTINATION_PROVIDER_DOMAIN}= Set Variable capifcore-b:1443 - ${ccf_id}= Get Capif Ccf Id + ${ccf_id_a}= Get Capif Ccf Id ${body}= Create Interconnection Request Body ${DESTINATION_PROVIDER_DOMAIN} ${resp}= Post Request Capif @@ -37,12 +37,23 @@ Interconnection 2 CCF Should Be Equal As Integers ${resp.status_code} 201 Log Dictionary ${resp.json()} + ${ccf_id_b}= Set Variable ${resp.json()['ccfId']} + + # Delete interconnection + ${resp}= Delete Request Capif + ... /helper/interconnection/request/${ccf_id_b} + ... server=${CAPIF_HTTPS_URL} + ... verify=ca.crt + ... username=${SUPERADMIN_USERNAME} + + Should Be Equal As Integers ${resp.status_code} 204 + Interconnection 2 CCF X [Tags] interconnection-2 ${DESTINATION_PROVIDER_DOMAIN}= Set Variable capifcore-b:1443 - ${ccf_id}= Get Capif Ccf Id + ${ccf_id_a}= Get Capif Ccf Id ${ccf_crt}= Read File Utf8 ${CCF_USERNAME}.crt ${ccf_key}= Read File Utf8 ${CCF_USERNAME}.key @@ -50,7 +61,7 @@ Interconnection 2 CCF X ${body}= Create Connection Request Body ... ${ccf_ca} - ... ${ccf_id} + ... ${ccf_id_a} ... ${DESTINATION_PROVIDER_DOMAIN} ... ${ccf_crt} ${resp}= Post Request Capif @@ -62,3 +73,12 @@ Interconnection 2 CCF X Should Be Equal As Integers ${resp.status_code} 201 Log Dictionary ${resp.json()} + + # Delete interconnection + ${resp}= Delete Request Capif + ... /helper/interconnection/establish/${ccf_id_a} + ... server=${CAPIF_HTTPS_URL} + ... verify=ca.crt + ... username=${CCF_USERNAME} + + Should Be Equal As Integers ${resp.status_code} 204 -- GitLab From 3ed0e4496bae635e32c3e1345887e078d3b9ebfa Mon Sep 17 00:00:00 2001 From: Jorge Moratinos Salcines Date: Mon, 3 Aug 2026 11:57:00 +0200 Subject: [PATCH 08/10] Fixed scripts to not run by default interconnection scenarios --- helm/scripts/run_remote_capif_tests.sh | 2 +- services/run_capif_tests.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/helm/scripts/run_remote_capif_tests.sh b/helm/scripts/run_remote_capif_tests.sh index c1f2961..e2b03a1 100755 --- a/helm/scripts/run_remote_capif_tests.sh +++ b/helm/scripts/run_remote_capif_tests.sh @@ -52,7 +52,7 @@ INPUT_OPTIONS=$@ # Check if input is provided if [ -z "$1" ]; then # Set default value if no input is provided - INPUT_OPTIONS="--include all" + INPUT_OPTIONS="--include all --exclude interconnection" fi cd $CAPIF_BASE_DIR diff --git a/services/run_capif_tests.sh b/services/run_capif_tests.sh index 968b1c5..d050207 100755 --- a/services/run_capif_tests.sh +++ b/services/run_capif_tests.sh @@ -121,7 +121,7 @@ INPUT_OPTIONS=$@ # Check if input is provided if [ -z "$1" ]; then # Set default value if no input is provided - INPUT_OPTIONS="--include all" + INPUT_OPTIONS="--include all --exclude interconnection" fi docker >/dev/null 2>/dev/null -- GitLab From 82f914fbb99a56bc3c573295cabf282c6b3f938c Mon Sep 17 00:00:00 2001 From: Stavros-Anastasios Charismiadis Date: Mon, 3 Aug 2026 13:24:14 +0300 Subject: [PATCH 09/10] add https port only when different from default, improve concecutive Publish requests, add GET request to interconection APIs --- .../core/serviceapidescriptions.py | 2 - .../openapi_helper_interconnection.yaml | 87 ++++++++++++++++++ .../controllers/default_controller.py | 27 ++++++ .../core/capifdomaindetails.py | 39 ++++++-- .../core/ccfinstancedetails.py | 75 +++++++++------- .../interconnection/openapi/openapi.yaml | 90 +++++++++++++++++++ 6 files changed, 281 insertions(+), 39 deletions(-) diff --git a/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/serviceapidescriptions.py b/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/serviceapidescriptions.py index 48eb78a..45486df 100644 --- a/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/serviceapidescriptions.py +++ b/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/serviceapidescriptions.py @@ -220,8 +220,6 @@ class PublishServiceOperations(Resource): rec.update({"pub_api_path": {"ccf_ids": ccf_list}}) else: rec['shareable_info']['capif_prov_doms'].remove(dom) - if "CCF" in apf_id: - rec['pub_api_path']['ccf_ids'].remove(apf_id) if "CCF" in apf_id: if rec.get("pub_api_path") is None: diff --git a/services/helper/helper_service/openapi_helper_interconnection.yaml b/services/helper/helper_service/openapi_helper_interconnection.yaml index be61376..b0ae2e2 100644 --- a/services/helper/helper_service/openapi_helper_interconnection.yaml +++ b/services/helper/helper_service/openapi_helper_interconnection.yaml @@ -12,6 +12,44 @@ servers: # 1. PATHS: Where you define your endpoints paths: /request: + get: + summary: Retrieve the interconnection information of every CCF + operationId: get_interconnect_requests # <--- Becomes 'def get_interconnect_requests():' in Python + responses: + "200": + description: Interconnection information of every interconnected CCF + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/CcfInstanceDetails' + "401": + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ProblemDetails' + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ProblemDetails' + description: Forbidden + "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 post: summary: Send a new interconnection request operationId: interconnect_request # <--- Becomes 'def interconnect_request(body):' in Python @@ -122,6 +160,55 @@ paths: default: description: Generic Error /request/{ccf_id}: + get: + summary: Retrieve the interconnection information of a single CCF + operationId: get_interconnect_request # <--- Becomes 'def get_interconnect_request(ccf_id):' in Python + parameters: + - description: CCF identifier of the interconnection to retrieve + in: path + name: ccf_id + required: true + schema: + type: string + responses: + "200": + description: Interconnection information of the requested CCF + content: + application/json: + schema: + $ref: '#/components/schemas/CcfInstanceDetails' + "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 delete: summary: Remove an interconnection request operationId: delete_interconnect_request # <--- Becomes 'def delete_interconnect_request(ccf_id):' in Python diff --git a/services/helper/helper_service/services/interconnection/controllers/default_controller.py b/services/helper/helper_service/services/interconnection/controllers/default_controller.py index 9335a76..cd279f5 100644 --- a/services/helper/helper_service/services/interconnection/controllers/default_controller.py +++ b/services/helper/helper_service/services/interconnection/controllers/default_controller.py @@ -48,6 +48,33 @@ def delete_connected_ccf(ccf_id): # noqa: E501 return res +def get_interconnect_requests(): # noqa: E501 + """Retrieve the interconnection information of every CCF + + # noqa: E501 + + :rtype: Union[List[CcfInstanceDetails], Tuple[List[CcfInstanceDetails], int], Tuple[List[CcfInstanceDetails], int, Dict[str, str]] + """ + res = capif_domain_operations.get_capifdomains() + + return res + + +def get_interconnect_request(ccf_id): # noqa: E501 + """Retrieve the interconnection information of a single CCF + + # noqa: E501 + + :param ccf_id: CCF identifier of the interconnection to retrieve + :type ccf_id: str + + :rtype: Union[CcfInstanceDetails, Tuple[CcfInstanceDetails, int], Tuple[CcfInstanceDetails, int, Dict[str, str]] + """ + res = capif_domain_operations.get_capifdomain(ccf_id) + + return res + + def interconnect_request(body): # noqa: E501 """Send a new interconnection request diff --git a/services/helper/helper_service/services/interconnection/core/capifdomaindetails.py b/services/helper/helper_service/services/interconnection/core/capifdomaindetails.py index 152fbc0..8f520c2 100644 --- a/services/helper/helper_service/services/interconnection/core/capifdomaindetails.py +++ b/services/helper/helper_service/services/interconnection/core/capifdomaindetails.py @@ -39,6 +39,35 @@ class CapifDomainOperations(Resource): self.db = get_mongo() self.ccf_operations = CcfInstanceOperations() + def get_capifdomains(self): + """Return the interconnection details of every interconnected CCF.""" + current_app.logger.debug("Interconnection: Get domains") + + interconnected_col = self.db.get_col_by_name(self.db.interconnected) + interconnected_ccfs = interconnected_col.find({}, {"_id": 0}) + + ccfinstances = [ + serialize_clean_camel_case(CcfInstanceDetails().from_dict(dict_to_camel_case(interconnected_ccf))) + for interconnected_ccf in interconnected_ccfs + ] + + return make_response(object=ccfinstances, status=200) + + def get_capifdomain(self, ccf_id): + """Return the interconnection details of a single interconnected CCF.""" + current_app.logger.debug("Interconnection: Get domain") + + interconnected_col = self.db.get_col_by_name(self.db.interconnected) + interconnected_ccf = interconnected_col.find_one({"ccf_id": ccf_id}, {"_id": 0}) + if interconnected_ccf is None: + return not_found_error( + detail="CAPIF domain with ccf_id {} not found".format(ccf_id), + cause="NOT_FOUND") + + ccfinstancedetails_new = CcfInstanceDetails().from_dict(dict_to_camel_case(interconnected_ccf)) + + return make_response(object=serialize_clean_camel_case(ccfinstancedetails_new), status=200) + def add_capifdomain(self, capifdomaindetails): current_app.logger.debug("Interconnection: Add domain") @@ -57,13 +86,12 @@ class CapifDomainOperations(Resource): 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}) + local_ccf_id, local_dom = self.ccf_operations.local_ccf_identity() payload = json.dumps({ "caRoot": server_ca.decode("utf-8"), - "ccfId": config['ccf_id'], - "dstProvDom": "{}:{}".format(os.getenv("CAPIF_HOSTNAME"), os.getenv("CAPIF_HTTPS_PORT")), + "ccfId": local_ccf_id, + "dstProvDom": local_dom, "publicKey": server_pub.decode("utf-8") }) headers = { @@ -87,7 +115,8 @@ class CapifDomainOperations(Resource): if response.status_code in (200, 201): if isinstance(inter_ccf, dict): interconnected_col.insert_one(inter_ccf) - self.ccf_operations.sync_with_ccf(capifdomaindetails.dst_prov_dom) + # The peer reports the domain it is reachable at, which is what got stored + self.ccf_operations.sync_with_ccf(inter_ccf.get("dst_prov_dom", capifdomaindetails.dst_prov_dom)) ccfinstancedetails_new = CcfInstanceDetails().from_dict(dict_to_camel_case(inter_ccf)) return make_response(object=ccfinstancedetails_new, 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 fb66401..ae5341b 100644 --- a/services/helper/helper_service/services/interconnection/core/ccfinstancedetails.py +++ b/services/helper/helper_service/services/interconnection/core/ccfinstancedetails.py @@ -31,6 +31,9 @@ publisher_ops = Publisher() service_api_not_found_message = "Service API not found" +# Seconds allowed for a single publish request towards a peer CCF +publish_timeout = 30 + class CcfInstanceOperations(Resource): @@ -64,13 +67,12 @@ class CcfInstanceOperations(Resource): interconnected_col.insert_one(clean_empty(ccfinstancedetails_dict)) status = 201 - config_col = self.db.get_col_by_name(self.db.capif_configuration) - config = config_col.find_one({}, {"_id": 0}) + local_ccf_id, local_dom = self.local_ccf_identity() 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'] = "{}:{}".format(os.getenv("CAPIF_HOSTNAME"), os.getenv("CAPIF_HTTPS_PORT")) + ccfinstancedetails_dict['ccf_id'] = local_ccf_id + ccfinstancedetails_dict['dst_prov_dom'] = local_dom ccfinstancedetails_new = CcfInstanceDetails().from_dict(dict_to_camel_case(ccfinstancedetails_dict)) res = make_response(object=serialize_clean(ccfinstancedetails_new), status=status) @@ -80,7 +82,11 @@ class CcfInstanceOperations(Resource): """Return the CCF identifier and the provider domain of this CAPIF instance.""" config_col = self.db.get_col_by_name(self.db.capif_configuration) config = config_col.find_one({}, {"_id": 0}) - dst_prov_dom = "{}:{}".format(os.getenv("CAPIF_HOSTNAME"), os.getenv("CAPIF_HTTPS_PORT")) + + hostname = os.getenv("CAPIF_HOSTNAME") + https_port = os.getenv("CAPIF_HTTPS_PORT") + + dst_prov_dom = hostname if https_port in (None, "", "443") else "{}:{}".format(hostname, https_port) return config['ccf_id'], dst_prov_dom @@ -104,36 +110,41 @@ class CcfInstanceOperations(Resource): }) url = "https://{}/published-apis/v1/{}/service-apis".format(peer_dom, local_ccf_id) - headers = { - 'accept': 'application/json', - 'Content-Type': 'application/json' - } published = 0 - for service_api in shareable_apis: - payload = { - key: value for key, value in service_api.items() - if key not in ("_id", "apf_id", "onboarding_date") - } - # The peer stores the API as its own, it must not share it any further - payload['shareable_info'] = {"is_shareable": False} - - try: - response = requests.request("POST", url, headers=headers, + # A single session keeps the connection alive, so the mTLS handshake with the peer + # is paid once for the whole batch instead of once per service API + with requests.Session() as session: + session.cert = ('certs/server.crt', 'certs/server.key') + session.verify = 'certs/ca.crt' + session.headers.update({ + 'accept': 'application/json', + 'Content-Type': 'application/json' + }) + + for service_api in shareable_apis: + payload = { + key: value for key, value in service_api.items() + if key not in ("_id", "apf_id", "onboarding_date") + } + # The peer stores the API as its own, it must not share it any further + payload['shareable_info'] = {"is_shareable": False} + + try: + response = session.post(url, data=json.dumps(clean_n_camel_case(payload), cls=CustomJSONEncoder), - cert=('certs/server.crt', 'certs/server.key'), - verify='certs/ca.crt') - except requests.exceptions.RequestException as exc: - current_app.logger.error("Interconnection: publish to {} failed: {}".format(peer_dom, str(exc))) - continue - - if response.status_code not in (200, 201): - current_app.logger.warning("Interconnection: {} rejected service api {} with status {}".format( - peer_dom, service_api.get("api_name"), response.status_code)) - continue - - self.add_ccf_to_pub_api_path(services_col, service_api, peer_ccf_id) - published += 1 + timeout=publish_timeout) + except requests.exceptions.RequestException as exc: + current_app.logger.error("Interconnection: publish to {} failed: {}".format(peer_dom, str(exc))) + continue + + if response.status_code not in (200, 201): + current_app.logger.warning("Interconnection: {} rejected service api {} with status {}".format( + peer_dom, service_api.get("api_name"), response.status_code)) + continue + + self.add_ccf_to_pub_api_path(services_col, service_api, peer_ccf_id) + published += 1 current_app.logger.debug("Interconnection: {} service apis shared with {}".format(published, peer_dom)) diff --git a/services/helper/helper_service/services/interconnection/openapi/openapi.yaml b/services/helper/helper_service/services/interconnection/openapi/openapi.yaml index f53aef3..7d6f998 100644 --- a/services/helper/helper_service/services/interconnection/openapi/openapi.yaml +++ b/services/helper/helper_service/services/interconnection/openapi/openapi.yaml @@ -120,6 +120,46 @@ paths: summary: Remove an established interconnection x-openapi-router-controller: interconnection.controllers.default_controller /request: + get: + operationId: get_interconnect_requests + responses: + "200": + content: + application/json: + schema: + items: + $ref: "#/components/schemas/CcfInstanceDetails" + title: interconnections + type: array + description: Interconnection information of every interconnected CCF + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/ProblemDetails" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/ProblemDetails" + description: Forbidden + "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: Retrieve the interconnection information of every CCF + x-openapi-router-controller: interconnection.controllers.default_controller post: operationId: interconnect_request requestBody: @@ -232,6 +272,56 @@ paths: summary: Send an interconnection sync request from one CCF to another x-openapi-router-controller: interconnection.controllers.default_controller /request/{ccf_id}: + get: + operationId: get_interconnect_request + parameters: + - description: CCF identifier of the interconnection to retrieve + in: path + name: ccf_id + required: true + schema: + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/CcfInstanceDetails" + description: Interconnection information of the requested CCF + "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: Retrieve the interconnection information of a single CCF + x-openapi-router-controller: interconnection.controllers.default_controller delete: operationId: delete_interconnect_request parameters: -- GitLab From ea3f3e281264fe3505e6a251dab86418616d03f9 Mon Sep 17 00:00:00 2001 From: Stavros-Anastasios Charismiadis Date: Mon, 3 Aug 2026 15:39:46 +0300 Subject: [PATCH 10/10] Upgrade pyopenssl, cryptography and starlette packages. Upgrade also the commands creating the certificates --- .../requirements.txt | 4 +- .../requirements.txt | 4 +- .../requirements.txt | 4 +- .../requirements.txt | 6 +-- .../requirements.txt | 4 +- .../TS29222_CAPIF_Events_API/requirements.txt | 6 +-- .../requirements.txt | 6 +-- .../requirements.txt | 2 +- .../requirements.txt | 6 +-- .../requirements.txt | 2 +- .../requirements.txt | 4 +- services/helper/helper_service/app.py | 39 +++++++++-------- services/helper/requirements.txt | 3 +- services/register/register_service/app.py | 37 +++++++++------- services/register/requirements.txt | 3 +- tests/libraries/helpers.py | 43 ++++++++++--------- tools/robot/basicRequirements.txt | 4 +- 17 files changed, 96 insertions(+), 81 deletions(-) diff --git a/services/TS29222_CAPIF_API_Invoker_Management_API/requirements.txt b/services/TS29222_CAPIF_API_Invoker_Management_API/requirements.txt index d2bc7d5..077b627 100644 --- a/services/TS29222_CAPIF_API_Invoker_Management_API/requirements.txt +++ b/services/TS29222_CAPIF_API_Invoker_Management_API/requirements.txt @@ -7,7 +7,7 @@ Flask == 3.0.3 pymongo == 4.7.3 redis == 4.5.4 flask_jwt_extended == 4.6.0 -cryptography == 46.0.1 +cryptography == 50.0.0 rfc3987 == 1.3.8 opentelemetry-instrumentation == 0.61b0 opentelemetry-instrumentation-flask == 0.61b0 @@ -23,4 +23,4 @@ Flask-APScheduler == 1.13.1 werkzeug == 3.1.4 gunicorn == 23.0.0 packaging == 24.0 -starlette == 0.52.1 \ No newline at end of file +starlette == 1.3.1 \ No newline at end of file diff --git a/services/TS29222_CAPIF_API_Provider_Management_API/requirements.txt b/services/TS29222_CAPIF_API_Provider_Management_API/requirements.txt index 7a55a99..8cc5953 100644 --- a/services/TS29222_CAPIF_API_Provider_Management_API/requirements.txt +++ b/services/TS29222_CAPIF_API_Provider_Management_API/requirements.txt @@ -6,7 +6,7 @@ Flask == 3.0.3 pymongo == 4.7.3 redis == 4.5.4 flask_jwt_extended == 4.6.0 -cryptography == 46.0.1 +cryptography == 50.0.0 rfc3987 == 1.3.8 opentelemetry-instrumentation == 0.61b0 opentelemetry-instrumentation-flask == 0.61b0 @@ -21,4 +21,4 @@ flask_executor == 1.0.0 werkzeug == 3.1.3 gunicorn == 23.0.0 packaging == 24.0 -starlette == 0.52.1 \ No newline at end of file +starlette == 1.3.1 \ No newline at end of file diff --git a/services/TS29222_CAPIF_Access_Control_Policy_API/requirements.txt b/services/TS29222_CAPIF_Access_Control_Policy_API/requirements.txt index c5676ce..becef42 100644 --- a/services/TS29222_CAPIF_Access_Control_Policy_API/requirements.txt +++ b/services/TS29222_CAPIF_Access_Control_Policy_API/requirements.txt @@ -6,7 +6,7 @@ Flask == 3.0.3 pymongo == 4.7.3 redis == 4.5.4 flask_jwt_extended == 4.6.0 -cryptography == 46.0.1 +cryptography == 50.0.0 opentelemetry-instrumentation == 0.61b0 opentelemetry-instrumentation-flask == 0.61b0 opentelemetry-instrumentation-redis == 0.61b0 @@ -21,4 +21,4 @@ Flask-APScheduler == 1.13.1 Flask-Script == 2.0.6 gunicorn == 23.0.0 packaging == 24.0 -starlette == 0.52.1 \ No newline at end of file +starlette == 1.3.1 \ No newline at end of file diff --git a/services/TS29222_CAPIF_Auditing_API/requirements.txt b/services/TS29222_CAPIF_Auditing_API/requirements.txt index 95d1a60..c282005 100644 --- a/services/TS29222_CAPIF_Auditing_API/requirements.txt +++ b/services/TS29222_CAPIF_Auditing_API/requirements.txt @@ -6,7 +6,7 @@ Flask == 3.0.3 pymongo == 4.7.3 elasticsearch == 8.4.3 flask_jwt_extended == 4.6.0 -cryptography == 46.0.1 +cryptography == 50.0.0 opentelemetry-instrumentation == 0.61b0 opentelemetry-instrumentation-flask == 0.61b0 opentelemetry-instrumentation-redis == 0.61b0 @@ -18,7 +18,7 @@ opentelemetry-api == 1.40.0 opentelemetry-sdk == 1.40.0 flask_executor == 1.0.0 werkzeug == 3.1.3 -pyopenssl == 25.3.0 +pyopenssl == 26.4.0 gunicorn == 23.0.0 packaging == 24.0 -starlette == 0.52.1 \ No newline at end of file +starlette == 1.3.1 \ No newline at end of file diff --git a/services/TS29222_CAPIF_Discover_Service_API/requirements.txt b/services/TS29222_CAPIF_Discover_Service_API/requirements.txt index 7a55a99..8cc5953 100644 --- a/services/TS29222_CAPIF_Discover_Service_API/requirements.txt +++ b/services/TS29222_CAPIF_Discover_Service_API/requirements.txt @@ -6,7 +6,7 @@ Flask == 3.0.3 pymongo == 4.7.3 redis == 4.5.4 flask_jwt_extended == 4.6.0 -cryptography == 46.0.1 +cryptography == 50.0.0 rfc3987 == 1.3.8 opentelemetry-instrumentation == 0.61b0 opentelemetry-instrumentation-flask == 0.61b0 @@ -21,4 +21,4 @@ flask_executor == 1.0.0 werkzeug == 3.1.3 gunicorn == 23.0.0 packaging == 24.0 -starlette == 0.52.1 \ No newline at end of file +starlette == 1.3.1 \ No newline at end of file diff --git a/services/TS29222_CAPIF_Events_API/requirements.txt b/services/TS29222_CAPIF_Events_API/requirements.txt index a9d508a..3dc0797 100644 --- a/services/TS29222_CAPIF_Events_API/requirements.txt +++ b/services/TS29222_CAPIF_Events_API/requirements.txt @@ -6,7 +6,7 @@ Flask == 3.0.3 pymongo == 4.7.3 redis == 4.5.4 flask_jwt_extended == 4.6.0 -cryptography == 46.0.1 +cryptography == 50.0.0 rfc3987 == 1.3.8 opentelemetry-instrumentation == 0.61b0 opentelemetry-instrumentation-flask == 0.61b0 @@ -22,7 +22,7 @@ Flask-APScheduler == 1.13.1 aiohttp == 3.10.5 async-timeout == 4.0.3 werkzeug == 3.1.3 -pyopenssl == 25.3.0 +pyopenssl == 26.4.0 gunicorn == 23.0.0 packaging == 24.0 -starlette == 0.52.1 \ No newline at end of file +starlette == 1.3.1 \ No newline at end of file diff --git a/services/TS29222_CAPIF_Logging_API_Invocation_API/requirements.txt b/services/TS29222_CAPIF_Logging_API_Invocation_API/requirements.txt index d429a19..256d4b5 100644 --- a/services/TS29222_CAPIF_Logging_API_Invocation_API/requirements.txt +++ b/services/TS29222_CAPIF_Logging_API_Invocation_API/requirements.txt @@ -6,7 +6,7 @@ Flask == 3.0.3 pymongo == 4.7.3 elasticsearch == 8.4.3 flask_jwt_extended == 4.6.0 -cryptography == 46.0.1 +cryptography == 50.0.0 redis == 4.5.4 opentelemetry-instrumentation == 0.61b0 opentelemetry-instrumentation-flask == 0.61b0 @@ -19,7 +19,7 @@ opentelemetry-api == 1.40.0 opentelemetry-sdk == 1.40.0 flask_executor == 1.0.0 werkzeug == 3.1.3 -pyopenssl == 25.3.0 +pyopenssl == 26.4.0 gunicorn == 23.0.0 packaging == 24.0 -starlette == 0.52.1 +starlette == 1.3.1 diff --git a/services/TS29222_CAPIF_Open_Discover_Service_API/requirements.txt b/services/TS29222_CAPIF_Open_Discover_Service_API/requirements.txt index 8e0a185..960138c 100644 --- a/services/TS29222_CAPIF_Open_Discover_Service_API/requirements.txt +++ b/services/TS29222_CAPIF_Open_Discover_Service_API/requirements.txt @@ -5,7 +5,7 @@ setuptools == 80.9.0 Flask == 3.0.3 pymongo == 4.7.3 flask_jwt_extended == 4.6.0 -cryptography == 46.0.1 +cryptography == 50.0.0 rfc3987 == 1.3.8 werkzeug == 3.1.3 gunicorn == 23.0.0 diff --git a/services/TS29222_CAPIF_Publish_Service_API/requirements.txt b/services/TS29222_CAPIF_Publish_Service_API/requirements.txt index 9d89531..c717a06 100644 --- a/services/TS29222_CAPIF_Publish_Service_API/requirements.txt +++ b/services/TS29222_CAPIF_Publish_Service_API/requirements.txt @@ -6,7 +6,7 @@ Flask == 3.0.3 pymongo == 4.7.3 redis == 4.5.4 flask_jwt_extended == 4.6.0 -cryptography == 46.0.1 +cryptography == 50.0.0 opentelemetry-instrumentation == 0.61b0 opentelemetry-instrumentation-flask == 0.61b0 opentelemetry-instrumentation-redis == 0.61b0 @@ -22,5 +22,5 @@ werkzeug == 3.1.3 gunicorn == 23.0.0 packaging == 24.0 requests == 2.32.2 -pyopenssl == 25.3.0 -starlette == 0.52.1 +pyopenssl == 26.4.0 +starlette == 1.3.1 diff --git a/services/TS29222_CAPIF_Routing_Info_API/requirements.txt b/services/TS29222_CAPIF_Routing_Info_API/requirements.txt index 35249f4..4b15236 100644 --- a/services/TS29222_CAPIF_Routing_Info_API/requirements.txt +++ b/services/TS29222_CAPIF_Routing_Info_API/requirements.txt @@ -6,4 +6,4 @@ Flask == 3.0.3 werkzeug == 3.1.3 gunicorn == 23.0.0 packaging == 24.0 -starlette == 0.52.1 +starlette == 1.3.1 diff --git a/services/TS29222_CAPIF_Security_API/requirements.txt b/services/TS29222_CAPIF_Security_API/requirements.txt index 51a08b2..64499e9 100644 --- a/services/TS29222_CAPIF_Security_API/requirements.txt +++ b/services/TS29222_CAPIF_Security_API/requirements.txt @@ -6,7 +6,7 @@ Flask == 3.0.3 pymongo == 4.7.3 redis == 4.5.4 flask_jwt_extended == 4.6.0 -cryptography == 46.0.1 +cryptography == 50.0.0 rfc3987 == 1.3.8 opentelemetry-instrumentation == 0.61b0 opentelemetry-instrumentation-flask == 0.61b0 @@ -22,4 +22,4 @@ flask_executor == 1.0.0 werkzeug == 3.1.3 gunicorn == 23.0.0 packaging == 24.0 -starlette == 0.52.1 \ No newline at end of file +starlette == 1.3.1 \ No newline at end of file diff --git a/services/helper/helper_service/app.py b/services/helper/helper_service/app.py index 63ec576..4041e70 100644 --- a/services/helper/helper_service/app.py +++ b/services/helper/helper_service/app.py @@ -10,8 +10,10 @@ from asgiref.wsgi import WsgiToAsgi from config import Config from db.db import get_mongo from flask import Flask -from OpenSSL.crypto import (FILETYPE_PEM, TYPE_RSA, PKey, X509Req, - dump_certificate_request, dump_privatekey) +from cryptography import x509 +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.x509.oid import NameOID # --- Paths setup: make 'services' discoverable so "import api..." works --- BASE_DIR = Path(__file__).resolve().parent @@ -40,21 +42,24 @@ logging.basicConfig(level=numeric_level) logger = logging.getLogger(__name__) # Create a superadmin CSR and keys -key = PKey() -key.generate_key(TYPE_RSA, 2048) -req = X509Req() -req.get_subject().O = 'OCF helper' -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') - -csr_request = dump_certificate_request(FILETYPE_PEM, req) -private_key = dump_privatekey(FILETYPE_PEM, key) +key = rsa.generate_private_key(public_exponent=65537, key_size=2048) +subject = x509.Name([ + x509.NameAttribute(NameOID.ORGANIZATION_NAME, 'OCF helper'), + x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, 'helper'), + x509.NameAttribute(NameOID.LOCALITY_NAME, 'Madrid'), + x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, 'Madrid'), + x509.NameAttribute(NameOID.COUNTRY_NAME, 'ES'), + # x509.NameAttribute(NameOID.COMMON_NAME, "superadmin{}".format(os.getenv("CAPIF_HOSTNAME"))), + x509.NameAttribute(NameOID.EMAIL_ADDRESS, 'helper@tid.es') +]) +req = x509.CertificateSigningRequestBuilder().subject_name(subject).sign(key, hashes.SHA256()) + +csr_request = req.public_bytes(serialization.Encoding.PEM) +private_key = key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption() +) # Save superadmin private key CERTS_DIR = Path(__file__).resolve().parent / "certs" diff --git a/services/helper/requirements.txt b/services/helper/requirements.txt index c2945f6..04508d3 100644 --- a/services/helper/requirements.txt +++ b/services/helper/requirements.txt @@ -15,4 +15,5 @@ gunicorn == 23.0.0 uvicorn == 0.34.2 asgiref == 3.8.1 pymongo == 4.7.3 -pyopenssl == 25.3.0 +cryptography == 50.0.0 +pyopenssl == 26.4.0 diff --git a/services/register/register_service/app.py b/services/register/register_service/app.py index 839b3fd..7bc93e4 100644 --- a/services/register/register_service/app.py +++ b/services/register/register_service/app.py @@ -10,8 +10,10 @@ from controllers.register_controller import register_routes from db.db import MongoDatabse from flask import Flask from flask_jwt_extended import JWTManager -from OpenSSL.crypto import (FILETYPE_PEM, TYPE_RSA, PKey, X509Req, - dump_certificate_request, dump_privatekey) +from cryptography import x509 +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.x509.oid import NameOID from utils.auth_utils import hash_password app = Flask(__name__) @@ -31,20 +33,23 @@ log_level = os.getenv('LOG_LEVEL', 'INFO').upper() numeric_level = getattr(logging, log_level, logging.INFO) # Create a superadmin CSR and keys -key = PKey() -key.generate_key(TYPE_RSA, 2048) -req = X509Req() -req.get_subject().O = 'Telefonica I+D' -req.get_subject().OU = 'Innovation' -req.get_subject().L = 'Madrid' -req.get_subject().ST = 'Madrid' -req.get_subject().C = 'ES' -req.get_subject().emailAddress = 'inno@tid.es' -req.set_pubkey(key) -req.sign(key, 'sha256') - -csr_request = dump_certificate_request(FILETYPE_PEM, req) -private_key = dump_privatekey(FILETYPE_PEM, key) +key = rsa.generate_private_key(public_exponent=65537, key_size=2048) +subject = x509.Name([ + x509.NameAttribute(NameOID.ORGANIZATION_NAME, 'Telefonica I+D'), + x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, 'Innovation'), + x509.NameAttribute(NameOID.LOCALITY_NAME, 'Madrid'), + x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, 'Madrid'), + x509.NameAttribute(NameOID.COUNTRY_NAME, 'ES'), + x509.NameAttribute(NameOID.EMAIL_ADDRESS, 'inno@tid.es') +]) +req = x509.CertificateSigningRequestBuilder().subject_name(subject).sign(key, hashes.SHA256()) + +csr_request = req.public_bytes(serialization.Encoding.PEM) +private_key = key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption() +) # Save superadmin private key key_file = open("certs/superadmin.key", 'wb+') diff --git a/services/register/requirements.txt b/services/register/requirements.txt index 2d5ea17..c53545c 100644 --- a/services/register/requirements.txt +++ b/services/register/requirements.txt @@ -3,7 +3,8 @@ setuptools == 80.9.0 Flask == 3.0.3 pymongo == 4.7.3 flask_jwt_extended == 4.6.0 -pyopenssl == 25.3.0 +cryptography == 50.0.0 +pyopenssl == 26.4.0 pyyaml == 6.0.1 requests == 2.32.2 bcrypt == 4.3.0 diff --git a/tests/libraries/helpers.py b/tests/libraries/helpers.py index eac2594..e739347 100644 --- a/tests/libraries/helpers.py +++ b/tests/libraries/helpers.py @@ -1,8 +1,9 @@ import re from urllib.parse import urlparse -from OpenSSL.crypto import (dump_certificate_request, dump_privatekey, - PKey, TYPE_RSA, X509Req) -from OpenSSL.SSL import FILETYPE_PEM +from cryptography import x509 +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.x509.oid import NameOID import socket import copy import pickle @@ -70,28 +71,30 @@ def add_dns_to_hosts(ip_address, host_name): def create_csr(csr_file_path, private_key_path, cn): # create public/private key - key = PKey() - key.generate_key(TYPE_RSA, 2048) + key = rsa.generate_private_key(public_exponent=65537, key_size=2048) # Generate CSR - req = X509Req() - req.get_subject().CN = cn - req.get_subject().O = 'Telefonica I+D' - req.get_subject().OU = 'Innovation' - req.get_subject().L = 'Madrid' - req.get_subject().ST = 'Madrid' - req.get_subject().C = 'ES' - req.get_subject().emailAddress = 'inno@tid.es' - req.set_pubkey(key) - req.sign(key, 'sha256') + subject = x509.Name([ + x509.NameAttribute(NameOID.COMMON_NAME, cn), + x509.NameAttribute(NameOID.ORGANIZATION_NAME, 'Telefonica I+D'), + x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, 'Innovation'), + x509.NameAttribute(NameOID.LOCALITY_NAME, 'Madrid'), + x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, 'Madrid'), + x509.NameAttribute(NameOID.COUNTRY_NAME, 'ES'), + x509.NameAttribute(NameOID.EMAIL_ADDRESS, 'inno@tid.es') + ]) + req = x509.CertificateSigningRequestBuilder().subject_name(subject).sign(key, hashes.SHA256()) + + csr_request = req.public_bytes(serialization.Encoding.PEM) with open(csr_file_path, 'wb+') as f: - f.write(dump_certificate_request(FILETYPE_PEM, req)) - f.close() - csr_request = dump_certificate_request(FILETYPE_PEM, req) + f.write(csr_request) with open(private_key_path, 'wb+') as f: - f.write(dump_privatekey(FILETYPE_PEM, key)) - f.close() + f.write(key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption() + )) return csr_request diff --git a/tools/robot/basicRequirements.txt b/tools/robot/basicRequirements.txt index d6b316f..1612c1a 100644 --- a/tools/robot/basicRequirements.txt +++ b/tools/robot/basicRequirements.txt @@ -17,7 +17,7 @@ click == 8.1.7 configparser == 5.3.0 cookiecutter == 2.1.1 coverage == 4.5.4 -cryptography == 42.0.8 +cryptography == 50.0.0 Deprecated == 1.2.13 distlib == 0.3.6 dnspython == 2.2.1 @@ -55,7 +55,7 @@ PyGithub == 1.56 PyJWT == 2.6.0 pymongo == 4.7.3 PyNaCl == 1.5.0 -pyOpenSSL == 25.3.0 +pyopenssl == 26.4.0 pyparsing == 3.0.9 PySocks == 1.7.1 pytest == 6.2.4 -- GitLab