diff --git a/helm/scripts/run_remote_capif_tests.sh b/helm/scripts/run_remote_capif_tests.sh index c1f296147272ad888fd9936b41c76deecd3a92bf..e2b03a1c0b08d055a7790d5b9f76b40c0cfd4371 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/TS29222_CAPIF_API_Invoker_Management_API/requirements.txt b/services/TS29222_CAPIF_API_Invoker_Management_API/requirements.txt index d2bc7d53b5eff749bf02e349321b0336033e5c97..077b62702f83a4bdf5c6f62a97755c2feb836bba 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 7a55a99e4cc3bf33864603f99f88fcccf94ebeb3..8cc595329ae55e592660b5a988f59d161e8bdc00 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 c5676ce9420b155dcd08509498a97fd704c4264d..becef42a9a827f43442caa384663109f68ec47ce 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 95d1a60e52959190b472414dacb81cd96e50a3fa..c282005a68e10a8c6ef97cfa89095f310ca28275 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 7a55a99e4cc3bf33864603f99f88fcccf94ebeb3..8cc595329ae55e592660b5a988f59d161e8bdc00 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 a9d508a0ae65d5859c8dc7b2652559dc863c6bec..3dc079773380e56fd50d21e1cf163dbf82e270ae 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 d429a19627becb4df9bddfcf1e5baac1a96a6ae7..256d4b51b6746bbcfbfcf2e4a71ceee117afb109 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 8e0a1857ee3e5412551d2b1a5867520013441c88..960138cd8b56cefe940168feaee3e7553c7ee81c 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/published_apis/core/serviceapidescriptions.py b/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/serviceapidescriptions.py index ba71ce639c1a5581ece70060b40eaa053b821359..45486dfd49b3b123c910f86814664a9e17b8a4b4 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,47 +197,56 @@ 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'] - serviceapidescription_dict['ccf_id'] = ccf_id serviceapidescription_dict['shareable_info'] = {"is_shareable": False} 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 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: + rec.update({"pub_api_path": {"ccf_ids": [interconnected_ccf.get("ccf_id")]}}) + else: + ccf_list = rec.get("pub_api_path").get("ccf_ids") + ccf_list.append(interconnected_ccf.get("ccf_id")) + rec.update({"pub_api_path": {"ccf_ids": ccf_list}}) + else: rec['shareable_info']['capif_prov_doms'].remove(dom) + if "CCF" in apf_id: + if rec.get("pub_api_path") is None: + rec.update({"pub_api_path": {"ccf_ids": [apf_id]}}) + else: + ccf_list = rec.get("pub_api_path").get("ccf_ids") + ccf_list.append(apf_id) + rec.update({"pub_api_path": {"ccf_ids": ccf_list}}) 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") + } 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/TS29222_CAPIF_Publish_Service_API/requirements.txt b/services/TS29222_CAPIF_Publish_Service_API/requirements.txt index 9d895319702a59ecc62a7645798fdd0669857337..c717a062d96d97ba63b743a01a7f1f42c52d6505 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 35249f471b4933e794d8c772d9f10e8cb66c1a6b..4b1523609684e58ad58613bb6b1bf1a23c9e0439 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 51a08b23cbd854d09c1bc8d90877dd99ce941a21..64499e90c3b8c595441b39611d3f7fc995732ce3 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/docker-compose-capif.yml b/services/docker-compose-capif.yml index 50e2c5db8c4bd1e29d22c76ac76a3e268f663cd3..c10683de61448c43fa91573efec9b9d5e90f20d5 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} @@ -272,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/app.py b/services/helper/helper_service/app.py index 63ec576b8f8f066fbf0f69e2578b8ba9a9c31d01..4041e704ce1c24da0a8ca146beb8122e9daba8b5 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/helper_service/openapi_helper_interconnection.yaml b/services/helper/helper_service/openapi_helper_interconnection.yaml index d038ec151423ae4b78ea3de880d57b02ddf5e624..b0ae2e29b903cab3ac32779654f09b6804da1143 100644 --- a/services/helper/helper_service/openapi_helper_interconnection.yaml +++ b/services/helper/helper_service/openapi_helper_interconnection.yaml @@ -11,7 +11,45 @@ servers: description: Base URL of the Helper service. # 1. PATHS: Where you define your endpoints paths: - /interconnect: + /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 @@ -66,10 +104,166 @@ 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}: + 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 + 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 +315,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 9a2d6d89dade409cdb96223ea0d124b63ae1543b..cd279f5d9691b83c19f11790d8f9a0002691b1cd 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,48 @@ 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 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 @@ -50,3 +92,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 = ccf_operations.sync_ccfinstance(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 08263f50c627342e6e3f5605227865dede38ed11..8f520c219f28d9eed5435fb74950d06dc83b6039 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,18 +37,46 @@ class CapifDomainOperations(Resource): Resource.__init__(self) self.auth_manager = AuthManager() 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") - current_app.logger.debug(capifdomaindetails) interconnected_col = self.db.get_col_by_name(self.db.interconnected) interconnected_ccf = interconnected_col.find_one({"dst_prov_dom": capifdomaindetails.dst_prov_dom}) if interconnected_ccf: - current_app.logger.debug("CAPIF domain already interconnected : {}".format(capifdomaindetails.dst_prov_dom)) return make_response("CAPIF domain already interconnected", 409) - url = "https://{}/helper/interconnection/connect".format(capifdomaindetails.dst_prov_dom) + url = "https://{}/helper/interconnection/establish".format(capifdomaindetails.dst_prov_dom) with open('certs/ca.crt', 'rb') as ca_cert: server_ca = ca_cert.read() @@ -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": os.getenv("CAPIF_HOSTNAME"), + "ccfId": local_ccf_id, + "dstProvDom": local_dom, "publicKey": server_pub.decode("utf-8") }) headers = { @@ -77,7 +105,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,11 +112,11 @@ 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) + # 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) @@ -101,3 +128,58 @@ 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 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.""" + + result = services_col.update_many( + {"pub_api_path.ccf_ids": ccf_id}, + {"$pull": {"pub_api_path.ccf_ids": ccf_id}} + ) + + # 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}} + ) + + return result.modified_count + + def delete_capifdomain(self, ccf_id): + """Remove an interconnection request for a CCF.""" + current_app.logger.debug("Interconnection: Delete domain") + + interconnected_col = self.db.get_col_by_name(self.db.interconnected) + interconnected_ccf = interconnected_col.find_one({"ccf_id": ccf_id}) + if interconnected_ccf: + # 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): + 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: + 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 30130038f8c5d83c7894ac03e0c6f94e504520c4..ae5341b0f48197eadf238fce7ec2aab40aa09b54 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 @@ -27,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): @@ -37,7 +44,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() @@ -61,16 +67,173 @@ 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'] = os.getenv("CAPIF_HOSTNAME") - current_app.logger.debug(ccfinstancedetails_dict) + 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)) - 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 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}) + + 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 + + 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) + + published = 0 + # 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), + 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)) + + 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.""" + + result = services_col.update_many( + {"pub_api_path.ccf_ids": ccf_id}, + {"$pull": {"pub_api_path.ccf_ids": ccf_id}} + ) + + # 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}} + ) + + 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") + + 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: + res = not_found_error("CCF instance with ccf_id {} not found".format(ccf_id)) + + return res 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 4845c9b9bc36189ab7fedee021f9fb07f30c5417..828bf6756777a4fff110b01023322475350452ea 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 41dbf42c35d07e96dae8e9a1d469b93f1afa72b9..7d6f99805a3fc6a19b71be8d6c236ff8a77de2f7 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,100 @@ 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: + 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: @@ -122,6 +215,165 @@ 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}: + 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: + - 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/helper/requirements.txt b/services/helper/requirements.txt index c2945f6f4f2d96d4d3163d7f995c6974258d08b6..04508d3cd5e6c5d82a7fd3d09b5bb3077fc25a28 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/nginx/endpoints/endpoints.conf b/services/nginx/endpoints/endpoints.conf index ec6e78c238350033b60c7ec6fc507ab4a13a810b..c225b46b184f36e856c85e752578a1533b32d87c 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 98a5c566e973946c68557610f2883d3b45cc32a8..af0cd5392076c1b96fa187bdc908f9c1afaa96ee 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; diff --git a/services/register/config.yaml b/services/register/config.yaml index efe628888855ef05c69d8e8f58994421f79ad1ac..fb2d19fcbee89def12054d79e622e0abd8a8ffd9 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/services/register/register_service/app.py b/services/register/register_service/app.py index 839b3fd05699e758c5ee1551c59afc37a65bffad..7bc93e45bfd143643d019998b8b66b863cfa85ca 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 2d5ea17f369c7ebb077319fba46c9582651ad2a9..c53545cbb2c71223bbfec30c46771805cb3bfa08 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/services/run_capif_tests.sh b/services/run_capif_tests.sh index 968b1c567d1b315730e3568ff3f605015246c87e..d0502074b7349fd78ce8a76265134cfd6dcba404 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 diff --git a/tests/features/Helper/Interconnection API/interconnection.robot b/tests/features/Helper/Interconnection API/interconnection.robot index 49aefda2c4b8729eda3a898fe152ea073de3d4a9..06f27e36ec7c209a8c22fb041d82c183516aead7 100644 --- a/tests/features/Helper/Interconnection API/interconnection.robot +++ b/tests/features/Helper/Interconnection API/interconnection.robot @@ -24,11 +24,11 @@ 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 - ... /helper/interconnection/interconnect + ... /helper/interconnection/request ... server=${CAPIF_HTTPS_URL} ... verify=ca.crt ... username=${SUPERADMIN_USERNAME} @@ -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,11 +61,11 @@ 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 - ... /helper/interconnection/connect + ... /helper/interconnection/establish ... server=${CAPIF_HTTPS_URL} ... verify=ca.crt ... username=${CCF_USERNAME} @@ -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 diff --git a/tests/libraries/helpers.py b/tests/libraries/helpers.py index eac2594aa73c8a44aa185ff4eaa6b37d656e835d..e73934780e680f59d45c40d648cedacdf8cbb341 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 d6b316fa906fe0e2effe9d9e6d9cf06ce2e62439..1612c1a9b1b4256e8c13235a12f8846603e97d24 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