diff --git a/README.md b/README.md index 1154502fb95cc8176dfff8ab054c0e2666c71d6b..df44a94f30c4a9e00555d156d55af927eae38cb4 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ OpenCAPIF SDK provides a set of libraries to enable either CAPIF provider and in Current version of OpenCAPIF SDK is compatible with following publicly available releases: - [OpenCAPIF Release 1.0](https://ocf.etsi.org/documentation/v1.0.0-release/) -- OpenCAPIF Release 2.0 +- [OpenCAPIF Release 2.0](https://ocf.etsi.org/documentation/v2.0.0-release/) This document serves as the [main bootstrap reference](#networkapp-developer-path) to start working with OpenCAPIF SDK. For advanced users, refer to [OpenCAPIF full documentation](./doc/sdk_full_documentation.md) section to dig into all available features. @@ -126,6 +126,8 @@ To install the OpenCAPIF SDK source code for developing purposes there is an ava To use the SDK, binary installer for the latest version is available at the [Python Package Index (Pipy)](https://pypi.org/project/opencapif-sdk/) +The SDK works with **Python 3.12** + ```console pip install opencapif_sdk ``` @@ -273,7 +275,6 @@ Code is next explained step by step: 5. **Retrieve security tokens:** \ Use the `get_tokens()` method to obtain the necessary tokens for authenticating API requests. - **At the end of this flow, the invoker has been onboarded and it is ready to use target APIs.** All required information, including the access_token to use the available APIs, is stored at `capif_api_security_context_details.json` file. This file is placed in the invoker_folder path, specifically in the folder that corresponds to the capif_username used in the `capif_sdk_config.json`. A sample of the [capif_api_security_context_details](./samples/capif_api_security_context_details_sample.json) is also available. diff --git a/doc/sdk_configuration.md b/doc/sdk_configuration.md index 568e41cb162bfe694c51134c4c4da5904d56e0ca..4a46a83ff79cc03de67b002863fb032c4925d645 100644 --- a/doc/sdk_configuration.md +++ b/doc/sdk_configuration.md @@ -81,7 +81,7 @@ This file can also be populated using [environment variables](../samples/envirom - `invoker_folder`: The path (relative or absolute) where invoker information (certificates, keys, etc.) is stored. - `provider_folder`: The path (relative or absolute) where provider information is stored. -- `supported_features`: A string used to indicate the features supported by an API. The string shall contain a bitmask indicating supported features in hexadecimal representation Each character in the string shall take a value of "0" to "9", "a" to "f" or "A" to "F". [More information](https://github.com/jdegre/5GC_APIs/blob/Rel-18/TS29571_CommonData.yaml) +- `supported_features`: A string used to indicate the features supported by an API, invoker or provider. The string shall contain a bitmask indicating supported features in hexadecimal representation Each character in the string shall take a value of "0" to "9", "a" to "f" or "A" to "F". [More information](https://github.com/jdegre/5GC_APIs/blob/Rel-18/TS29571_CommonData.yaml) - `capif_host`: The domain name of the CAPIF host. - `register_host`: The domain name of the register host. - `capif_https_port`: The CAPIF host port number. diff --git a/opencapif_sdk/capif_event_feature.py b/opencapif_sdk/capif_event_feature.py index 32d0bf35938c3570f701774b646438da7b73c5a6..90b4664efaca5c31bc547b71ff361c710444796a 100644 --- a/opencapif_sdk/capif_event_feature.py +++ b/opencapif_sdk/capif_event_feature.py @@ -1,19 +1,8 @@ -from opencapif_sdk import capif_invoker_connector,capif_provider_connector +from opencapif_sdk import capif_invoker_connector, capif_provider_connector import os import logging -import shutil -from requests.auth import HTTPBasicAuth import urllib3 -from OpenSSL.SSL import FILETYPE_PEM -from OpenSSL.crypto import ( - dump_certificate_request, - dump_privatekey, - PKey, - TYPE_RSA, - X509Req -) import requests -import json import warnings from requests.exceptions import RequestsDependencyWarning urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) @@ -61,7 +50,7 @@ class capif_invoker_event_feature(capif_invoker_connector): }, "supportedFeatures": f"{self.supported_features}" } - + try: response = requests.post( url=path, @@ -219,15 +208,15 @@ class capif_invoker_event_feature(capif_invoker_connector): class capif_provider_event_feature(capif_provider_connector): - + def create_subscription(self, name, id): subscriberId = id path = self.capif_https_url + f"capif-events/v1/{subscriberId}/subscriptions" - + list_of_ids = self._load_provider_api_details() - + number = self._find_key_by_value(list_of_ids, id) payload = { @@ -239,14 +228,14 @@ class capif_provider_event_feature(capif_provider_connector): "websockNotifConfig": self.websock_notif_config, "supportedFeatures": f"{self.supported_features}" } - + number_low = number.lower() - + cert = ( os.path.join(self.provider_folder, f"{number_low}.crt"), os.path.join(self.provider_folder, f"{number}_private_key.key"), ) - + try: response = requests.post( url=path, @@ -256,7 +245,7 @@ class capif_provider_event_feature(capif_provider_connector): verify=os.path.join(self.provider_folder, "ca.crt") ) response.raise_for_status() - + location_header = response.headers.get("Location") if location_header: @@ -296,7 +285,7 @@ class capif_provider_event_feature(capif_provider_connector): except Exception as e: self.logger.error("Unexpected error: %s", e) return None, {"error": f"Unexpected error: {e}"} - + def delete_subscription(self, name, id): subscriberId = id @@ -312,13 +301,13 @@ class capif_provider_event_feature(capif_provider_connector): # Attempt to delete the subscription from CAPIF delete_path = self.capif_https_url + f"capif-events/v1/{subscriberId}/subscriptions/{identifier}" - + list_of_ids = self._load_provider_api_details() number = self._find_key_by_value(list_of_ids, id) - + number_low = number.lower() - + cert = ( os.path.join(self.provider_folder, f"{number_low}.crt"), os.path.join(self.provider_folder, f"{number}_private_key.key"), @@ -355,15 +344,15 @@ class capif_provider_event_feature(capif_provider_connector): else: self.logger.error("Subscription file not found at path: %s", path) return None, {"error": "Subscription file not found"} - + def update_subcription(self, name, id): - + subscriberId = id - + path = os.path.join(self.provider_folder, "capif_subscriptions_id.json") - + list_of_ids = self._load_provider_api_details() - + number = self._find_key_by_value(list_of_ids, id) payload = { @@ -375,7 +364,7 @@ class capif_provider_event_feature(capif_provider_connector): "websockNotifConfig": self.websock_notif_config, "supportedFeatures": f"{self.supported_features}" } - + if os.path.exists(path): subscription = self._load_config_file(path) if not isinstance(subscription, dict): @@ -386,13 +375,13 @@ class capif_provider_event_feature(capif_provider_connector): # Attempt to delete the subscription from CAPIF put_path = self.capif_https_url + f"capif-events/v1/{subscriberId}/subscriptions/{identifier}" - + list_of_ids = self._load_provider_api_details() number = self._find_key_by_value(list_of_ids, id) - + number_low = number.lower() - + cert = ( os.path.join(self.provider_folder, f"{number_low}.crt"), os.path.join(self.provider_folder, f"{number}_private_key.key"), @@ -432,4 +421,4 @@ class capif_provider_event_feature(capif_provider_connector): return None, {"error": "Subscription file not found"} def patch_subcription(self, name, id): - self.update_subcription(self, name, id) \ No newline at end of file + self.update_subcription(self, name, id) diff --git a/opencapif_sdk/capif_invoker_connector.py b/opencapif_sdk/capif_invoker_connector.py index 5ebdec38da13bd30895077d50948279b608b1d8d..7a3753c0f5ca4daf2c8433cbe1a105aa8fd662f6 100644 --- a/opencapif_sdk/capif_invoker_connector.py +++ b/opencapif_sdk/capif_invoker_connector.py @@ -42,6 +42,7 @@ class capif_invoker_connector: """ Τhis class is responsbile for onboarding an Invoker (ex. a Invoker) to CAPIF """ + def __init__(self, config_file: str): config_file = os.path.abspath(config_file) @@ -80,14 +81,14 @@ class capif_invoker_connector: capif_register_port = str(os.getenv('CAPIF_REGISTER_PORT', config.get('capif_register_port', '')).strip()) capif_username = os.getenv('CAPIF_USERNAME', config.get('capif_username', '')).strip() capif_invoker_password = os.getenv('CAPIF_PASSWORD', config.get('capif_password', '')).strip() - + capif_callback_url = os.getenv('INVOKER_CAPIF_CALLBACK_URL', invoker_config.get('capif_callback_url', '')).strip() supported_features = os.getenv('INVOKER_SUPPORTED_FEATURES', invoker_config.get('supported_features', '')).strip() check_authentication_data = invoker_config.get('check_authentication_data', {}) self.check_authentication = { "ip": os.getenv('INVOKER_CHECK_AUTHENTICATION_DATA_IP', check_authentication_data.get('ip', '')).strip(), "port": os.getenv('INVOKER_CHECK_AUTHENTICATION_DATA_PORT', check_authentication_data.get('port', '')).strip() - } + } # Extract CSR configuration from the JSON csr_config = invoker_config.get('cert_generation', {}) @@ -109,7 +110,7 @@ class capif_invoker_connector: os.makedirs(self.invoker_folder, exist_ok=True) if supported_features is None: supported_features = 0 - self.supported_features = supported_features + self.supported_features = supported_features # Configure URLs for CAPIF HTTPS and register services if len(capif_https_port) == 0 or int(capif_https_port) == 443: @@ -144,17 +145,17 @@ class capif_invoker_connector: ) if os.path.exists(path): self.invoker_capif_details = self.__load_invoker_api_details() - + self.signed_key_crt_path = os.path.join( - self.invoker_folder, - self.capif_username + ".crt" - ) - + self.invoker_folder, + self.capif_username + ".crt" + ) + self.private_key_path = os.path.join( - self.invoker_folder, - "private.key" - ) - + self.invoker_folder, + "private.key" + ) + self.pathca = os.path.join(self.invoker_folder, "ca.crt") self.logger.info("capif_invoker_connector initialized with the JSON parameters") @@ -465,7 +466,7 @@ class capif_invoker_connector: self.logger.error( f"Error during updating Invoker to CAPIF: {e} - Response: {response.text}") raise - + def _create_or_update_file(self, file_name, file_type, content, mode="w"): """ Create or update a file with the specified content. @@ -500,7 +501,7 @@ class capif_invoker_connector: # Open the file in the specified mode with open(full_path, mode, encoding="utf-8") as file: file.write(content) - + # Log success based on the mode if mode == "w": self.logger.info(f"File '{full_file_name}' created or overwritten successfully.") @@ -509,6 +510,3 @@ class capif_invoker_connector: except Exception as e: self.logger.error(f"Error handling the file '{full_file_name}': {e}") raise - - - diff --git a/opencapif_sdk/capif_logging_feature.py b/opencapif_sdk/capif_logging_feature.py index 0ea2f27a957f5c0255211c2034476201a433aa08..8c8c2c5c58f3d9219331aed42c8ad516cef13544 100644 --- a/opencapif_sdk/capif_logging_feature.py +++ b/opencapif_sdk/capif_logging_feature.py @@ -196,9 +196,9 @@ class capif_logging_feature: raise ValueError(f"No ID was found for the API '{name}'.") def create_logs(self, aefId, jwt): - + api_invoker_id = self._decrypt_jwt(jwt) - + path = self.capif_https_url + f"/api-invocation-logs/v1/{aefId}/logs" log_entry = { @@ -211,7 +211,7 @@ class capif_logging_feature: "operation": self.log["operation"], "result": self.log["result"] } - + payload = { "aefId": f"{aefId}", "apiInvokerId": f"{api_invoker_id}", @@ -291,7 +291,7 @@ class capif_logging_feature: self.logger.warning( f"Configuration file {config_file} not found. Using defaults or environment variables.") return {} - + def _decrypt_jwt(self, jwt_token): """ Decrypts the given JWT using the provided certificate. @@ -327,4 +327,3 @@ class capif_logging_feature: except Exception as e: raise Exception(f"An error occurred while decrypting the JWT: {e}") - diff --git a/opencapif_sdk/capif_provider_connector.py b/opencapif_sdk/capif_provider_connector.py index 1276713c23422309eed41fd78d02f70383b84d61..58437f1b5c9e306ab121a04a176bbda440980a2e 100644 --- a/opencapif_sdk/capif_provider_connector.py +++ b/opencapif_sdk/capif_provider_connector.py @@ -13,7 +13,6 @@ from OpenSSL.SSL import FILETYPE_PEM import os import logging import shutil -import subprocess from requests.auth import HTTPBasicAuth import urllib3 import ssl @@ -605,7 +604,6 @@ class capif_provider_connector: publish = self.publish_req api_id = "/" + publish["service_api_id"] APF_api_prov_func_id = publish["publisher_apf_id"] - AEFs_list = publish["publisher_aefs_ids"] apf_number = None for key, value in provider_details.items(): if value == APF_api_prov_func_id and key.startswith("APF-"): @@ -626,8 +624,7 @@ class capif_provider_connector: cert = ( os.path.join(self.provider_folder, f"apf-{apf_number}.crt"), - os.path.join(self.provider_folder, - f"APF-{apf_number}_private_key.key"), + os.path.join(self.provider_folder, f"APF-{apf_number}_private_key.key"), ) self.logger.info(f"Unpublishing service to URL: {url}") @@ -784,7 +781,7 @@ class capif_provider_connector: """ self.logger.info("Starting the service publication process") - # Load provider details + # Load provider details provider_details_path = os.path.join( self.provider_folder, "provider_capif_ids.json") self.logger.info( @@ -1149,7 +1146,7 @@ class capif_provider_connector: ccf_publish_url = capif_postauth_info["ccf_publish_url"] onboarding_response = self.update_onboard( capif_onboarding_url, access_token) - + capif_registration_id = onboarding_response["apiProvDomId"] self.__write_to_file( onboarding_response, capif_registration_id, ccf_publish_url diff --git a/opencapif_sdk/service_discoverer.py b/opencapif_sdk/service_discoverer.py index 6089d3d70ab46af80dea1ee398ad72d80ce55ea2..5a0915692dc24a26d6a58dc580683eb9db4f8b71 100644 --- a/opencapif_sdk/service_discoverer.py +++ b/opencapif_sdk/service_discoverer.py @@ -31,7 +31,6 @@ logging.basicConfig( ) - class service_discoverer: class ServiceDiscovererException(Exception): pass @@ -69,7 +68,7 @@ class service_discoverer: # Retrieve host and port information from environment variables or config capif_host = os.getenv('CAPIF_HOST', config.get('capif_host', '')).strip() capif_https_port = str(os.getenv('CAPIF_HTTPS_PORT', config.get('capif_https_port', '')).strip()) - + # Get the folder for storing invoker certificates from environment or config invoker_config = config.get('invoker', {}) invoker_general_folder = os.path.abspath( @@ -81,7 +80,7 @@ class service_discoverer: self.check_authentication_data = { "ip": os.getenv('INVOKER_CHECK_AUTHENTICATION_DATA_IP', check_authentication_data.get('ip', '')).strip(), "port": os.getenv('INVOKER_CHECK_AUTHENTICATION_DATA_PORT', check_authentication_data.get('port', '')).strip() - } + } # Retrieve CAPIF invoker username capif_invoker_username = os.getenv('CAPIF_USERNAME', config.get('capif_username', '')).strip() @@ -122,7 +121,7 @@ class service_discoverer: try: self.token = self.invoker_capif_details["access_token"] - except : + except: pass # Define paths for certificates, private keys, and CA root @@ -218,7 +217,7 @@ class service_discoverer: number_of_apis = len( self.invoker_capif_details["registered_security_contexes"]) - + for i in range(0, number_of_apis): # Obtaining the values of api_id and aef_id for each API api_id = self.invoker_capif_details["registered_security_contexes"][i]['api_id'] @@ -233,11 +232,11 @@ class service_discoverer: } payload["securityInfo"].append(security_info) try: - response = requests.post(url, - json=payload, - cert=(self.signed_key_crt_path, - self.private_key_path), - verify=self.ca_root_path) + response = requests.post( + url, + json=payload, + cert=(self.signed_key_crt_path, self.private_key_path), + verify=self.ca_root_path) response.raise_for_status() self.logger.info("Security context correctly updated") @@ -468,7 +467,7 @@ class service_discoverer: self.invoker_capif_details["registered_security_contexes"] = self.convert_keys_to_snake_case(endpoints["serviceAPIDescriptions"]) self.save_api_details() - + def convert_keys_to_snake_case(self, data): if isinstance(data, dict): new_dict = {} @@ -480,11 +479,11 @@ class service_discoverer: return [self.convert_keys_to_snake_case(item) if isinstance(item, (dict, list)) else item for item in data] else: return data - + def to_snake_case(self, camel_case_str): # Convertir CamelCase a snake_case return re.sub(r'(?<!^)(?=[A-Z])', '_', camel_case_str).lower() - + def save_api_details(self): try: # Define the path to save the details @@ -511,27 +510,27 @@ class service_discoverer: invoker_id = invoker_details["api_invoker_id"] check_auth = self.check_authentication_data url = "http://"+f"{check_auth['ip']}:{check_auth['port']}/" + "aef-security/v1/check-authentication" - + payload = { "apiInvokerId": f"{invoker_id}", "supportedFeatures": f"{supported_features}" } - + headers = { "Authorization": "Bearer {}".format(self.token), "Content-Type": "application/json", } - + response = requests.request( "POST", url, headers=headers, - json=payload + json=payload ) - + response.raise_for_status() self.logger.info("Authentication of supported_features checked") - + except Exception as e: self.logger.error( f"Error during checking Invoker supported_features : {e} - Response: {response.text}")