diff --git a/README.md b/README.md index cda2d468c6ba86c06e184596940ac26e6701511a..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. @@ -64,7 +64,7 @@ Network Apps can be developed by third-party service providers, network operator Next image illustrates how CAPIF works and where the SDK provides means to integrate with it: - + For that purpose Network Apps play 2 different roles when interacting with CAPIF: - **Invoker**: a Network App acting as an Invoker is responsible for consuming APIs exposed by other services. This role represents an external application or service that calls the 3GPP northbound APIs to utilize the network’s functionalities. @@ -95,8 +95,12 @@ 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** +| /capif-events/v1/{subscriberId}/subscriptions/{subscriptionId} (PATCH) | [patch_subscription(name, id)](./doc/sdk_full_documentation.md#patch_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** NOTE: Above mentioned CAPIF APIs are defined in these 3GPP references: - [CAPIF Invoker API specification](https://github.com/jdegre/5GC_APIs/blob/Rel-18/TS29222_CAPIF_API_Invoker_Management_API.yaml) @@ -106,7 +110,7 @@ NOTE: Above mentioned CAPIF APIs are defined in these 3GPP references: - [CAPIF Security API specification](https://github.com/jdegre/5GC_APIs/blob/Rel-18/TS29222_CAPIF_Security_API.yaml) - [AEF Security API specification](https://github.com/jdegre/5GC_APIs/blob/Rel-18/TS29222_AEF_Security_API.yaml) - [CAPIF Logging API management](https://github.com/jdegre/5GC_APIs/blob/Rel-18/TS29222_CAPIF_Logging_API_Invocation_API.yaml) - +- [CAPIF Events API management](https://github.com/jdegre/5GC_APIs/blob/Rel-18/TS29222_CAPIF_Events_API.yaml) NOTE: In the [3GPP Technical Specification (TS) 29.222 V18.5.0 Common API Framework for 3GPP Northbound APIs](https://www.etsi.org/deliver/etsi_ts/129200_129299/129222/18.05.00_60/ts_129222v180500p.pdf) the `service` concept is understood as equal as the `API` concept. @@ -122,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 ``` @@ -132,7 +138,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 @@ -144,7 +150,7 @@ Here is a good explanation about how a usual flow of a Network App should work: A Network App development running as a Provider would typically follow this process step by step, making use of the SDK: - + Now, it is described in 4 simple steps how a Provider can be developed in just some code lines, below snippet. It describes the usual flow a Provider would follow to publish an API service. @@ -155,7 +161,7 @@ Now, it is described in 4 simple steps how a Provider can be developed in just s provider.onboard_provider() #translator = opencapif_sdk.api_schema_translator("./path/to/openapi.yaml") - #translator.build("api_description_name",ip="0.0.0.0",port=9090) + #translator.build("api_description_name",ip="0.0.0.0",port=9090,supported_features="0",api_supp_features="0") provider.api_description_path = "./api_description_name.json" APF = provider.provider_capif_ids["APF-1"] @@ -228,7 +234,7 @@ The `provider_capif_ids` variable stores the `provider_capif_ids.json` content i A Network App development running as an Invoker would typically follow this process step by step, making use of the SDK: - + Now, it is described in some simple steps how an Invoker can be developed in just some code lines. Find below the code snippet. It describes the usual flow an Invoker would follow to consume APIs from CAPIF. @@ -269,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. @@ -317,9 +322,9 @@ There are some features which **are not currently available at latest OpenCAPIF - [CAPIF Access control policy management](https://github.com/jdegre/5GC_APIs/blob/Rel-18/TS29222_CAPIF_Access_Control_Policy_API.yaml) - [CAPIF Auditing API management](https://github.com/jdegre/5GC_APIs/blob/Rel-18/TS29222_CAPIF_Auditing_API.yaml) - - [CAPIF Events API management](https://github.com/jdegre/5GC_APIs/blob/Rel-18/TS29222_CAPIF_Events_API.yaml) - [CAPIF Routing info API management](https://github.com/jdegre/5GC_APIs/blob/Rel-18/TS29222_CAPIF_Routing_Info_API.yaml) - [CAPIF Security API management](https://github.com/jdegre/5GC_APIs/blob/Rel-18/TS29222_CAPIF_Security_API.yaml) - /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/README_pipy.md b/README_pipy.md index 908a9e27e5cf7929902ed27213c3ce431b980b34..09b4ab309662bc98726170582248b9f53ad66ada 100644 --- a/README_pipy.md +++ b/README_pipy.md @@ -44,6 +44,11 @@ OpenCAPIF SDK brings a set of functions to integrate with the 5G Core's function | /{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` +| /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** +| /capif-events/v1/{subscriberId}/subscriptions/{subscriptionId} (PATCH) | [patch_subscription(name, id)](./doc/sdk_full_documentation.md#patch_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** NOTE: Above mentioned CAPIF APIs are defined in these 3GPP references: - [CAPIF Invoker API specification](https://github.com/jdegre/5GC_APIs/blob/Rel-18/TS29222_CAPIF_API_Invoker_Management_API.yaml) @@ -52,10 +57,10 @@ NOTE: Above mentioned CAPIF APIs are defined in these 3GPP references: - [CAPIF Publish API specification](https://github.com/jdegre/5GC_APIs/blob/Rel-18/TS29222_CAPIF_Publish_Service_API.yaml) - [CAPIF Security API specification](https://github.com/jdegre/5GC_APIs/blob/Rel-18/TS29222_CAPIF_Security_API.yaml) - [AEF Security API specification](https://github.com/jdegre/5GC_APIs/blob/Rel-18/TS29222_AEF_Security_API.yaml) - +- [CAPIF Logging API management](https://github.com/jdegre/5GC_APIs/blob/Rel-18/TS29222_CAPIF_Logging_API_Invocation_API.yaml) +- [CAPIF Events API management](https://github.com/jdegre/5GC_APIs/blob/Rel-18/TS29222_CAPIF_Events_API.yaml) NOTE: In the [3GPP Technical Specification (TS) 29.222 V18.5.0 Common API Framework for 3GPP Northbound APIs](https://www.etsi.org/deliver/etsi_ts/129200_129299/129222/18.05.00_60/ts_129222v180500p.pdf) the `service` concept is understood as equal as the `API` concept. - ## OpenCAPIF SDK requirements To use the OpenCAPIF SDK, a registered user account within the target CAPIF instance is required. diff --git a/config/capif_sdk_config.json b/config/capif_sdk_config.json index f76613236f6d5319187c3be9b169a79127ae7846..4fc7f767c9a4149816d43377d56363328d9c148b 100644 --- a/config/capif_sdk_config.json +++ b/config/capif_sdk_config.json @@ -9,10 +9,10 @@ "invoker": { "invoker_folder": "", "capif_callback_url": "", - "supported_features":"", - "check_authentication_data":{ - "ip":"", - "port":"" + "supported_features": "", + "check_authentication_data": { + "ip": "", + "port": "" }, "cert_generation": { "csr_common_name": "", @@ -37,21 +37,21 @@ "api-supported-features": "", "ue-ip-addr": "", "service-kpis": "" + }, + "events": { + "description": [""], + "eventFilters": [ + { + "apiIds": [""], + "apiInvokerIds": [""], + "aefIds": [""] + } + ] } }, "provider": { "provider_folder": "", "supported_features": "", - "apfs": "", - "aefs": "", - "publish_req": { - "service_api_id": "", - "publisher_apf_id": "", - "publisher_aefs_ids": [ - "", - "" - ] - }, "cert_generation": { "csr_common_name": "", "csr_organizational_unit": "", @@ -61,8 +61,30 @@ "csr_country_name": "", "csr_email_address": "" }, + "apfs": "", + "aefs": "", + "publish_req": { + "service_api_id": "", + "publisher_apf_id": "", + "publisher_aefs_ids": ["", ""] + }, "api_description_path": "", - "log":{ + "events": { + "description": [""], + "eventFilters": [ + { + "apiIds": [""], + "apiInvokerIds": [""], + "aefIds": [""] + } + ], + "notificationDestination": "", + "websockNotifConfig": { + "websocketUri": "", + "requestWebsocketUri": false + } + }, + "log": { "apiName": "", "apiVersion": "", "resourceName": "", @@ -71,5 +93,5 @@ "operation": "", "result": "" } - } + } } diff --git a/doc/images/capif_provider_details_example.png b/doc/images/capif_provider_details_example.png deleted file mode 100644 index a22b797c2bd2328a0b0e63bdedad98e6b46e94ff..0000000000000000000000000000000000000000 Binary files a/doc/images/capif_provider_details_example.png and /dev/null differ diff --git a/doc/images/flows_capif_illustration.jpg b/doc/images/flows-capif_illustration.jpg similarity index 100% rename from doc/images/flows_capif_illustration.jpg rename to doc/images/flows-capif_illustration.jpg 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-event_subscription.jpg b/doc/images/flows-event_subscription.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e2315231bc3d0385c78146ba118d26e4d38426fd Binary files /dev/null and b/doc/images/flows-event_subscription.jpg differ diff --git a/doc/images/flows-invoker_check_authentication.jpg b/doc/images/flows-invoker_check_authentication.jpg new file mode 100644 index 0000000000000000000000000000000000000000..589a305a3c7a3909b32b1be60cc105000d0dc3d5 Binary files /dev/null and b/doc/images/flows-invoker_check_authentication.jpg differ diff --git a/doc/images/flows-invoker_discover.jpg b/doc/images/flows-invoker_discover.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e51f04600fe2bd25686fb627c5084873b8c81fed Binary files /dev/null and b/doc/images/flows-invoker_discover.jpg differ diff --git a/doc/images/flows-invoker_get_tokens.jpg b/doc/images/flows-invoker_get_tokens.jpg new file mode 100644 index 0000000000000000000000000000000000000000..103966840ac019aed2cda9a4535a6b225e9bba37 Binary files /dev/null and b/doc/images/flows-invoker_get_tokens.jpg differ diff --git a/doc/images/flows-invoker_onboard.jpg b/doc/images/flows-invoker_onboard.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a80a5ebdda7b539ddbbfedb5718feb064324bc74 Binary files /dev/null and b/doc/images/flows-invoker_onboard.jpg differ diff --git a/doc/images/flows-invoker_path.jpg b/doc/images/flows-invoker_path.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6879f18f3ee43da6d9537d326e7885a2560e15c9 Binary files /dev/null and b/doc/images/flows-invoker_path.jpg differ diff --git a/doc/images/flows-invoker_update_offboard.jpg b/doc/images/flows-invoker_update_offboard.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b1aef970f23cb65f82f24940dcaa16d2aa8533a2 Binary files /dev/null and b/doc/images/flows-invoker_update_offboard.jpg differ diff --git a/doc/images/flows-provider_logs.jpg b/doc/images/flows-provider_logs.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f200d091af66958a0c5c6924cf4e28c64227c310 Binary files /dev/null and b/doc/images/flows-provider_logs.jpg differ diff --git a/doc/images/flows-provider_onboard.jpg b/doc/images/flows-provider_onboard.jpg new file mode 100644 index 0000000000000000000000000000000000000000..85c4662ba81fbbcaadbe5a66080a906a5d24819c Binary files /dev/null and b/doc/images/flows-provider_onboard.jpg differ diff --git a/doc/images/flows-provider_path.jpg b/doc/images/flows-provider_path.jpg new file mode 100644 index 0000000000000000000000000000000000000000..253b828ff2954e1bcee57e54c3e45571132554de Binary files /dev/null and b/doc/images/flows-provider_path.jpg differ diff --git a/doc/images/flows-provider_publish_functions.jpg b/doc/images/flows-provider_publish_functions.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8104ee61855cd0a911cf47bff4af2c7b7e321b3a Binary files /dev/null and b/doc/images/flows-provider_publish_functions.jpg differ diff --git a/doc/images/flows-provider_update_offboard.jpg b/doc/images/flows-provider_update_offboard.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4c8c4a3f9b16d5795e17f4cb29aaf0c367c18de3 Binary files /dev/null and b/doc/images/flows-provider_update_offboard.jpg differ diff --git a/doc/images/flows_sdk_with_register.jpg b/doc/images/flows-sdk_with_register.jpg similarity index 100% rename from doc/images/flows_sdk_with_register.jpg rename to doc/images/flows-sdk_with_register.jpg diff --git a/doc/images/flows-updated_opencapif .jpg b/doc/images/flows-updated_opencapif .jpg new file mode 100644 index 0000000000000000000000000000000000000000..6e2ac44952889f949e79a62fca210d798ecfa57e Binary files /dev/null and b/doc/images/flows-updated_opencapif .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/images/flows_invoker_check_authentication.jpg b/doc/images/flows_invoker_check_authentication.jpg deleted file mode 100644 index 29a348a902dd47f492e6bb2a29102b107cb2a003..0000000000000000000000000000000000000000 Binary files a/doc/images/flows_invoker_check_authentication.jpg and /dev/null differ diff --git a/doc/images/flows_invoker_discover.jpg b/doc/images/flows_invoker_discover.jpg deleted file mode 100644 index 8d74e9b3c716136fde6d5ab7ee9a0af4302ff148..0000000000000000000000000000000000000000 Binary files a/doc/images/flows_invoker_discover.jpg and /dev/null differ diff --git a/doc/images/flows_invoker_get_tokens.jpg b/doc/images/flows_invoker_get_tokens.jpg deleted file mode 100644 index 744496ced00b34ab994e7f04c0021d1075119ecb..0000000000000000000000000000000000000000 Binary files a/doc/images/flows_invoker_get_tokens.jpg and /dev/null differ diff --git a/doc/images/flows_invoker_onboard.jpg b/doc/images/flows_invoker_onboard.jpg deleted file mode 100644 index ba7f69e258d420cd0cdb61b351f2ced7c554c3fb..0000000000000000000000000000000000000000 Binary files a/doc/images/flows_invoker_onboard.jpg and /dev/null differ diff --git a/doc/images/flows_invoker_path.jpg b/doc/images/flows_invoker_path.jpg deleted file mode 100644 index 2cba9d6a1f7db7967501e5aec2845d223c979d55..0000000000000000000000000000000000000000 Binary files a/doc/images/flows_invoker_path.jpg and /dev/null differ diff --git a/doc/images/flows_invoker_update_offboard.jpg b/doc/images/flows_invoker_update_offboard.jpg deleted file mode 100644 index 7944c6bd51a18fb028fd7b074a2058a09cfc9860..0000000000000000000000000000000000000000 Binary files a/doc/images/flows_invoker_update_offboard.jpg and /dev/null differ diff --git a/doc/images/flows_provider_onboard.jpg b/doc/images/flows_provider_onboard.jpg deleted file mode 100644 index b352ea3de7944f4ca573b6c091fa26993fa33739..0000000000000000000000000000000000000000 Binary files a/doc/images/flows_provider_onboard.jpg and /dev/null differ diff --git a/doc/images/flows_provider_path.jpg b/doc/images/flows_provider_path.jpg deleted file mode 100644 index f80c48277993e7b9073c2d8c0892054bdd921e38..0000000000000000000000000000000000000000 Binary files a/doc/images/flows_provider_path.jpg and /dev/null differ diff --git a/doc/images/flows_provider_publish_functions.jpg b/doc/images/flows_provider_publish_functions.jpg deleted file mode 100644 index 38dec6ee58618f3e540c340f4e1eefd3a492e116..0000000000000000000000000000000000000000 Binary files a/doc/images/flows_provider_publish_functions.jpg and /dev/null differ diff --git a/doc/images/flows_provider_update_offboard.jpg b/doc/images/flows_provider_update_offboard.jpg deleted file mode 100644 index 09ad5c7e1633eda2df037d15eb0b3979213355eb..0000000000000000000000000000000000000000 Binary files a/doc/images/flows_provider_update_offboard.jpg and /dev/null differ diff --git a/doc/images/flows_updated_opencapif.jpg b/doc/images/flows_updated_opencapif.jpg deleted file mode 100644 index eb7c724cabe315da35c08f5025f2f4a1bf5daedd..0000000000000000000000000000000000000000 Binary files a/doc/images/flows_updated_opencapif.jpg and /dev/null differ diff --git a/doc/images/publish_req_example.png b/doc/images/publish_req_example.png deleted file mode 100644 index ace48252f76e40edebbfe82717a010009d87851a..0000000000000000000000000000000000000000 Binary files a/doc/images/publish_req_example.png and /dev/null differ diff --git a/doc/sdk_configuration.md b/doc/sdk_configuration.md index f5ee0cc0656ad5f396572d1a525affb33377fc40..4a46a83ff79cc03de67b002863fb032c4925d645 100644 --- a/doc/sdk_configuration.md +++ b/doc/sdk_configuration.md @@ -32,7 +32,7 @@ When configuring the SDK as a **Network App Invoker**, the following fields must - `invoker_folder` - `capif_callback_url` - `supported_features` -- `cert_generation` (fields such as `csr_common_name`, `csr_country_name`, etc.) +- `cert_generation` (fields such as `csr_common_name`, `csr_country_name`, etc.) For csr_country_name it is important to fulfill the field with [THIS format](https://www.ssl.com/country-codes/) **Optional:** - `discover_filter`: useful to enable the discovery of specific APIs. Some fields under [`discover_filter`](#configuration-of-discover_filter) structure required to be configured when using discovery filters. Check devoted section below, @@ -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. @@ -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 32d9f43021fa51a29162ef78f31c7667b7a0a34e..990c0a8b556b8c9e251e9296b687c34c8866445f 100644 --- a/doc/sdk_full_documentation.md +++ b/doc/sdk_full_documentation.md @@ -15,7 +15,7 @@ Before using the SDK, the following steps should be completed: ## Available SDK Usage Modes - + The repository provides two modes for utilizing the OpenCAPIF SDK: @@ -77,7 +77,7 @@ OpenCAPIF SDK references: The SDK simplifies the onboarding process, allowing providers to register multiple APFs and AEFs. All APFs, AEFs, and AMF certificates are created and stored in `provider_service_ids.json`. - + ### Service Publishing @@ -148,7 +148,7 @@ Retrieve information about all previously published services in `service_receive **Required SDK input**: - publisher_apf_id - + ### Update and Offboard Provider @@ -161,12 +161,12 @@ OpenCAPIF SDK references: 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,9 +174,83 @@ 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) + + +### Create subscription + +OpenCAPIF SDK references: +- **Function**: `create_subscription(name, id)` + +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#descriptions-of-capif_sdk_config-fields) + +For leveraging this feature the Provider must have onboarded previously. + +**Required SDK input**: + +- 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#descriptions-of-capif_sdk_config-fields) or object) + +### Delete subscription + +OpenCAPIF SDK references: +- **Function**: `delete_subscription(name, id)` + +The provider ask to the CCF to withdraw the subscription to the notifications asked previously + +For leveraging this feature the Provider must have onboarded and created a subscription previously. + +**Required SDK input**: + +- aefId//apfId//amfId (Within the function) +- name: The name of your subscription. + +### Update subscription + +OpenCAPIF SDK references: +- **Function**: `update_subscription(name, id)` + +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#descriptions-of-capif_sdk_config-fields) + +For leveraging this feature the Provider must have onboarded and created a subscription previously. + + + +**ONLY AVAILABLE IN CAPIF RELEASE 2** + +**Required SDK input**: + +- aefId//apfId//amfId (Within the function) +- name: The name of your subscription. +- events (Within [SDK configuration](./sdk_configuration.md#events_configuration) or object) + +### Patch subscription + +OpenCAPIF SDK references: +- **Function**: `update_subscription(name, id)` + +The provider ask to the CCF about updating the subscription for receiving different 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) + +For leveraging this feature the Provider must have onboarded and created a subscription previously. + +**ONLY AVAILABLE IN CAPIF RELEASE 2** + +**Required SDK input**: + +- aefId//apfId//amfId (Within the function) +- name: The name of your subscription. +- events (Within [SDK configuration](./sdk_configuration.md#events_configuration) or object) + + ## Invoker Network App The OpenCAPIF SDK enables efficient implementation of invoker functionality for Network App. This section details the SDK features related to CAPIF invokers. @@ -202,7 +276,7 @@ OpenCAPIF SDK references: The SDK streamlines the invoker onboarding process, storing the `api_invoker_id` in the `capif_api_security_context_details.json`. - + ### Service Discovery @@ -216,7 +290,7 @@ The [discover_filter](./sdk_configuration.md) can be used to retrieve access to Use the [discover_filter](./sdk_configuration.md) to retrieve access to target APIs. Ensure you are [onboarded as an invoker](#invoker-onboarding) before using this feature. - + ### Obtain JWT Tokens @@ -226,12 +300,12 @@ OpenCAPIF SDK references: The SDK facilitates JWT token creation for secure access to target APIs. This process stores JWT access token in `capif_api_security_context_details.json`. - + ### 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). @@ -240,7 +314,7 @@ It is mandatory to have obtained the [JWT token](#obtain-jwt-tokens) previously. **Required SDK inputs**: - check_authentication_data - + ### Update and Offboard Invoker @@ -250,7 +324,7 @@ OpenCAPIF SDK references: Onboarding is required before utilizing these functions. - + ## Other Features @@ -263,9 +337,10 @@ This schema could be obtained by applying this code. import opencapif_sdk translator = opencapif_sdk.api_schema_translator("./path/to/openapi.yaml") - translator.build("api_description_name",ip="0.0.0.0",port=9090) + translator.build("api_description_name",ip="0.0.0.0",port=9090,supported_features="0",api_supp_features="0") ``` This code will read `openapi.yaml`, ensure the structure of it and translate the content into ServiceAPIDescription schema, then will create a .json named `api_description_name`. Also it is necessary to fill the ip and port fields to create correctly the schema. +The supported_features and api_supp_features fields corresponds to the capabilities of the provider and the service that the user is sharing. ### CAPIF Registration and Login @@ -281,4 +356,4 @@ OpenCAPIF SDK reference: Simplifies the logout process for admin users and removes a CAPIF user. - + diff --git a/opencapif_sdk/__init__.py b/opencapif_sdk/__init__.py index b977cb1451bf04b91f3b305bfd736519ca01c76a..4137e12c3451336a680699d82a8795c3bcfe9f68 100644 --- a/opencapif_sdk/__init__.py +++ b/opencapif_sdk/__init__.py @@ -3,5 +3,6 @@ from opencapif_sdk.capif_provider_connector import capif_provider_connector from opencapif_sdk.service_discoverer import service_discoverer from opencapif_sdk.api_schema_translator import api_schema_translator from opencapif_sdk.capif_logging_feature import capif_logging_feature +from opencapif_sdk.capif_event_feature import capif_invoker_event_feature, capif_provider_event_feature -__all__ = ["capif_invoker_connector", "service_discoverer", "capif_provider_connector", "api_schema_translator", "capif_logging_feature"] \ No newline at end of file +__all__ = ["capif_invoker_connector", "service_discoverer", "capif_provider_connector", "api_schema_translator", "capif_logging_feature", "capif_invoker_event_feature", "capif_provider_event_feature"] \ No newline at end of file diff --git a/opencapif_sdk/api_schema_translator.py b/opencapif_sdk/api_schema_translator.py index 12519903c5df540483c3b15b12a8c5df686d7aa1..a55ae0c34bb8dd20ea3d830283d3ffde6af3d0e9 100644 --- a/opencapif_sdk/api_schema_translator.py +++ b/opencapif_sdk/api_schema_translator.py @@ -34,31 +34,53 @@ 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, supported_features, api_supp_features, 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 required fields + if not supported_features or not api_supp_features: + self.logger.error("Both 'supported_features' and 'api_supp_features' are required. Aborting build.") + return + + # 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": supported_features, + "shareableInfo": { + "isShareable": True, + "capifProvDoms": ["string"] + }, + "serviceAPICategory": "string", + "apiSuppFeats": api_supp_features, + "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 +112,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 +129,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 +181,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_event_feature.py b/opencapif_sdk/capif_event_feature.py new file mode 100644 index 0000000000000000000000000000000000000000..90b4664efaca5c31bc547b71ff361c710444796a --- /dev/null +++ b/opencapif_sdk/capif_event_feature.py @@ -0,0 +1,424 @@ +from opencapif_sdk import capif_invoker_connector, capif_provider_connector +import os +import logging +import urllib3 +import requests +import warnings +from requests.exceptions import RequestsDependencyWarning +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) +warnings.filterwarnings("ignore", category=RequestsDependencyWarning) +# noqa: E501 +# Basic configuration of the logger functionality + +log_path = 'logs/sdk_logs.log' + +log_dir = os.path.dirname(log_path) + +if not os.path.exists(log_dir): + os.makedirs(log_dir) + +logging.basicConfig( + level=logging.NOTSET, # Minimum severity level to log + # Log message format + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler(log_path), # Log to a file + logging.StreamHandler() # Also display in the console + ] +) + + +class capif_invoker_event_feature(capif_invoker_connector): + + def create_subscription(self, name): + + invoker_capif_details = self.invoker_capif_details + + subscriberId = invoker_capif_details["api_invoker_id"] + + path = self.capif_https_url + f"capif-events/v1/{subscriberId}/subscriptions" + + payload = { + "events": self.events_description, + "eventFilters": self.events_filter, + "eventReq": {}, # TO IMPROVE !!! + "notificationDestination": f"{self.capif_callback_url}", + "requestTestNotification": True, + "websockNotifConfig": { + "websocketUri": f"{self.capif_callback_url}", + "requestWebsocketUri": True + }, + "supportedFeatures": f"{self.supported_features}" + } + + try: + response = requests.post( + url=path, + json=payload, + headers={"Content-Type": "application/json"}, + cert=(self.signed_key_crt_path, self.private_key_path), + verify=os.path.join(self.invoker_folder, "ca.crt") + ) + response.raise_for_status() + location_header = response.headers.get("Location") + + if location_header: + # Extrae el identificador de la URL en el encabezado 'Location' + identifier = location_header.rstrip('/').split('/')[-1] + self.logger.info(f"Subscriptionid obtained: {identifier}") + else: + self.logger.error("The Location header is not available in the response") + + path = os.path.join(self.invoker_folder, "capif_subscriptions_id.json") + + # Load or initialize the subscription dictionary + # Load or initialize the subscription dictionary + if os.path.exists(path): + subscription = self._load_config_file(path) + if not isinstance(subscription, dict): + raise TypeError(f"Expected 'subscription' to be a dict, but got {type(subscription).__name__}") + else: + subscription = {} + + if not isinstance(subscriberId, (str, int)): + raise TypeError(f"Expected 'subscriberId' to be a string or integer, but got {type(subscriberId).__name__}") + + # Convert events_description to a string if it isn't already + if not isinstance(name, str): + name = str(name) + + if str(subscriberId) not in subscription: + # If the subscriberId is not in the subscription, create an empty dictionary for it + subscription[str(subscriberId)] = {} + # Update the subscription structure + subscription[str(subscriberId)][name] = identifier + + # Save the updated dictionary back to the file + self._create_or_update_file("capif_subscriptions_id", "json", subscription, "w") + + except Exception as e: + self.logger.error("Unexpected error: %s", e) + return None, {"error": f"Unexpected error: {e}"} + + def delete_subscription(self, name): + invoker_capif_details = self.invoker_capif_details + + subscriberId = invoker_capif_details["api_invoker_id"] + + path = os.path.join(self.invoker_folder, "capif_subscriptions_id.json") + + if os.path.exists(path): + subscription = self._load_config_file(path) + if not isinstance(subscription, dict): + raise TypeError(f"Expected 'subscription' to be a dict, but got {type(subscription).__name__}") + + if subscriberId in subscription and name in subscription[subscriberId]: + identifier = subscription[subscriberId][name] + + # Attempt to delete the subscription from CAPIF + delete_path = self.capif_https_url + f"capif-events/v1/{subscriberId}/subscriptions/{identifier}" + + try: + response = requests.delete( + url=delete_path, + headers={"Content-Type": "application/json"}, + cert=(self.signed_key_crt_path, self.private_key_path), + verify=os.path.join(self.invoker_folder, "ca.crt") + ) + response.raise_for_status() + + # Remove the service entry from the subscription dictionary + del subscription[subscriberId][name] + + # If no more services exist for the subscriber, remove the subscriber entry + if not subscription[subscriberId]: + del subscription[subscriberId] + + # Save the updated dictionary back to the file + self._create_or_update_file("capif_subscriptions_id", "json", subscription, "w") + + self.logger.info(f"Successfully deleted subscription for service '{name}'") + + except Exception as e: + self.logger.error("Unexpected error: %s", e) + return None, {"error": f"Unexpected error: {e}"} + + else: + self.logger.warning(f"Service '{name}' not found for subscriber '{subscriberId}'") + return None, {"error": f"Service '{name}' not found for subscriber '{subscriberId}'"} + else: + self.logger.error("Subscription file not found at path: %s", path) + return None, {"error": "Subscription file not found"} + + def update_subcription(self, name): + invoker_capif_details = self.invoker_capif_details + + subscriberId = invoker_capif_details["api_invoker_id"] + + path = os.path.join(self.invoker_folder, "capif_subscriptions_id.json") + + payload = { + "events": self.events_description, + "eventFilters": self.events_filter, + "eventReq": {}, # TO IMPROVE !!! + "notificationDestination": f"{self.capif_callback_url}", + "requestTestNotification": True, + "websockNotifConfig": { + "websocketUri": f"{self.capif_callback_url}", + "requestWebsocketUri": True + }, + "supportedFeatures": f"{self.supported_features}" + } + if os.path.exists(path): + subscription = self._load_config_file(path) + if not isinstance(subscription, dict): + raise TypeError(f"Expected 'subscription' to be a dict, but got {type(subscription).__name__}") + + if subscriberId in subscription and name in subscription[subscriberId]: + identifier = subscription[subscriberId][name] + + # Attempt to delete the subscription from CAPIF + put_path = self.capif_https_url + f"capif-events/v1/{subscriberId}/subscriptions/{identifier}" + + try: + response = requests.put( + url=put_path, + json=payload, + headers={"Content-Type": "application/json"}, + cert=(self.signed_key_crt_path, self.private_key_path), + verify=os.path.join(self.invoker_folder, "ca.crt") + ) + response.raise_for_status() + + self.logger.info(f"Successfully updated subscription for service '{name}'") + + except Exception as e: + self.logger.error("Unexpected error: %s", e) + return None, {"error": f"Unexpected error: {e}"} + + else: + self.logger.warning(f"Service '{name}' not found for subscriber '{subscriberId}'") + return None, {"error": f"Service '{name}' not found for subscriber '{subscriberId}'"} + else: + self.logger.error("Subscription file not found at path: %s", path) + return None, {"error": "Subscription file not found"} + + def patch_subcription(self, name): + self.update_subcription(self, name) + + +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 = { + "events": self.events_description, + "eventFilters": self.events_filter, + "eventReq": {}, # TO IMPROVE !!! + "notificationDestination": f"{self.notification_destination}", + "requestTestNotification": True, + "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, + json=payload, + headers={"Content-Type": "application/json"}, + cert=cert, + verify=os.path.join(self.provider_folder, "ca.crt") + ) + response.raise_for_status() + + location_header = response.headers.get("Location") + + if location_header: + # Extrae el identificador de la URL en el encabezado 'Location' + identifier = location_header.rstrip('/').split('/')[-1] + self.logger.info(f"Subscriptionid obtained: {identifier}") + else: + self.logger.error("The Location header is not available in the response") + + path = os.path.join(self.provider_folder, "capif_subscriptions_id.json") + + # Load or initialize the subscription dictionary + # Load or initialize the subscription dictionary + if os.path.exists(path): + subscription = self._load_config_file(path) + if not isinstance(subscription, dict): + raise TypeError(f"Expected 'subscription' to be a dict, but got {type(subscription).__name__}") + else: + subscription = {} + + if not isinstance(subscriberId, (str, int)): + raise TypeError(f"Expected 'subscriberId' to be a string or integer, but got {type(subscriberId).__name__}") + + # Convert events_description to a string if it isn't already + if not isinstance(name, str): + name = str(name) + + if str(subscriberId) not in subscription: + # If the subscriberId is not in the subscription, create an empty dictionary for it + subscription[str(subscriberId)] = {} + # Update the subscription structure + subscription[str(subscriberId)][name] = identifier + + # Save the updated dictionary back to the file + self._create_or_update_file("capif_subscriptions_id", "json", subscription, "w") + + 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 + + path = os.path.join(self.provider_folder, "capif_subscriptions_id.json") + + if os.path.exists(path): + subscription = self._load_config_file(path) + if not isinstance(subscription, dict): + raise TypeError(f"Expected 'subscription' to be a dict, but got {type(subscription).__name__}") + + if subscriberId in subscription and name in subscription[subscriberId]: + identifier = subscription[subscriberId][name] + + # 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"), + ) + + try: + response = requests.delete( + url=delete_path, + headers={"Content-Type": "application/json"}, + cert=cert, + verify=os.path.join(self.provider_folder, "ca.crt") + ) + response.raise_for_status() + + # Remove the service entry from the subscription dictionary + del subscription[subscriberId][name] + + # If no more services exist for the subscriber, remove the subscriber entry + if not subscription[subscriberId]: + del subscription[subscriberId] + + # Save the updated dictionary back to the file + self._create_or_update_file("capif_subscriptions_id", "json", subscription, "w") + + self.logger.info(f"Successfully deleted subscription for service '{name}'") + + except Exception as e: + self.logger.error("Unexpected error: %s", e) + return None, {"error": f"Unexpected error: {e}"} + + else: + self.logger.warning(f"Service '{name}' not found for subscriber '{subscriberId}'") + return None, {"error": f"Service '{name}' not found for subscriber '{subscriberId}'"} + 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 = { + "events": self.events_description, + "eventFilters": self.events_filter, + "eventReq": {}, # TO IMPROVE !!! + "notificationDestination": f"{self.notification_destination}", + "requestTestNotification": True, + "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): + raise TypeError(f"Expected 'subscription' to be a dict, but got {type(subscription).__name__}") + + if subscriberId in subscription and name in subscription[subscriberId]: + identifier = subscription[subscriberId][name] + + # 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"), + ) + + try: + response = requests.put( + url=put_path, + json=payload, + headers={"Content-Type": "application/json"}, + cert=cert, + verify=os.path.join(self.provider_folder, "ca.crt") + ) + response.raise_for_status() + + # Remove the service entry from the subscription dictionary + del subscription[subscriberId][name] + + # If no more services exist for the subscriber, remove the subscriber entry + if not subscription[subscriberId]: + del subscription[subscriberId] + + # Save the updated dictionary back to the file + self._create_or_update_file("capif_subscriptions_id", "json", subscription, "w") + + self.logger.info(f"Successfully updated subscription for service '{name}'") + + except Exception as e: + self.logger.error("Unexpected error: %s", e) + return None, {"error": f"Unexpected error: {e}"} + + else: + self.logger.warning(f"Service '{name}' not found for subscriber '{subscriberId}'") + return None, {"error": f"Service '{name}' not found for subscriber '{subscriberId}'"} + else: + self.logger.error("Subscription file not found at path: %s", path) + return None, {"error": "Subscription file not found"} + + def patch_subcription(self, name, id): + self.update_subcription(self, name, id) diff --git a/opencapif_sdk/capif_invoker_connector.py b/opencapif_sdk/capif_invoker_connector.py index 6a159f11f6b7fa7fe48f0dfacef32e7ccb2ac973..7a3753c0f5ca4daf2c8433cbe1a105aa8fd662f6 100644 --- a/opencapif_sdk/capif_invoker_connector.py +++ b/opencapif_sdk/capif_invoker_connector.py @@ -42,11 +42,12 @@ 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) # Load configuration from file if necessary - config = self.__load_config_file(config_file) + config = self._load_config_file(config_file) debug_mode = os.getenv('DEBUG_MODE', config.get('debug_mode', 'False')).strip().lower() if debug_mode == "false": @@ -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', {}) @@ -99,10 +100,17 @@ class capif_invoker_connector: csr_country_name = os.getenv('INVOKER_CSR_COUNTRY_NAME', csr_config.get('csr_country_name', '')).strip() csr_email_address = os.getenv('INVOKER_CSR_EMAIL_ADDRESS', csr_config.get('csr_email_address', '')).strip() + # Events configuration + events_config = invoker_config.get('events', {}) + self.events_description = os.getenv('INVOKER_EVENTS_DESCRIPTION', events_config.get('description', '')) + self.events_filter = os.getenv('INVOKER_EVENTS_FILTERS', events_config.get('eventFilters', '')) + # 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) - self.supported_features = supported_features + if supported_features is None: + supported_features = 0 + 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: @@ -138,9 +146,21 @@ 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.private_key_path = os.path.join( + 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") - def __load_config_file(self, config_file: str): + def _load_config_file(self, config_file: str): """Loads the configuration file.""" try: with open(config_file, 'r') as file: @@ -194,25 +214,11 @@ class capif_invoker_connector: + invoker_capif_details["api_invoker_id"] ) - signed_key_crt_path = os.path.join( - self.invoker_folder, - invoker_capif_details["user_name"] + ".crt" - ) - - private_key_path = os.path.join( - self.invoker_folder, - "private.key" - ) - - path = os.path.join( - self.invoker_folder, - "ca.crt" - ) response = requests.request( "DELETE", url, - cert=(signed_key_crt_path, private_key_path), - verify=path, + cert=(self.signed_key_crt_path, self.private_key_path), + verify=self.pathca, ) response.raise_for_status() self.logger.info("Invoker offboarded successfully") @@ -237,7 +243,6 @@ class capif_invoker_connector: self.logger.info( "Creating private and public keys for the Invoker cert") try: - private_key_path = os.path.join(self.invoker_folder, "private.key") csr_file_path = os.path.join(self.invoker_folder, "cert_req.csr") @@ -258,7 +263,7 @@ class capif_invoker_connector: with open(csr_file_path, "wb+") as f: f.write(dump_certificate_request(FILETYPE_PEM, req)) public_key = dump_certificate_request(FILETYPE_PEM, req) - with open(private_key_path, "wb+") as f: + with open(self.private_key_path, "wb+") as f: f.write(dump_privatekey(FILETYPE_PEM, key)) self.logger.info("Keys created successfully") @@ -305,7 +310,7 @@ class capif_invoker_connector: response.raise_for_status() response_payload = json.loads(response.text) - ca_root_file_path = os.path.join(self.invoker_folder, "ca.crt") + ca_root_file_path = self.pathca ca_root_file = open(ca_root_file_path, "wb+") ca_root_file.write(bytes(response_payload["ca_root"], "utf-8")) self.logger.info( @@ -339,13 +344,12 @@ class capif_invoker_connector: "Authorization": "Bearer {}".format(capif_access_token), "Content-Type": "application/json", } - pathca = os.path.join(self.invoker_folder, "ca.crt") response = requests.request( "POST", url, headers=headers, data=payload, - verify=pathca, + verify=self.pathca, ) response.raise_for_status() response_payload = json.loads(response.text) @@ -443,23 +447,14 @@ class capif_invoker_connector: "Authorization": "Bearer {}".format(capif_access_token), "Content-Type": "application/json", } - signed_key_crt_path = os.path.join( - self.invoker_folder, - self.capif_username + ".crt" - ) - private_key_path = os.path.join( - self.invoker_folder, - "private.key" - ) - pathca = os.path.join(self.invoker_folder, "ca.crt") response = requests.request( "PUT", url, headers=headers, data=payload, - cert=(signed_key_crt_path, private_key_path), - verify=pathca, + cert=(self.signed_key_crt_path, self.private_key_path), + verify=self.pathca, ) response.raise_for_status() @@ -472,5 +467,46 @@ class capif_invoker_connector: 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. + + :param file_name: Name of the file (without extension). + :param file_type: File type or extension (e.g., "txt", "json", "html"). + :param content: Content to write into the file. Can be a string, dictionary, or list. + :param mode: Write mode ('w' to overwrite, 'a' to append). Default is 'w'. + """ + # Validate the mode + if mode not in ["w", "a"]: + raise ValueError("Mode must be 'w' (overwrite) or 'a' (append).") + + # Construct the full file name + full_file_name = f"{file_name}.{file_type}" + full_path = os.path.join(self.invoker_folder, full_file_name) + + # Ensure the content is properly formatted + if isinstance(content, (dict, list)): + if file_type == "json": + try: + # Serialize content to JSON + content = json.dumps(content, indent=4) + except TypeError as e: + raise ValueError(f"Failed to serialize content to JSON: {e}") + else: + raise TypeError("Content must be a string when the file type is not JSON.") + elif not isinstance(content, str): + raise TypeError("Content must be a string, dictionary, or list.") + try: + # 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.") + elif mode == "a": + self.logger.info(f"Content appended to file '{full_file_name}' successfully.") + 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 c2f3b42492729ee10d08a4fa5f82134bc665da7b..8c8c2c5c58f3d9219331aed42c8ad516cef13544 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 = { @@ -205,7 +211,7 @@ class capif_logging_feature: "operation": self.log["operation"], "result": self.log["result"] } - + payload = { "aefId": f"{aefId}", "apiInvokerId": f"{api_invoker_id}", @@ -285,3 +291,39 @@ 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 1ea9bc227691095c333e3100d036bf62f18d99d1..58437f1b5c9e306ab121a04a176bbda440980a2e 100644 --- a/opencapif_sdk/capif_provider_connector.py +++ b/opencapif_sdk/capif_provider_connector.py @@ -13,10 +13,10 @@ from OpenSSL.SSL import FILETYPE_PEM import os import logging import shutil -import subprocess from requests.auth import HTTPBasicAuth import urllib3 - +import ssl +import socket urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) @@ -102,7 +102,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,14 +150,14 @@ 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") + + 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") + 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) + self.provider_service_ids = self.__load_config_file(path_published) # Construct the CAPIF HTTPS URL if len(self.capif_https_port) == 0 or int(self.capif_https_port) == 443: @@ -171,6 +171,11 @@ class capif_provider_connector: 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', '')) + self.notification_destination = os.getenv('PROVIDER_EVENTS_FILTERS', events_config.get('notificationDestination', '')) + self.websock_notif_config = os.getenv('PROVIDER_EVENTS_FILTERS', events_config.get('websockNotifConfig', '')) # Log initialization success message self.logger.info("capif_provider_connector initialized with the capif_sdk_config.json parameters") @@ -180,31 +185,22 @@ 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): @@ -258,7 +254,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}", @@ -350,7 +346,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): @@ -427,7 +422,7 @@ class capif_provider_connector: self.logger.info( f"Loading provider details from {provider_details_path}") - provider_details = self.__load_provider_api_details() + provider_details = self._load_provider_api_details() publish_url = provider_details["publish_url"] @@ -457,7 +452,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( @@ -466,7 +460,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__}") @@ -504,7 +498,7 @@ class capif_provider_connector: "description": "Revoke authorization for service APIs." }) i -= 1 - + self.logger.info( "Service API description modified successfully") @@ -533,7 +527,6 @@ class capif_provider_connector: ) self.logger.info(f"Publishing services to URL: {url}") - try: response = requests.post( url, @@ -604,14 +597,13 @@ class capif_provider_connector: self.logger.info( f"Loading provider details from {provider_details_path}") - provider_details = self.__load_provider_api_details() + provider_details = self._load_provider_api_details() publish_url = provider_details["publish_url"] # Load provider details 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-"): @@ -632,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}") @@ -663,7 +654,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 @@ -722,7 +713,7 @@ class capif_provider_connector: self.logger.info( f"Loading provider details from {provider_details_path}") - provider_details = self.__load_provider_api_details() + provider_details = self._load_provider_api_details() publish_url = provider_details["publish_url"] chosenAPFsandAEFs = self.publish_req @@ -790,13 +781,13 @@ 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( f"Loading provider details from {provider_details_path}") - provider_details = self.__load_provider_api_details() + provider_details = self._load_provider_api_details() publish_url = provider_details["publish_url"] chosenAPFsandAEFs = self.publish_req @@ -872,7 +863,7 @@ class capif_provider_connector: self.logger.info( f"Loading provider details from {provider_details_path}") - provider_details = self.__load_provider_api_details() + provider_details = self._load_provider_api_details() publish_url = provider_details["publish_url"] chosenAPFsandAEFs = self.publish_req @@ -946,7 +937,7 @@ class capif_provider_connector: "description": "Revoke authorization for service APIs." }) i -= 1 - + self.logger.info( "Service API description modified successfully") @@ -1071,7 +1062,7 @@ class capif_provider_connector: self.logger.info("Offboarding the provider") # Load CAPIF API details - capif_api_details = self.__load_provider_api_details() + capif_api_details = self._load_provider_api_details() url = f"{self.capif_https_url}api-provider-management/v1/registrations/{capif_api_details['capif_registration_id']}" # Define certificate paths @@ -1120,7 +1111,7 @@ class capif_provider_connector: self.logger.error(f"Error during removing folder contents: {e}") raise - def __load_provider_api_details(self) -> dict: + def _load_provider_api_details(self) -> dict: """ Loads NEF API details from the CAPIF provider details JSON file. @@ -1153,7 +1144,6 @@ 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) @@ -1163,97 +1153,28 @@ class capif_provider_connector: ) 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-"] + 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( "Onboarding Provider to CAPIF and waiting signed certificate by giving our public keys to CAPIF") - api_details = self.__load_provider_api_details() + api_details = self._load_provider_api_details() capif_id = "/" + api_details["capif_registration_id"] url = f"{self.capif_https_url}{capif_onboarding_url}{capif_id}" @@ -1346,3 +1267,70 @@ 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. + + :param file_name: Name of the file (without extension). + :param file_type: File type or extension (e.g., "txt", "json", "html"). + :param content: Content to write into the file. Can be a string, dictionary, or list. + :param mode: Write mode ('w' to overwrite, 'a' to append). Default is 'w'. + """ + # Validate the mode + if mode not in ["w", "a"]: + raise ValueError("Mode must be 'w' (overwrite) or 'a' (append).") + + # Construct the full file name + full_file_name = f"{file_name}.{file_type}" + full_path = os.path.join(self.provider_folder, full_file_name) + + # Ensure the content is properly formatted + if isinstance(content, (dict, list)): + if file_type == "json": + try: + # Serialize content to JSON + content = json.dumps(content, indent=4) + except TypeError as e: + raise ValueError(f"Failed to serialize content to JSON: {e}") + else: + raise TypeError("Content must be a string when the file type is not JSON.") + elif not isinstance(content, str): + raise TypeError("Content must be a string, dictionary, or list.") + + try: + # 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.") + elif mode == "a": + self.logger.info(f"Content appended to file '{full_file_name}' successfully.") + 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. + + :param data: Dictionary to search. + :param target_value: Value to find the corresponding key for. + :return: Key corresponding to the target value, or None if not found. + """ + for key, value in data.items(): + if value == target_value: + return key + return None + + def _load_config_file(self, config_file: str): + """Loads the configuration file.""" + try: + with open(config_file, 'r') as file: + return json.load(file) + except FileNotFoundError: + self.logger.warning( + f"Configuration file {config_file} not found. Using defaults or environment variables.") + return {} diff --git a/opencapif_sdk/service_discoverer.py b/opencapif_sdk/service_discoverer.py index 6272e94ca7e9b6988c0d5a3a4a3b6694ebc8ad2f..5a0915692dc24a26d6a58dc580683eb9db4f8b71 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) @@ -31,7 +31,6 @@ logging.basicConfig( ) - class service_discoverer: class ServiceDiscovererException(Exception): pass @@ -69,18 +68,19 @@ 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( 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 = { "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() @@ -107,6 +107,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,12 +116,12 @@ 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"] - except : + except: pass # Define paths for certificates, private keys, and CA root @@ -130,9 +132,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 +206,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", @@ -218,29 +217,26 @@ 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 - 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, - 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") @@ -268,7 +264,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 +278,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 +463,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.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,34 +503,34 @@ 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() 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"{self.supported_features}" + "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}") diff --git a/scripts/invoker_capif_event_subcription.py b/scripts/invoker_capif_event_subcription.py new file mode 100644 index 0000000000000000000000000000000000000000..a8a95d45dbfcf8eae9c69329cd40438893bcf03f --- /dev/null +++ b/scripts/invoker_capif_event_subcription.py @@ -0,0 +1,30 @@ +import utilities +from opencapif_sdk import capif_invoker_connector, capif_invoker_event_feature + + +def showcase_capif_connector(): + """ + This method showcases how one can use the CAPIFConnector class. + """ + # invoker = capif_invoker_connector(config_file=utilities.get_config_file()) + + # invoker.onboard_invoker() + + events = capif_invoker_event_feature(config_file=utilities.get_config_file()) + + events.create_subscription(name="Servicio_2") + + events.create_subscription(name="Servicio_3") + + # events.update_subcription(name="Servicio_2") + + events.delete_subscription(name="Servicio_2") + + events.delete_subscription(name="Servicio_3") + + print("COMPLETED") + + +if __name__ == "__main__": + # Register invoker to CAPIF. This should happen exactly once + showcase_capif_connector() diff --git a/scripts/provider_capif_event_feature.py b/scripts/provider_capif_event_feature.py new file mode 100644 index 0000000000000000000000000000000000000000..5665369e7a67abf270bfeb97554bf8a4c5324376 --- /dev/null +++ b/scripts/provider_capif_event_feature.py @@ -0,0 +1,32 @@ +import utilities +from opencapif_sdk import capif_provider_connector, capif_provider_event_feature + + +def showcase_capif_connector(): + """ + This method showcases how one can use the CAPIFConnector class. + """ + provider = capif_provider_connector(config_file=utilities.get_config_file()) + + # provider.onboard_provider() + + id = provider.provider_capif_ids['AEF-1'] + + events = capif_provider_event_feature(config_file=utilities.get_config_file()) + + events.create_subscription(name="Servicio_2", id=id) + + events.create_subscription(name="Servicio_3", id=id) + + # events.update_subcription(name="Servicio_2", id=id) + + events.delete_subscription(name="Servicio_2", id=id) + + events.delete_subscription(name="Servicio_3", id=id) + + print("COMPLETED") + + +if __name__ == "__main__": + # Register invoker to CAPIF. This should happen exactly once + showcase_capif_connector() diff --git a/scripts/utilities.py b/scripts/utilities.py index 238f969ea14f4fefe6ce480a856f4c94046c5c1d..0b4855f7c45b122119cfaceba01094038ff2e33f 100755 --- a/scripts/utilities.py +++ b/scripts/utilities.py @@ -1,7 +1,7 @@ def get_config_file() -> str: - return "../config/capif_sdk_config.json" + return "../test/capif_sdk_config_sample_test.json" def get_register_file() -> str: diff --git a/setup.py b/setup.py index 9b80f7301ef731099c682bd20962b2e06056fe29..9d4f0f57a47dbb4043be6e8622ebdc93d95a7d56 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.17.6", + version="0.1.20", 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 7096619f0a2c99cf3b9d746298a1eb42506f48be..21f55edc7e43568fa7f5e33319d50ab2d50669fd 100644 --- a/test/capif_sdk_config_sample_test.json +++ b/test/capif_sdk_config_sample_test.json @@ -1,27 +1,27 @@ { - "capif_host": "", - "register_host": "", - "capif_https_port": "", - "capif_register_port": "", - "capif_username": "", - "capif_password": "", - "debug_mode": "", + "capif_host": "capif-prev.mobilesandbox.cloud", + "register_host": "registercapif-prev.mobilesandbox.cloud", + "capif_https_port": "36212", + "capif_register_port": "36211", + "capif_username": "echeva_0", + "capif_password": "echevapass", + "debug_mode": "True", "invoker": { - "invoker_folder": "", - "capif_callback_url": "", - "supported_features":"", - "check_authentication_data":{ - "ip":"", - "port":"" + "invoker_folder": "/Users/IDB0128/Documents/OpenCapif/test_invoker_certificate_folder", + "capif_callback_url": "http://localhost:5000", + "supported_features": "0", + "check_authentication_data": { + "ip": "", + "port": "" }, "cert_generation": { - "csr_common_name": "", - "csr_organizational_unit": "", - "csr_organization": "", - "csr_locality": "", - "csr_state_or_province_name": "", - "csr_country_name": "", - "csr_email_address": "" + "csr_common_name": "Echeva", + "csr_organizational_unit": "discovery", + "csr_organization": "telefonica", + "csr_locality": "madrid", + "csr_state_or_province_name": "madrid", + "csr_country_name": "ES", + "csr_email_address": "adios@gmail.com" }, "discover_filter": { "api-name": "", @@ -37,30 +37,61 @@ "api-supported-features": "", "ue-ip-addr": "", "service-kpis": "" + }, + "events": { + "description": ["SERVICE_API_AVAILABLE"], + "eventFilters": [ + { + "apiIds": [""], + "apiInvokerIds": [""], + "aefIds": [""] + } + ] } }, "provider": { - "provider_folder": "", - "supported_features": "", - "apfs": "", - "aefs": "", + "provider_folder": "/Users/IDB0128/Documents/OpenCapif/test_provider_certificate_folder", + "supported_features": "0", + "cert_generation": { + "csr_common_name": "provider", + "csr_organizational_unit": "discovery", + "csr_organization": "telefonica", + "csr_locality": "madrid", + "csr_state_or_province_name": "madrid", + "csr_country_name": "ES", + "csr_email_address": "hola@gmail.com" + }, + "apfs": "2", + "aefs": "3", "publish_req": { "service_api_id": "", "publisher_apf_id": "", - "publisher_aefs_ids": [ - "", - "" - ] + "publisher_aefs_ids": ["", ""] }, - "cert_generation": { - "csr_common_name": "", - "csr_organizational_unit": "", - "csr_organization": "", - "csr_locality": "", - "csr_state_or_province_name": "", - "csr_country_name": "", - "csr_email_address": "" + "api_description_path": "", + "events": { + "description": ["SERVICE_API_AVAILABLE"], + "eventFilters": [ + { + "apiIds": [""], + "apiInvokerIds": [""], + "aefIds": [""] + } + ], + "notificationDestination" : "http://localhost:5000", + "websockNotifConfig": { + "websocketUri" : "http://localhost:5000", + "requestWebsocketUri": true + } }, - "api_description_path": "" - } + "log": { + "apiName": "API of dummy Network-App to test", + "apiVersion": "v1", + "resourceName": "MONITORING_SUBSCRIPTIONS", + "uri": "/{scsAsId}/subscriptions", + "protocol": "HTTP_2", + "operation": "GET", + "result": "200" + } + } } diff --git a/test/test.py b/test/main.py similarity index 84% rename from test/test.py rename to test/main.py index 9ca0b1fa78d24825e7ef4f449a10fe7c68ca63f4..520d07d1a3fb43846de1160b6e4053cf1db28357 100644 --- a/test/test.py +++ b/test/main.py @@ -2,7 +2,7 @@ import json # flake8: noqa -from opencapif_sdk import capif_invoker_connector, capif_provider_connector, service_discoverer,capif_logging_feature +from opencapif_sdk import capif_invoker_connector, capif_provider_connector, service_discoverer, capif_logging_feature, capif_invoker_event_feature, capif_provider_event_feature capif_sdk_config_path = "./capif_sdk_config_sample_test.json" @@ -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,23 +80,32 @@ 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) + + event_provider.create_subscription(name="Ejemplo1",id=AEF2) + + event_provider.create_subscription(name="Ejemplo2",id=APF1) + + event_provider.delete_subscription(name="Ejemplo1",id=AEF2) + + event_provider.delete_subscription(name="Ejemplo2",id=APF1) capif_provider_connector.publish_services() 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 @@ -119,22 +127,28 @@ if __name__ == "__main__": print("INVOKER ONBOARDING COMPLETED") discoverer = service_discoverer(config_file=capif_sdk_config_path) - - discoverer.discover_filter["api-name"]= "safe-6g-resilience-function" 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, jwt=discoverer.token) + + event_invoker = capif_invoker_event_feature(config_file=capif_sdk_config_path) + + event_invoker.create_subscription(name="Ejemplo3") + + event_invoker.create_subscription(name="Ejemplo4") + + event_invoker.delete_subscription(name="Ejemplo3") - logger.create_logs(aefId=AEF1,api_invoker_id=invoker_id) + event_invoker.delete_subscription(name="Ejemplo4") capif_invoker_connector.update_invoker() 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..7cc5b3794319955126d81a5c39b658a36119745b --- 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": "AEF5ab315755935b121309e787620c354", "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": "AEF32cfb4a7b1b8c705217a6bb37b43f2", "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": "0", "shareableInfo": { "isShareable": true, "capifProvDoms": [ @@ -145,7 +177,7 @@ ] }, "serviceAPICategory": "string", - "apiSuppFeats": "fffff", + "apiSuppFeats": "0", "pubApiPath": { "ccfIds": [ "string" diff --git a/test/network_app_provider_api_spec_3.json b/test/network_app_provider_api_spec_3.json index 7b9fbe4f167620a2fb673fa02612b6af2b0b9364..aa68defe3625f5c8a8f9a56f9e4b3bcce561a7d5 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": "AEFae4735f83ce9adc2caf460a30cc6dd", "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": "AEF6b4030a99398cadd25c89307cc6e04", "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": "0", "shareableInfo": { "isShareable": true, "capifProvDoms": [ @@ -145,7 +177,7 @@ ] }, "serviceAPICategory": "string", - "apiSuppFeats": "fffff", + "apiSuppFeats": "0", "pubApiPath": { "ccfIds": [ "string" 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..aa13df999ca80299c2e4eeb4c44a0a1b71c5b30a --- 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": "AEF31918dcf7bd894c86af1faf271a2d7", "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,22 +130,21 @@ "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" } ] } ], "description": "API of dummy Network-App to test", - "supportedFeatures": "fffff", + "supportedFeatures": "0", "shareableInfo": { "isShareable": true, "capifProvDoms": [ @@ -204,7 +152,7 @@ ] }, "serviceAPICategory": "string", - "apiSuppFeats": "fffff", + "apiSuppFeats": "0", "pubApiPath": { "ccfIds": [ "string" 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..6d5dc3c1bce83924adfe8344d231d5b9a7d7f668 --- /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,supported_features="0",api_supp_features="0") + 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,test_provider_publish): + 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 + + +