diff --git a/README.md b/README.md index dc9b11163b75a52363b85b6cc4681deedcf55514..337108ec29f7c4fb64b5fb5c0bc4d5253dd6ff60 100644 --- a/README.md +++ b/README.md @@ -95,8 +95,8 @@ OpenCAPIF SDK brings a set of functions to integrate with the 5G Core's function | /{apfId}/service-apis/{serviceApiId} (PUT) | [update_service()](./doc/sdk_full_documentation.md#services-update) | Updates the details of an existing service API for a specific `apfId`and `serviceApiId` | | /{apfId}/service-apis/{serviceApiId} (GET) | [get_service()](./doc/sdk_full_documentation.md#get-services) | Retrieves the details of a specific service API for a specific `apfId` and `serviceApiId` | | /{apfId}/service-apis (GET) | [get_all_services()](./doc/sdk_full_documentation.md#get-all-services) | Retrieves a list of all available service APIs for a specific `apfId` | -| /aef-security/v1/check-authentication (POST) | [check_authentication()](./doc/sdk_full_documentation.md#check_authentication) | This custom operation allows the API invoker to confirm the `supported_features` from the API exposing function(AEF) | -| /api-invocation-logs/v1/{aefId}/logs (POST) | [create_logs( aefId, api_invoker_id)](./doc/sdk_full_documentation.md#create_logs) | This operation allows to the Provider to notice to the CCF about the query of an invoker for an especific `aefId` +| /aef-security/v1/check-authentication (POST) | [check_authentication(supported_features)](./doc/sdk_full_documentation.md#check_authentication) | This custom operation allows the API invoker to confirm the `supported_features` from the API exposing function(AEF) | +| /api-invocation-logs/v1/{aefId}/logs (POST) | [create_logs(aefId, jwt)](./doc/sdk_full_documentation.md#create_logs) | This operation allows to the Provider to notice to the CCF about the query of an invoker with the JWT token recieved | /capif-events/v1/{subscriberId}/subscriptions (POST) | [create_subscription(name, id)](./doc/sdk_full_documentation.md#create_subscription) | This operation allows to the Invoker/AEF/APF/AMF to ask to the CCF about notifications related to certain functionalities. | /capif-events/v1/{subscriberId}/subscriptions/{subscriptionId} (DELETE) | [delete_subscription(name, id)](./doc/sdk_full_documentation.md#delete_subscription) | This operation allows to the Invoker/AEF/APF/AMF to withdraw the petition to receive notifications related to certain functionalities. | /capif-events/v1/{subscriberId}/subscriptions/{subscriptionId} (PUT) | [update_subscription(name, id)](./doc/sdk_full_documentation.md#update_subscription) | This operation allows to the Invoker/AEF/APF/AMF to modify to the petition to receive notifications related to certain functionalities. **ONLY AVAILABLE IN OPENCAPIF RELEASE 2** @@ -136,7 +136,7 @@ Here is a visual look on the variables of the CAPIF sdk referenced in: - [Important information for Invoker Consumer](#important-information-for-invoker-consumer) - [Important information for Provider Consumer](#important-information-for-provider-consumers) - + # Network App developer path @@ -326,3 +326,4 @@ There are some features which **are not currently available at latest OpenCAPIF - /trustedInvokers/{apiInvokerId}/delete (POST) - /trustedInvokers/{apiInvokerId} (GET) - /trustedInvokers/{apiInvokerId} (DELETE) + - Nontype Error: When using SDK as a Provider, if the user does update the provider to more AEFs/APFs than previously, the SDK has an error using the publish functionality diff --git a/doc/images/flows-data_schema.jpg b/doc/images/flows-data_schema.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9cafb7c1e6aaa02e0ef089ca20d7efbf4b33f323 Binary files /dev/null and b/doc/images/flows-data_schema.jpg differ diff --git a/doc/images/flows-data_schema.png b/doc/images/flows-data_schema.png deleted file mode 100644 index d2dbeda12ae9502a563b4a5a173c1569a72bc72e..0000000000000000000000000000000000000000 Binary files a/doc/images/flows-data_schema.png and /dev/null differ diff --git a/doc/sdk_configuration.md b/doc/sdk_configuration.md index 738d5948a2644b07e941e021c61a1d437b831be2..568e41cb162bfe694c51134c4c4da5904d56e0ca 100644 --- a/doc/sdk_configuration.md +++ b/doc/sdk_configuration.md @@ -98,6 +98,7 @@ This file can also be populated using [environment variables](../samples/envirom - `api_description_path`: The path to the [ServiceAPIDescription](https://github.com/jdegre/5GC_APIs/blob/Rel-18/TS29222_CAPIF_Publish_Service_API.yaml) JSON file. - `check_authentication_data`: The `ip` and `port` of the target Provider's AEF to get their supported features from. - `log`: The structure defined in the [Log schema](https://github.com/jdegre/5GC_APIs/blob/Rel-18/TS29222_CAPIF_Logging_API_Invocation_API.yaml), it is not needed to fulfill the `apiId` field. +- `events`: The structure defined in the [EventSubscription schema](https://github.com/jdegre/5GC_APIs/blob/Rel-18/TS29222_CAPIF_Events_API.yaml), It is only necessary the `description` and `eventFilters` fields, in case of provider is also mandatory to fulfill `notificationDestination` and `websockNotifConfig` ## Configuration via `capif_sdk_register.json` diff --git a/doc/sdk_full_documentation.md b/doc/sdk_full_documentation.md index d309f3d440e7383a67edc2ae4e78172bc21de444..3377baa1ac7d1d49d58e004668ecc2fac1b289a4 100644 --- a/doc/sdk_full_documentation.md +++ b/doc/sdk_full_documentation.md @@ -166,7 +166,7 @@ The provider must be onboarded before using these features. ### Create logs OpenCAPIF SDK references: -- **Function**: `create_logs(aefId, api_invoker_id)` +- **Function**: `create_logs(aefId, jwt)` The provider notifies to the CCF that the published API has been used by certain invoker. @@ -174,7 +174,7 @@ For leveraging this feature the Provider must have onboarded and published an AP **Required SDK input**: - aefId (Within the function) -- api_invoker_id (Within the function) +- jwt (Within the function) - log (Within [SDK configuration](./sdk_configuration.md) or object)  @@ -186,7 +186,7 @@ OpenCAPIF SDK references: The provider ask to the CCF about notifications related to services such as SERVICE_API_AVAILABLE or API_INVOKER_UPDATED. -This services are specificated in [CAPIF Events API management](https://github.com/jdegre/5GC_APIs/blob/Rel-18/TS29222_CAPIF_Events_API.yaml) explained in [SDK configuration](./sdk_configuration.md#events_configuration) +This services are specificated in [CAPIF Events API management](https://github.com/jdegre/5GC_APIs/blob/Rel-18/TS29222_CAPIF_Events_API.yaml) explained in [SDK configuration](./sdk_configuration.md#descriptions-of-capif_sdk_config-fields) For leveraging this feature the Provider must have onboarded previously. @@ -194,7 +194,7 @@ For leveraging this feature the Provider must have onboarded previously. - aefId//apfId//amfId (Within the function) - name: An arbitrary name we want to set in order to store it. -- events (Within [SDK configuration](./sdk_configuration.md#events_configuration) or object) +- events (Within [SDK configuration](./sdk_configuration.md#descriptions-of-capif_sdk_config-fields) or object) ### Delete subscription @@ -217,7 +217,7 @@ OpenCAPIF SDK references: The provider ask to the CCF about updating the subscription for receiving different services such as SERVICE_API_AVAILABLE or API_INVOKER_UPDATED, changing the URL for receiving the notifications... -This services are specificated in [CAPIF Events API management](https://github.com/jdegre/5GC_APIs/blob/Rel-18/TS29222_CAPIF_Events_API.yaml) explained in [SDK configuration](./sdk_configuration.md#events_configuration) +This services are specificated in [CAPIF Events API management](https://github.com/jdegre/5GC_APIs/blob/Rel-18/TS29222_CAPIF_Events_API.yaml) explained in [SDK configuration](./sdk_configuration.md#descriptions-of-capif_sdk_config-fields) For leveraging this feature the Provider must have onboarded and created a subscription previously. @@ -305,7 +305,7 @@ The SDK facilitates JWT token creation for secure access to target APIs. This pr ### Check authentication OpenCAPIF SDK references: -- **Function**: `check_authentication()` +- **Function**: `check_authentication(supported_features)` The SDK allows the Network App Invoker to check the `supported_features` from the target Provider's API exposing function (AEF). diff --git a/opencapif_sdk/api_schema_translator.py b/opencapif_sdk/api_schema_translator.py index 12519903c5df540483c3b15b12a8c5df686d7aa1..7faef309160605fabb1b1fbafc87e9fdc3e90695 100644 --- a/opencapif_sdk/api_schema_translator.py +++ b/opencapif_sdk/api_schema_translator.py @@ -34,31 +34,48 @@ class api_schema_translator: self.api_info = self.__load_api_file(self.api_path) self.__validate_api_info() - def build(self, api_name, ip, port): - if not self.__validate_ip_port(ip, port): + def build(self, api_name, ip=None, port=None, fqdn=None, ipv6Addr=None): + """ + Builds the API description and saves it to a JSON file. + Supports either IPv4 (ip), IPv6 (ipv6Addr), or FQDN (fqdn). + """ + # Validate that at least one of ip, ipv6Addr, or fqdn is provided + if not (ip or ipv6Addr or fqdn): + self.logger.error("At least one of 'ip', 'ipv6Addr', or 'fqdn' must be provided. Aborting build.") + return + + # Validate IP and port if IPv4 is provided + if ip and not self.__validate_ip_port(ip, port): self.logger.error("Invalid IP or port. Aborting build.") return - api_data = { - "apiName": self.api_info["info"].get("title", api_name), - "aefProfiles": self.__build_aef_profiles(ip, port), - "description": self.api_info["info"].get("description", "No description provided"), - "supportedFeatures": "fffff", - "shareableInfo": { - "isShareable": True, - "capifProvDoms": ["string"] - }, - "serviceAPICategory": "string", - "apiSuppFeats": "fffff", - "pubApiPath": { - "ccfIds": ["string"] - }, - "ccfId": "string" - } + # Build the API data + try: + api_data = { + "apiName": self.api_info["info"].get("title", api_name), + "aefProfiles": self.__build_aef_profiles(ip, port, fqdn, ipv6Addr), + "description": self.api_info["info"].get("description", "No description provided"), + "supportedFeatures": "fffff", + "shareableInfo": { + "isShareable": True, + "capifProvDoms": ["string"] + }, + "serviceAPICategory": "string", + "apiSuppFeats": "fffff", + "pubApiPath": { + "ccfIds": ["string"] + }, + "ccfId": "string" + } + + # Save the API data to a JSON file + with open(f"{api_name}.json", "w") as outfile: + json.dump(api_data, outfile, indent=4) + self.logger.info(f"API description saved to {api_name}.json") + + except Exception as e: + self.logger.error(f"An error occurred during the build process: {e}") - with open(f"{api_name}.json", "w") as outfile: - json.dump(api_data, outfile, indent=4) - self.logger.info(f"API description saved to {api_name}.json") def __load_api_file(self, api_file: str): """Loads the Swagger API configuration file and converts YAML to JSON format if necessary.""" @@ -90,7 +107,7 @@ class api_schema_translator: else: self.logger.info("All required components are present in the API specification.") - def __build_aef_profiles(self, ip, port): + def __build_aef_profiles(self, ip, port, fqdn=None, ipv6Addr=None): """Builds the aefProfiles section based on the paths and components in the API info.""" aef_profiles = [] @@ -107,6 +124,21 @@ class api_schema_translator: } resources.append(resource) + # Create interface description based on the standard + interface_description = { + "port": port, + "securityMethods": ["OAUTH"] + } + # Include ipv4Addr, ipv6Addr, or fqdn as per the standard + if ip: + interface_description["ipv4Addr"] = ip + elif ipv6Addr: + interface_description["ipv6Addr"] = ipv6Addr + elif fqdn: + interface_description["fqdn"] = fqdn + else: + raise ValueError("At least one of ipv4Addr, ipv6Addr, or fqdn must be provided.") + # Example profile creation based on paths, customize as needed aef_profile = { "aefId": "", # Placeholder AEF ID @@ -144,16 +176,9 @@ class api_schema_translator: "protocol": "HTTP_1_1", "dataFormat": "JSON", "securityMethods": ["OAUTH"], - "interfaceDescriptions": [ - { - "ipv4Addr": ip, - "port": port, - "securityMethods": ["OAUTH"] - } - ] + "interfaceDescriptions": [interface_description] } aef_profiles.append(aef_profile) - return aef_profiles def __validate_ip_port(self, ip, port): diff --git a/opencapif_sdk/capif_invoker_connector.py b/opencapif_sdk/capif_invoker_connector.py index e04ccb1ac4f9b75ba09c3e7b8bfcac0b13ea3638..5ebdec38da13bd30895077d50948279b608b1d8d 100644 --- a/opencapif_sdk/capif_invoker_connector.py +++ b/opencapif_sdk/capif_invoker_connector.py @@ -107,6 +107,8 @@ class capif_invoker_connector: # Define the invoker folder path and create it if it doesn't exist self.invoker_folder = os.path.join(invoker_general_folder, capif_username) os.makedirs(self.invoker_folder, exist_ok=True) + if supported_features is None: + supported_features = 0 self.supported_features = supported_features # Configure URLs for CAPIF HTTPS and register services diff --git a/opencapif_sdk/capif_logging_feature.py b/opencapif_sdk/capif_logging_feature.py index c2f3b42492729ee10d08a4fa5f82134bc665da7b..0ea2f27a957f5c0255211c2034476201a433aa08 100644 --- a/opencapif_sdk/capif_logging_feature.py +++ b/opencapif_sdk/capif_logging_feature.py @@ -4,6 +4,9 @@ import urllib3 import requests import json import warnings +import jwt +from OpenSSL import crypto +from jwt.exceptions import InvalidTokenError from requests.exceptions import RequestsDependencyWarning urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) warnings.filterwarnings("ignore", category=RequestsDependencyWarning) @@ -192,7 +195,10 @@ class capif_logging_feature: if not self.api_id: raise ValueError(f"No ID was found for the API '{name}'.") - def create_logs(self, aefId, api_invoker_id): + 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 = { @@ -285,3 +291,40 @@ 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. + + :param jwt_token: The JWT to decrypt. + :return: The payload of the JWT if it is valid. + :raises: InvalidTokenError if the JWT is invalid or cannot be decrypted. + """ + try: + # Path to the certificate + path = os.path.join(self.provider_folder, "capif_cert_server.pem") + + # Ensure the certificate file exists + if not os.path.exists(path): + raise FileNotFoundError(f"Certificate file not found at {path}") + + # Load the public key from the certificate + with open(path, "r") as cert_file: + cert = cert_file.read() + + # Decode the JWT using the public key + crtObj = crypto.load_certificate(crypto.FILETYPE_PEM, cert) + pubKeyObject = crtObj.get_pubkey() + pubKeyString = crypto.dump_publickey(crypto.FILETYPE_PEM, pubKeyObject) + payload = jwt.decode(jwt_token, pubKeyString, algorithms=["RS256"]) + + for key, value in payload.items(): + if key == "sub": + return value + + except InvalidTokenError as e: + raise InvalidTokenError(f"Invalid JWT token: {e}") + + 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 062b6a119c45ab1b9b313405c572d384d9392f12..65f3d593329ab07f0e19118ce5fb1aa7f94fb475 100644 --- a/opencapif_sdk/capif_provider_connector.py +++ b/opencapif_sdk/capif_provider_connector.py @@ -16,7 +16,8 @@ import shutil import subprocess from requests.auth import HTTPBasicAuth import urllib3 - +import ssl +import socket urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) @@ -102,7 +103,7 @@ class capif_provider_connector: supported_features = os.getenv('PROVIDER_SUPPORTED_FEATURES', provider_config.get('supported_features', '')).strip() if not supported_features: supported_features = "0" - + apfs = os.getenv('PROVIDER_APFS', provider_config.get('apfs', '')).strip() aefs = os.getenv('PROVIDER_AEFS', provider_config.get('aefs', '')).strip() api_description_path = os.path.abspath(os.getenv('PROVIDER_API_DESCRIPTION_PATH', provider_config.get('api_description_path', '')).strip()) @@ -150,11 +151,11 @@ class capif_provider_connector: self.capif_https_port = str(capif_https_port) self.provider_capif_ids = {} - + path_prov_funcs = os.path.join(self.provider_folder, "provider_capif_ids.json") if os.path.exists(path_prov_funcs): self.provider_capif_ids = self._load_provider_api_details() - + path_published = os.path.join(self.provider_folder, "provider_service_ids.json") if os.path.exists(path_published): self.provider_service_ids = self.__load_config_file(path_published) @@ -170,7 +171,7 @@ class capif_provider_connector: self.capif_register_url = f"https://{capif_register_host.strip()}:8084/" else: self.capif_register_url = f"https://{capif_register_host.strip()}:{capif_register_port.strip()}/" - + events_config = provider_config.get('events', {}) self.events_description = os.getenv('PROVIDER_EVENTS_DESCRIPTION', events_config.get('description', '')) self.events_filter = os.getenv('PROVIDER_EVENTS_FILTERS', events_config.get('eventFilters', '')) @@ -185,31 +186,23 @@ class capif_provider_connector: raise def __store_certificate(self) -> None: - # Retrieves and stores the cert_server.pem from CAPIF. - self.logger.info( - "Retrieving capif_cert_server.pem, this may take a few minutes.") - - cmd = f"openssl s_client -connect {self.capif_host}:{self.capif_https_port} | openssl x509 -text > {self.provider_folder}/capif_cert_server.pem" + self.logger.info("Retrieving capif_cert_server.pem...") try: - # Redirects standard output and error to os.devnull to hide logs - with open(os.devnull, 'w') as devnull: - subprocess.run(cmd, shell=True, check=True, - stdout=devnull, stderr=devnull) - - cert_file = os.path.join( - self.provider_folder, "capif_cert_server.pem") - if os.path.exists(cert_file) and os.path.getsize(cert_file) > 0: - self.logger.info("cert_server.pem successfully generated!") - else: - self.logger.error("Failed to generate cert_server.pem.") - raise FileNotFoundError( - f"Certificate file not found at {cert_file}") - except subprocess.CalledProcessError as e: - self.logger.error(f"Command failed: {e}") - raise + # Crear un contexto SSL que no valide certificados + context = ssl.create_default_context() + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + + with socket.create_connection((self.capif_host, self.capif_https_port)) as sock: + with context.wrap_socket(sock, server_hostname=self.capif_host) as ssock: + cert = ssock.getpeercert(binary_form=True) + cert_file = os.path.join(self.provider_folder, "capif_cert_server.pem") + with open(cert_file, "wb") as f: + f.write(ssl.DER_cert_to_PEM_cert(cert).encode()) + self.logger.info("cert_server.pem successfully generated!") except Exception as e: - self.logger.error(f"Error occurred: {e}") + self.logger.error(f"Error occurred while retrieving certificate: {e}") raise def __load_config_file(self, config_file: str): @@ -263,7 +256,7 @@ class capif_provider_connector: def __onboard_exposer_to_capif(self, access_token, capif_onboarding_url): self.logger.info( "Onboarding Provider to CAPIF and waiting signed certificate by giving our public keys to CAPIF") - + url = f"{self.capif_https_url}{capif_onboarding_url}" headers = { "Authorization": f"Bearer {access_token}", @@ -355,7 +348,6 @@ class capif_provider_connector: self.provider_capif_ids[indexedroles[i]] = api_prov_func["apiProvFuncId"] json.dump(data, outfile, indent=4) - self.logger.info("Data saved") def __save_capif_ca_root_file_and_get_auth_token(self): @@ -462,7 +454,6 @@ class capif_provider_connector: with open(service_api_description_json_full_path, "r") as service_file: data = json.load(service_file) - data["supportedFeatures"] = f"{self.supported_features}" # Verifying that the number of AEFs is equal to the aefProfiles if len(AEFs_list) != len(data.get("aefProfiles", [])): self.logger.error( @@ -471,7 +462,7 @@ class capif_provider_connector: "Mismatch between number of AEFs and profiles") # Assigning each AEF - + for profile, aef_id in zip(data.get("aefProfiles", []), AEFs_list): if not isinstance(profile, dict): # Verificar que profile sea un diccionario raise TypeError(f"Expected profile to be a dict, got {type(profile).__name__}") @@ -509,7 +500,7 @@ class capif_provider_connector: "description": "Revoke authorization for service APIs." }) i -= 1 - + self.logger.info( "Service API description modified successfully") @@ -538,7 +529,6 @@ class capif_provider_connector: ) self.logger.info(f"Publishing services to URL: {url}") - try: response = requests.post( url, @@ -668,7 +658,7 @@ class capif_provider_connector: break output_path = os.path.join( - + self.provider_folder, "provider_service_ids.json") # Read the existing file of published APIs @@ -951,7 +941,7 @@ class capif_provider_connector: "description": "Revoke authorization for service APIs." }) i -= 1 - + self.logger.info( "Service API description modified successfully") @@ -1158,102 +1148,32 @@ class capif_provider_connector: capif_onboarding_url = capif_postauth_info["ccf_api_onboarding_url"] access_token = capif_postauth_info["access_token"] 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 ) def certs_modifications(self): - api_details = self._load_provider_api_details() - - apf_count = 0 - aef_count = 0 - - # Iterate over the dictionary keys (the fields of the JSON) - - for key in api_details.keys(): - if key.startswith("APF"): - apf_count += 1 - elif key.startswith("AEF"): - aef_count += 1 - - # Log the results (or return them, depending on what you need) - self.logger.info(f"Total APFs: {apf_count}, Total AEFs: {aef_count}") - APFscertstoremove = 0 - APFscertstoadd = 0 - AEFscertstoremove = 0 - AEFscertstoadd = 0 - # Calculate the difference of APFs - if apf_count != self.apfs: - diff = apf_count - self.apfs - if diff < 0: - self.APFscertstoadd = abs(diff) - else: - APFscertstoremove = diff - else: - APFscertstoremove = 0 - APFscertstoadd = 0 - - # Calculate the difference of AEFs - if aef_count != self.aefs: - diff = aef_count - self.aefs - if diff < 0: - self.AEFscertstoadd = abs(diff) - else: - AEFscertstoremove = diff - else: - AEFscertstoremove = 0 - AEFscertstoadd = 0 - - # Remove APF files in descending order if there are more APFs than there should be - if APFscertstoremove: - while apf_count > self.apfs: - # List files starting with "APF-" or "apf-" in the directory - file_path = os.path.join( - self.provider_folder, f"APF-{apf_count}_private_key.key") - os.remove(file_path) - self.logger.info( - f"Removed APF file: APF-{apf_count}_private_key.key") + self.logger.info("Starting certificate removal process...") - file_path = os.path.join( - self.provider_folder, f"APF-{apf_count}_public.csr") - os.remove(file_path) - self.logger.info( - f"Removed APF file: APF-{apf_count}_public.csr") - - file_path = os.path.join( - self.provider_folder, f"apf-{apf_count}.crt") - os.remove(file_path) - self.logger.info(f"Removed APF file: apf-{apf_count}.crt") - # Decrease the APF count - apf_count -= 1 - - # Remove AEF files in descending order if there are more AEFs than there should be - if AEFscertstoremove: - while aef_count > self.aefs: - # List files starting with "AEF-" or "aef-" in the directory - file_path = os.path.join( - self.provider_folder, f"AEF-{aef_count}_private_key.key") - os.remove(file_path) - self.logger.info( - f"Removed AEF file: AEF-{aef_count}_private_key.key") + # List of possible certificate patterns to remove + cert_patterns = ["APF-", "apf-", "AEF-", "aef-", "AMF", "amf"] + cert_extensions = ["_private_key.key", "_public.csr", ".crt"] - file_path = os.path.join( - self.provider_folder, f"AEF-{aef_count}_public.csr") - os.remove(file_path) - self.logger.info( - f"Removed AEF file: AEF-{aef_count}_public.csr") + # Iterate over the directory and remove matching files + for file_name in os.listdir(self.provider_folder): + if any(file_name.startswith(pattern) for pattern in cert_patterns) and any(file_name.endswith(ext) for ext in cert_extensions): + file_path = os.path.join(self.provider_folder, file_name) + try: + os.remove(file_path) + self.logger.info(f"Removed certificate file: {file_name}") + except Exception as e: + self.logger.error(f"Error removing {file_name}: {e}") - file_path = os.path.join( - self.provider_folder, f"aef-{aef_count}.crt") - os.remove(file_path) - self.logger.info(f"Removed AEF file: aef-{aef_count}.crt") - # Decrease the APF count - aef_count -= 1 + self.logger.info("Certificate removal process completed.") def update_onboard(self, capif_onboarding_url, access_token): self.logger.info( @@ -1351,7 +1271,7 @@ class capif_provider_connector: self.logger.error( f"Onboarding failed: {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. @@ -1386,7 +1306,7 @@ class capif_provider_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.") @@ -1395,7 +1315,7 @@ class capif_provider_connector: except Exception as e: self.logger.error(f"Error handling the file '{full_file_name}': {e}") raise - + def _find_key_by_value(self, data, target_value): """ Given a dictionary and a value, return the key corresponding to that value. @@ -1408,7 +1328,7 @@ class capif_provider_connector: if value == target_value: return key return None - + def _load_config_file(self, config_file: str): """Loads the configuration file.""" try: @@ -1417,4 +1337,4 @@ class capif_provider_connector: except FileNotFoundError: self.logger.warning( f"Configuration file {config_file} not found. Using defaults or environment variables.") - return {} \ No newline at end of file + return {} diff --git a/opencapif_sdk/service_discoverer.py b/opencapif_sdk/service_discoverer.py index 6272e94ca7e9b6988c0d5a3a4a3b6694ebc8ad2f..6089d3d70ab46af80dea1ee398ad72d80ce55ea2 100644 --- a/opencapif_sdk/service_discoverer.py +++ b/opencapif_sdk/service_discoverer.py @@ -5,7 +5,7 @@ import requests import os import logging import urllib3 - +import re urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) @@ -75,6 +75,7 @@ class service_discoverer: invoker_general_folder = os.path.abspath( os.getenv('invoker_folder', invoker_config.get('invoker_folder', '')).strip() ) + capif_callback_url = os.getenv('INVOKER_CAPIF_CALLBACK_URL', invoker_config.get('capif_callback_url', '')).strip() supported_features = os.getenv('INVOKER_FOLDER', invoker_config.get('supported_features', '')).strip() check_authentication_data = invoker_config.get('check_authentication_data', {}) self.check_authentication_data = { @@ -107,6 +108,8 @@ class service_discoverer: self.capif_host = capif_host self.capif_https_port = capif_https_port self.token = "" + if supported_features is None: + supported_features = 0 self.supported_features = supported_features # Create invoker folder dynamically based on username and folder path @@ -114,7 +117,7 @@ class service_discoverer: os.makedirs(self.invoker_folder, exist_ok=True) # Load CAPIF API details - + self.capif_callback_url = capif_callback_url self.invoker_capif_details = self.__load_provider_api_details() try: self.token = self.invoker_capif_details["access_token"] @@ -130,9 +133,6 @@ class service_discoverer: # Log initialization success message self.logger.info("ServiceDiscoverer initialized correctly") - def get_api_provider_id(self): - return self.invoker_capif_details["api_provider_id"] - def __load_config_file(self, config_file: str): """Carga el archivo de configuración.""" try: @@ -207,7 +207,7 @@ class service_discoverer: url = f"https://{self.capif_host}:{self.capif_https_port}/capif-security/v1/trustedInvokers/{self.invoker_capif_details['api_invoker_id']}/update" payload = { "securityInfo": [], - "notificationDestination": "https://mynotificationdest.com", + "notificationDestination": f"{self.capif_callback_url}", "requestTestNotification": True, "websockNotifConfig": { "websocketUri": "string", @@ -221,20 +221,17 @@ class service_discoverer: for i in range(0, number_of_apis): # Obtaining the values of api_id and aef_id for each API - aef_profiles = self.invoker_capif_details["registered_security_contexes"][i]['aef_profiles'] api_id = self.invoker_capif_details["registered_security_contexes"][i]['api_id'] - for n in range(0, len(aef_profiles)): + for n in range(len(self.invoker_capif_details["registered_security_contexes"][i]['aef_profiles'])): aef_id = self.invoker_capif_details["registered_security_contexes"][i]['aef_profiles'][n]['aef_id'] - security_info = { - "prefSecurityMethods": ["OAUTH"], + "prefSecurityMethods": self.invoker_capif_details["registered_security_contexes"][i]['aef_profiles'][n]['security_methods'], "authenticationInfo": "string", "authorizationInfo": "string", "aefId": aef_id, "apiId": api_id } payload["securityInfo"].append(security_info) - try: response = requests.post(url, json=payload, @@ -268,7 +265,7 @@ class service_discoverer: url = f"https://{self.capif_host}:{self.capif_https_port}/capif-security/v1/trustedInvokers/{self.invoker_capif_details['api_invoker_id']}" payload = { "securityInfo": [], - "notificationDestination": "https://mynotificationdest.com", + "notificationDestination": f"{self.capif_callback_url}", "requestTestNotification": True, "websockNotifConfig": { "websocketUri": "string", @@ -282,12 +279,11 @@ class service_discoverer: for i in range(0, number_of_apis): # Obtaining the values of api_id and aef_id for each API - aef_profiles = self.invoker_capif_details["registered_security_contexes"][i]['aef_profiles'] api_id = self.invoker_capif_details["registered_security_contexes"][i]['api_id'] - for n in range(0, len(aef_profiles)): + for n in range(len(self.invoker_capif_details["registered_security_contexes"][i]['aef_profiles'])): aef_id = self.invoker_capif_details["registered_security_contexes"][i]['aef_profiles'][n]['aef_id'] security_info = { - "prefSecurityMethods": ["OAUTH"], + "prefSecurityMethods": self.invoker_capif_details["registered_security_contexes"][i]['aef_profiles'][n]['security_methods'], "authenticationInfo": "string", "authorizationInfo": "string", "aefId": aef_id, @@ -468,24 +464,27 @@ class service_discoverer: def save_api_discovered(self, endpoints): self.invoker_capif_details["registered_security_contexes"] = [] - p = 0 - for service in endpoints["serviceAPIDescriptions"]: - api_id = service["apiId"] - api_name = service["apiName"] - aef_profiles = [] - self.invoker_capif_details["registered_security_contexes"].append({"api_name": api_name, "api_id": api_id, "aef_profiles": aef_profiles}) - for n in service["aefProfiles"]: - versions = n["versions"] - aef_id = n["aefId"] - for m in n["interfaceDescriptions"]: - ip = m["ipv4Addr"] - port = m["port"] - self.invoker_capif_details["registered_security_contexes"][p]['aef_profiles'].append( - {"aef_id": aef_id, "ip": ip, "port": port, "versions": versions}) - p += 1 - self.save_api_details() + 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 = {} + for key, value in data.items(): + new_key = self.to_snake_case(key) + new_dict[new_key] = self.convert_keys_to_snake_case(value) if isinstance(value, (dict, list)) else value + return new_dict + elif isinstance(data, list): + 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 @@ -505,7 +504,7 @@ class service_discoverer: "Error while saving API provider details: %s", str(e)) raise - def check_authentication(self): + def check_authentication(self, supported_features): self.logger.info("Checking authentication") try: invoker_details = self.__load_provider_api_details() @@ -515,7 +514,7 @@ class service_discoverer: payload = { "apiInvokerId": f"{invoker_id}", - "supportedFeatures": f"{self.supported_features}" + "supportedFeatures": f"{supported_features}" } headers = { diff --git a/setup.py b/setup.py index aec5a5422dd10418d7c5f0863ba8ab3ec187fafa..f082936b3e5bc860335635920559761c882d01e0 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ with open(os.path.join(this_directory, "README_pipy.md"), encoding="utf-8") as f setup( name="opencapif_sdk", - version="0.1.18", + version="0.1.19", author="JorgeEcheva, dgs-cgm", author_email="jorge.echevarriauribarri.practicas@telefonica.com, daniel.garciasanchez@telefonica.com", description=( diff --git a/test/capif_sdk_config_sample_test.json b/test/capif_sdk_config_sample_test.json index 598a11cbca8733e85285321bd6087ba544e92b7c..26caa737c731fbec56aa28e886720aab94cc4607 100644 --- a/test/capif_sdk_config_sample_test.json +++ b/test/capif_sdk_config_sample_test.json @@ -85,7 +85,7 @@ } }, "log": { - "apiName": "Testtrece", + "apiName": "API of dummy Network-App to test", "apiVersion": "v1", "resourceName": "MONITORING_SUBSCRIPTIONS", "uri": "/{scsAsId}/subscriptions", diff --git a/test/test.py b/test/main.py similarity index 93% rename from test/test.py rename to test/main.py index a4ec863c28f783e964817bce1eb7af71c138e9d7..520d07d1a3fb43846de1160b6e4053cf1db28357 100644 --- a/test/test.py +++ b/test/main.py @@ -12,7 +12,7 @@ def preparation_for_update(APFs, AEFs, second_network_app_api,capif_provider_con capif_provider_connector.apfs = APFs capif_provider_connector.aefs = AEFs if second_network_app_api: - capif_provider_connector.api_description_path = "./network_app_provider_api_spec_2.json" + capif_provider_connector.api_description_path = "./network_app_provider_api_spec.json" else: capif_provider_connector.api_description_path = "./network_app_provider_api_spec_3.json" @@ -26,14 +26,13 @@ def ensure_update(chosen_apf, chosen_aefs, second_network_app_api,capif_provider APF = capif_provider_connector.provider_capif_ids[chosen_apf] AEF1 = capif_provider_connector.provider_capif_ids[chosen_aefs[0]] AEF2 = capif_provider_connector.provider_capif_ids[chosen_aefs[1]] - AEF3 = capif_provider_connector.provider_capif_ids[chosen_aefs[2]] if not APF or not AEF1 or not AEF2: raise ValueError("Not all necessary values were found in 'provider_service_ids.json'") # Update configuration file capif_provider_connector.publish_req['publisher_apf_id'] = APF - capif_provider_connector.publish_req['publisher_aefs_ids'] = [AEF1, AEF2,AEF3] + capif_provider_connector.publish_req['publisher_aefs_ids'] = [AEF1, AEF2] else: @@ -48,7 +47,7 @@ def ensure_update(chosen_apf, chosen_aefs, second_network_app_api,capif_provider capif_provider_connector.publish_services() if second_network_app_api: - service_api_id = capif_provider_connector.provider_service_ids['Test-two'] + service_api_id = capif_provider_connector.provider_service_ids['Testtrece'] else: service_api_id = capif_provider_connector.provider_service_ids['Test-three'] @@ -81,17 +80,16 @@ if __name__ == "__main__": # Get AEFs ids and APFs ids to publish an API - APF1 = capif_provider_connector.provider_capif_ids['APF-1'] APF2 = capif_provider_connector.provider_capif_ids['APF-2'] AEF1 = capif_provider_connector.provider_capif_ids['AEF-1'] AEF2 = capif_provider_connector.provider_capif_ids['AEF-2'] AEF3 = capif_provider_connector.provider_capif_ids['AEF-3'] - capif_provider_connector.api_description_path="network_app_provider_api_spec.json" + capif_provider_connector.api_description_path="test1.json" # Update configuration file capif_provider_connector.publish_req['publisher_apf_id'] = APF1 - capif_provider_connector.publish_req['publisher_aefs_ids'] = [AEF1, AEF2] + capif_provider_connector.publish_req['publisher_aefs_ids'] = [AEF1] event_provider = capif_provider_event_feature(config_file=capif_sdk_config_path) @@ -107,7 +105,7 @@ if __name__ == "__main__": print("PROVIDER PUBLISH COMPLETED") - service_api_id = capif_provider_connector.provider_service_ids["Testtrece"] + service_api_id = capif_provider_connector.provider_service_ids["API of dummy Network-App to test"] capif_provider_connector.publish_req['service_api_id'] = service_api_id @@ -129,22 +127,18 @@ if __name__ == "__main__": print("INVOKER ONBOARDING COMPLETED") discoverer = service_discoverer(config_file=capif_sdk_config_path) - - discoverer.discover_filter["api-name"]= "Testtrece" discoverer.discover() print("SERVICE DISCOVER COMPLETED") - + discoverer.get_tokens() print("SERVICE GET TOKENS COMPLETED") logger=capif_logging_feature(config_file=capif_sdk_config_path) - invoker_id=discoverer.invoker_capif_details["api_invoker_id"] - - logger.create_logs(aefId=AEF1,api_invoker_id=invoker_id) + logger.create_logs(aefId=AEF1, jwt=discoverer.token) event_invoker = capif_invoker_event_feature(config_file=capif_sdk_config_path) diff --git a/test/network_app_provider_api_spec.json b/test/network_app_provider_api_spec.json old mode 100755 new mode 100644 index 5cf40654214dcde228a5b85c5d7e3c6441a93d18..765fa2dc5d7f92c0b48e833089702e28a698e65f --- a/test/network_app_provider_api_spec.json +++ b/test/network_app_provider_api_spec.json @@ -2,7 +2,7 @@ "apiName": "Testtrece", "aefProfiles": [ { - "aefId": "AEF0a7db19a1968aeb46da269e6e307c5", + "aefId": "AEF6b074911c72c9a49c8a3ea7e881b85", "versions": [ { "apiVersion": "v1", @@ -40,6 +40,22 @@ "POST" ], "description": "Custom operation for specific request" + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "check-authentication", + "operations": [ + "POST" + ], + "description": "Check authentication request." + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "revoke-authentication", + "operations": [ + "POST" + ], + "description": "Revoke authorization for service APIs." } ] } @@ -61,7 +77,7 @@ ] }, { - "aefId": "AEFa3c0228d148f38c7171bfde164804e", + "aefId": "AEFfaa1b3b961a3a09c71b633fe8327c3", "versions": [ { "apiVersion": "v1", @@ -116,6 +132,22 @@ "POST" ], "description": "Custom operation for specific request" + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "check-authentication", + "operations": [ + "POST" + ], + "description": "Check authentication request." + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "revoke-authentication", + "operations": [ + "POST" + ], + "description": "Revoke authorization for service APIs." } ] } @@ -137,7 +169,7 @@ } ], "description": "API of dummy Network-App to test", - "supportedFeatures": "fffff", + "supportedFeatures": "fffffff", "shareableInfo": { "isShareable": true, "capifProvDoms": [ diff --git a/test/network_app_provider_api_spec_3.json b/test/network_app_provider_api_spec_3.json index 7b9fbe4f167620a2fb673fa02612b6af2b0b9364..825108e5920125c1c150ffb9c8053f7460e8d5de 100755 --- a/test/network_app_provider_api_spec_3.json +++ b/test/network_app_provider_api_spec_3.json @@ -2,7 +2,7 @@ "apiName": "Test-three", "aefProfiles": [ { - "aefId": "AEF67f10e46783f3e68356b4bb78c1cfc", + "aefId": "AEF46db4d6b56d212ab007201ac8224dc", "versions": [ { "apiVersion": "v1", @@ -40,6 +40,22 @@ "POST" ], "description": "string" + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "check-authentication", + "operations": [ + "POST" + ], + "description": "Check authentication request." + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "revoke-authentication", + "operations": [ + "POST" + ], + "description": "Revoke authorization for service APIs." } ] } @@ -61,7 +77,7 @@ ] }, { - "aefId": "AEF528e9c4c1918f3c10205104e4336b8", + "aefId": "AEF7d3d9ec715697bd6dc5974e5d78081", "versions": [ { "apiVersion": "v1", @@ -116,6 +132,22 @@ "POST" ], "description": "string" + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "check-authentication", + "operations": [ + "POST" + ], + "description": "Check authentication request." + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "revoke-authentication", + "operations": [ + "POST" + ], + "description": "Revoke authorization for service APIs." } ] } @@ -137,7 +169,7 @@ } ], "description": "API of dummy Network-App to test", - "supportedFeatures": "fffff", + "supportedFeatures": "fffffff", "shareableInfo": { "isShareable": true, "capifProvDoms": [ diff --git a/test/pytest.ini b/test/pytest.ini new file mode 100644 index 0000000000000000000000000000000000000000..4499ecc7fb157d98c13794862c3b51d98c6e534d --- /dev/null +++ b/test/pytest.ini @@ -0,0 +1,4 @@ +[pytest] +filterwarnings = + ignore:Unverified HTTPS request is being made:urllib3.exceptions.InsecureRequestWarning +log_cli = true \ No newline at end of file diff --git a/test/network_app_provider_api_spec_2.json b/test/test1.json old mode 100755 new mode 100644 similarity index 56% rename from test/network_app_provider_api_spec_2.json rename to test/test1.json index 94175bdcf3e1c29b993d30a91e99a1f4ced2b64f..6bd5aacc97b0ae83ecd009b01c9329bf19c4fd10 --- a/test/network_app_provider_api_spec_2.json +++ b/test/test1.json @@ -1,75 +1,66 @@ { - "apiName": "Test-two", + "apiName": "API of dummy Network-App to test", "aefProfiles": [ { - "aefId": "AEF71fd7e0328beb8863ec9d4eeef5a08", + "aefId": "AEF74df7938fe9b102e1014ec3b0058d6", "versions": [ { "apiVersion": "v1", "expiry": "2100-11-30T10:32:02.004Z", "resources": [ { - "resourceName": "MONITORING_SUBSCRIPTIONS", - "commType": " SUBSCRIBE_NOTIFY", + "resourceName": "Retrieve monitoring subscriptions", + "commType": "REQUEST_RESPONSE", + "uri": "/{scsAsId}/subscriptions", + "custOpName": "http_get", + "operations": [ + "GET" + ], + "description": "Endpoint to manage monitoring subscriptions" + }, + { + "resourceName": "Create monitoring subscription", + "commType": "REQUEST_RESPONSE", "uri": "/{scsAsId}/subscriptions", "custOpName": "http_post", "operations": [ - "GET", "POST" ], "description": "Endpoint to manage monitoring subscriptions" }, { - "resourceName": "MONITORING_SUBSCRIPTION_SINGLE", - "commType": " SUBSCRIBE_NOTIFY", + "resourceName": "Retrieve single subscription", + "commType": "REQUEST_RESPONSE", "uri": "/{scsAsId}/subscriptions/{subscriptionId}", "custOpName": "http_get", "operations": [ - "GET", - "PUT", - "DELETE" + "GET" ], "description": "Endpoint to manage single subscription" - } - ], - "custOperations": [ + }, { + "resourceName": "Update subscription", "commType": "REQUEST_RESPONSE", - "custOpName": "string", + "uri": "/{scsAsId}/subscriptions/{subscriptionId}", + "custOpName": "http_put", "operations": [ - "POST" + "PUT" ], - "description": "string" - } - ] - } - ], - "protocol": "HTTP_1_1", - "dataFormat": "JSON", - "securityMethods": [ - "OAUTH", - "PSK" - ], - "interfaceDescriptions": [ - { - "ipv4Addr": "127.0.0.1", - "port": 8888, - "securityMethods": [ - "OAUTH" - ] - } - ] - }, - { - "aefId": "AEF2d5b96486ce2f2c46a353a9438ad38", - "versions": [ - { - "apiVersion": "v1", - "expiry": "2100-11-30T10:32:02.004Z", - "resources": [ + "description": "Endpoint to manage single subscription" + }, + { + "resourceName": "Delete subscription", + "commType": "REQUEST_RESPONSE", + "uri": "/{scsAsId}/subscriptions/{subscriptionId}", + "custOpName": "http_delete", + "operations": [ + "DELETE" + ], + "description": "Endpoint to manage single subscription" + }, { - "resourceName": "TSN_LIST_PROFILES", - "commType": " SUBSCRIBE_NOTIFY", + "resourceName": "Retrieve TSN profiles", + "commType": "REQUEST_RESPONSE", "uri": "/profile", "custOpName": "http_get", "operations": [ @@ -78,8 +69,8 @@ "description": "Endpoint for retrieving the list of available TSN profiles" }, { - "resourceName": "TSN_DETAIL_PROFILE", - "commType": " SUBSCRIBE_NOTIFY", + "resourceName": "Retrieve a TSN profile", + "commType": "REQUEST_RESPONSE", "uri": "/profile?name={profileName}", "custOpName": "http_get", "operations": [ @@ -88,8 +79,8 @@ "description": "Endpoint for retrieving information about a single TSN profile" }, { - "resourceName": "TSN_APPLY_CONFIGURATION", - "commType": " SUBSCRIBE_NOTIFY", + "resourceName": "Apply TSN configuration", + "commType": "REQUEST_RESPONSE", "uri": "/apply", "custOpName": "http_post", "operations": [ @@ -98,8 +89,8 @@ "description": "Endpoint for configuring TSN connection parameters" }, { - "resourceName": "TSN_CLEAR_CONFIGURATION", - "commType": " SUBSCRIBE_NOTIFY", + "resourceName": "Clear TSN configuration", + "commType": "REQUEST_RESPONSE", "uri": "/clear", "custOpName": "http_post", "operations": [ @@ -116,64 +107,22 @@ "POST" ], "description": "string" - } - ] - } - ], - "protocol": "HTTP_1_1", - "dataFormat": "JSON", - "securityMethods": [ - "OAUTH" - ], - "interfaceDescriptions": [ - { - "ipv4Addr": "127.0.0.1", - "port": 8899, - "securityMethods": [ - "OAUTH" - ] - } - ] - }, - { - "aefId": "AEFbe2013357187c44a772b832d9d194e", - "versions": [ - { - "apiVersion": "v1", - "expiry": "2100-11-30T10:32:02.004Z", - "resources": [ + }, { - "resourceName": "MONITORING_SUBSCRIPTIONS", - "commType": " SUBSCRIBE_NOTIFY", - "uri": "/{scsAsId}/subscriptions", - "custOpName": "http_post", + "commType": "REQUEST_RESPONSE", + "custOpName": "check-authentication", "operations": [ - "GET", "POST" ], - "description": "Endpoint to manage monitoring subscriptions" + "description": "Check authentication request." }, - { - "resourceName": "MONITORING_SUBSCRIPTION_SINGLE", - "commType": " SUBSCRIBE_NOTIFY", - "uri": "/{scsAsId}/subscriptions/{subscriptionId}", - "custOpName": "http_get", - "operations": [ - "GET", - "PUT", - "DELETE" - ], - "description": "Endpoint to manage single subscription" - } - ], - "custOperations": [ { "commType": "REQUEST_RESPONSE", - "custOpName": "string", + "custOpName": "revoke-authentication", "operations": [ "POST" ], - "description": "string" + "description": "Revoke authorization for service APIs." } ] } @@ -181,16 +130,15 @@ "protocol": "HTTP_1_1", "dataFormat": "JSON", "securityMethods": [ - "OAUTH", - "PSK" + "OAUTH" ], "interfaceDescriptions": [ { - "ipv4Addr": "127.0.0.1", - "port": 8888, + "port": 9090, "securityMethods": [ "OAUTH" - ] + ], + "ipv4Addr": "0.0.0.0" } ] } diff --git a/test/test1.yaml b/test/test1.yaml new file mode 100755 index 0000000000000000000000000000000000000000..00d1722d6c9553c5f0eb300880e34e23188a7e84 --- /dev/null +++ b/test/test1.yaml @@ -0,0 +1,90 @@ +openapi: 3.0.0 +info: + title: API of dummy Network-App to test + version: v1 + description: API of dummy Network-App to test + x-supportedFeatures: fffffff +paths: + /{scsAsId}/subscriptions: + get: + summary: Retrieve monitoring subscriptions + description: Endpoint to manage monitoring subscriptions + operationId: getMonitoringSubscriptions + responses: + '200': + description: Successful operation + post: + summary: Create monitoring subscription + description: Endpoint to manage monitoring subscriptions + operationId: createMonitoringSubscription + responses: + '201': + description: Subscription created successfully + /{scsAsId}/subscriptions/{subscriptionId}: + get: + summary: Retrieve single subscription + description: Endpoint to manage single subscription + operationId: getSingleSubscription + responses: + '200': + description: Successful operation + put: + summary: Update subscription + description: Endpoint to manage single subscription + operationId: updateSubscription + responses: + '200': + description: Subscription updated successfully + delete: + summary: Delete subscription + description: Endpoint to manage single subscription + operationId: deleteSubscription + responses: + '204': + description: Subscription deleted successfully + /profile: + get: + summary: Retrieve TSN profiles + description: Endpoint for retrieving the list of available TSN profiles + operationId: getTsnProfiles + responses: + '200': + description: Successful operation + /profile?name={profileName}: + get: + summary: Retrieve a TSN profile + description: Endpoint for retrieving information about a single TSN profile + operationId: getTsnProfile + responses: + '200': + description: Successful operation + /apply: + post: + summary: Apply TSN configuration + description: Endpoint for configuring TSN connection parameters + operationId: applyTsnConfiguration + responses: + '201': + description: Configuration applied successfully + /clear: + post: + summary: Clear TSN configuration + description: Endpoint for removing a previous TSN connection configuration + operationId: clearTsnConfiguration + responses: + '204': + description: Configuration cleared successfully +components: + securitySchemes: + oauth: + type: oauth2 + flows: + clientCredentials: + tokenUrl: https://example.com/oauth/token +security: + - oauth: [] +servers: + - url: http://127.0.0.1:8888 + description: Main server for AEF services + - url: http://127.0.0.1:8899 + description: Alternate server for AEF services diff --git a/test/test_main.py b/test/test_main.py new file mode 100644 index 0000000000000000000000000000000000000000..6e4087634b99b8ab54f0a3b4614f7414cf1cc1ff --- /dev/null +++ b/test/test_main.py @@ -0,0 +1,117 @@ +import subprocess +import pytest +import urllib3 +# Desactivar solo el warning de solicitudes HTTPS no verificadas +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) +import json +# flake8: noqa + +from opencapif_sdk import capif_invoker_connector, capif_provider_connector, service_discoverer, capif_logging_feature, capif_invoker_event_feature, capif_provider_event_feature,api_schema_translator + + +capif_sdk_config_path = "./capif_sdk_config_sample_test.json" + +# Fixture para configurar el proveedor +@pytest.fixture +def provider_setup(): + provider = capif_provider_connector(capif_sdk_config_path) + provider.onboard_provider() + yield provider + provider.offboard_provider() + +# Fixture para configurar el proveedor +@pytest.fixture +def invoker_setup(): + invoker = capif_invoker_connector(capif_sdk_config_path) + invoker.onboard_invoker() + yield invoker + invoker.offboard_invoker() + +@pytest.fixture +def test_provider_update(provider_setup): + provider = capif_provider_connector(capif_sdk_config_path) + provider.aefs=1 + provider.apfs=1 + provider.update_provider() + +@pytest.fixture +def test_provider_publish(test_provider_update): + provider = capif_provider_connector(capif_sdk_config_path) + APF1 = provider.provider_capif_ids['APF-1'] + AEF1 = provider.provider_capif_ids['AEF-1'] + + translator = api_schema_translator("./test1.yaml") + translator.build("test1",ip="0.0.0.0",port=9090) + provider.api_description_path="./test1.json" + # Update configuration file + provider.publish_req['publisher_apf_id'] = APF1 + provider.publish_req['publisher_aefs_ids'] = [AEF1] + + provider.publish_services() + +@pytest.fixture +def test_events(test_provider_publish): + provider=capif_provider_connector(capif_sdk_config_path) + event_provider = capif_provider_event_feature(config_file=capif_sdk_config_path) + + APF1 = provider.provider_capif_ids['APF-1'] + AEF1 = provider.provider_capif_ids['AEF-1'] + + event_provider.create_subscription(name="Ejemplo1",id=AEF1) + + event_provider.create_subscription(name="Ejemplo2",id=APF1) + + event_provider.delete_subscription(name="Ejemplo1",id=AEF1) + + event_provider.delete_subscription(name="Ejemplo2",id=APF1) +@pytest.fixture +def tokens(invoker_setup): + discoverer = service_discoverer(config_file=capif_sdk_config_path) + discoverer.discover() + discoverer.get_tokens() + + +def test_logs(test_provider_publish,tokens): + provider=capif_provider_connector(capif_sdk_config_path) + discoverer = service_discoverer(config_file=capif_sdk_config_path) + AEF1 = provider.provider_capif_ids['AEF-1'] + token = discoverer.token + capif_log = capif_logging_feature(capif_sdk_config_path) + + capif_log.create_logs(aefId=AEF1,jwt=token) + +def test_invoker_discover(invoker_setup): + discoverer = service_discoverer(config_file=capif_sdk_config_path) + discoverer.discover() + discoverer.get_tokens() + +def test_provider_unpublish_1(test_events): + provider=capif_provider_connector(capif_sdk_config_path) + APF1 = provider.provider_capif_ids['APF-1'] + provider.publish_req['publisher_apf_id'] = APF1 + service_api_id = provider.provider_service_ids["API of dummy Network-App to test"] + provider.publish_req['service_api_id'] = service_api_id + provider.unpublish_service() + +def test_provider_update_service(test_provider_publish): + provider=capif_provider_connector(capif_sdk_config_path) + APF1 = provider.provider_capif_ids['APF-1'] + AEF1 = provider.provider_capif_ids['AEF-1'] + provider.publish_req['publisher_apf_id'] = APF1 + provider.publish_req['publisher_aefs_ids'] = [AEF1] + service_api_id = provider.provider_service_ids["API of dummy Network-App to test"] + provider.publish_req['service_api_id'] = service_api_id + provider.api_description_path="test1.json" + + provider.update_service() + + +def preparation_for_update(APFs, AEFs,capif_provider_connector): + + capif_provider_connector.apfs = APFs + capif_provider_connector.aefs = AEFs + + return capif_provider_connector + + +