diff --git a/services/TS29222_CAPIF_Discover_Service_API/service_apis/core/discoveredapis.py b/services/TS29222_CAPIF_Discover_Service_API/service_apis/core/discoveredapis.py index d032b4be81b846ae46c419b6a495ed0524ec4dbd..5971de20599e2528691f8dbec60b50369aa585be 100644 --- a/services/TS29222_CAPIF_Discover_Service_API/service_apis/core/discoveredapis.py +++ b/services/TS29222_CAPIF_Discover_Service_API/service_apis/core/discoveredapis.py @@ -1,5 +1,7 @@ import json +import os +import requests from flask import current_app @@ -11,6 +13,7 @@ from ..util import serialize_clean_camel_case from ..vendor_specific import (filter_apis_with_vendor_specific_params, find_attribute_in_body, remove_vendor_specific_fields) +from ..encoder import CustomJSONEncoder TOTAL_FEATURES = 4 SUPPORTED_FEATURES_HEX = "2" @@ -20,7 +23,8 @@ def filter_fields(filtered_apis): key_filter = [ "api_name", "api_id", "aef_profiles", "description", "supported_features", "shareable_info", "service_api_category", - "api_supp_feats", "pub_api_path", "ccf_id", "api_status" + "api_supp_feats", "pub_api_path", "ccf_id", "api_status", + "api_prov_name" ] field_filtered_api = {} for key in filtered_apis.keys(): @@ -117,6 +121,42 @@ class DiscoverApisOperations(Resource): if len(json_docs) == 0: return not_found_error(detail="API Invoker " + api_invoker_id + " has no API Published that accomplish filter conditions", cause="No API Published accomplish filter conditions") + # Visibility Control Integration + + try: + visibility_control_url = os.getenv( + "VISIBILITY_CONTROL_URL", + "http://helper:8080/helper/visibility-control/decision/invokers/{}/discoverable-apis" + ).format(api_invoker_id) + visibility_payload = { + "serviceAPIDescriptions": json_docs + } + + current_app.logger.debug("Calling visibility control for invoker: " + api_invoker_id) + visibility_response = requests.post( + visibility_control_url, + data=json.dumps(visibility_payload, cls=CustomJSONEncoder), + headers={"Content-Type": "application/json"}, + timeout=int(os.getenv("TIMEOUT", "10")) + ) + + if visibility_response.status_code == 200: + filtered_data = visibility_response.json() + json_docs = filtered_data.get("serviceAPIDescriptions", []) + current_app.logger.debug(f"Visibility control filtered {len(json_docs)} APIs for invoker {api_invoker_id}") + else: + current_app.logger.warning(f"Visibility control returned status {visibility_response.status_code}: {visibility_response.text}") + + except requests.exceptions.RequestException as e: + current_app.logger.warning(f"Visibility control unreachable for invoker {api_invoker_id}: {str(e)}") + except Exception as e: + current_app.logger.warning(f"Error filtering APIs for invoker {api_invoker_id}: {str(e)}") + + if len(json_docs) == 0: + return not_found_error(detail="API Invoker " + api_invoker_id + " has no visible APIs after applying visibility rules", cause="No APIs visible after visibility filtering") + + # End of Visibility Control Integration + apis_discovered = DiscoveredAPIs(service_api_descriptions=json_docs) res = make_response(object=serialize_clean_camel_case(apis_discovered), status=200) return res @@ -125,4 +165,3 @@ class DiscoverApisOperations(Resource): exception = "An exception occurred in discover services" current_app.logger.error(exception + "::" + str(e)) return internal_server_error(detail=exception, cause=str(e)) - diff --git a/services/helper/helper_service/openapi_helper_visibility_control.yaml b/services/helper/helper_service/openapi_helper_visibility_control.yaml deleted file mode 100644 index 43d181fe9b09397dd305c03f537a5959e5986139..0000000000000000000000000000000000000000 --- a/services/helper/helper_service/openapi_helper_visibility_control.yaml +++ /dev/null @@ -1,536 +0,0 @@ -openapi: 3.0.3 -info: - title: OpenCAPIF Access Control - version: 1.0.0 - description: | - Access-control API to manage visibility rules and evaluate decisions for API discovery and - security-context access within OpenCAPIF. This API controls whether APIs are visible to invokers - (discovery) and whether invokers are allowed to create a security context to access them. - - Rules are global and evaluated with "more specific wins" precedence. - - If no rule matches, the decision uses OpenCAPIF's global default (outside this API). - - Provider selector is mandatory in rules and must contain at least one selector field. -servers: - - url: https://capif.example.com/access-control - description: Production - - url: https://sandbox.capif.example.com/access-control - description: Sandbox - -tags: - - name: Rules - description: Manage visibility rules - - name: Decision - description: Evaluate discovery and access decisions - -paths: - /rules: - get: - tags: [Rules] - summary: List rules - responses: - '200': - description: List of rules - content: - application/json: - schema: - type: object - properties: - items: - type: array - items: - $ref: '#/components/schemas/Rule' - nextPageToken: - type: string - required: [items] - post: - tags: [Rules] - summary: Create a rule - description: Server generates the ruleId. Provider selector must include at least one field. - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/RuleCreateRequest' - examples: - allow_except_some_invokers: - value: - providerSelector: - userName: "userA" - apiProviderId: [ "capif-prov-01", "capif-prov-02" ] - apiName: [ "apiName-001" ] - apiId: [ "apiId-001" ] - aefId: [ "aef-001" ] - invokerExceptions: - apiInvokerId: [ "invk-123", "invk-999" ] - default_access: ALLOW - enabled: true - responses: - '201': - description: Rule created - content: - application/json: - schema: - $ref: '#/components/schemas/Rule' - '400': - description: Invalid input - content: - application/json: - schema: { $ref: '#/components/schemas/Error' } - - /rules/{ruleId}: - get: - tags: [Rules] - summary: Get a rule - parameters: - - $ref: '#/components/parameters/RuleId' - responses: - '200': - description: Rule - content: - application/json: - schema: - $ref: '#/components/schemas/Rule' - '404': - description: Rule not found - content: - application/json: - schema: { $ref: '#/components/schemas/Error' } - patch: - tags: [Rules] - summary: Update a rule (partial) - parameters: - - $ref: '#/components/parameters/RuleId' - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/RulePatchRequest' - responses: - '200': - description: Rule updated - content: - application/json: - schema: - $ref: '#/components/schemas/Rule' - '400': - description: Invalid input - content: - application/json: - schema: { $ref: '#/components/schemas/Error' } - '404': - description: Rule not found - content: - application/json: - schema: { $ref: '#/components/schemas/Error' } - delete: - tags: [Rules] - summary: Delete a rule - parameters: - - $ref: '#/components/parameters/RuleId' - responses: - '204': - description: Deleted - '404': - description: Rule not found - content: - application/json: - schema: { $ref: '#/components/schemas/Error' } - - /decision/invokers/{apiInvokerId}/discoverable-apis: - get: - tags: [Decision] - summary: Get discoverable APIs filter for an invoker (global scope) - description: | - Returns a filtered list of APIs for the API Invoker. - parameters: - - $ref: '#/components/parameters/ApiInvokerId' - responses: - '200': - description: Discover filter - content: - application/json: - schema: - $ref: '#/components/schemas/DiscoveredAPIs' - '400': - description: Invalid input - content: - application/json: - schema: { $ref: '#/components/schemas/Error' } - '404': - description: Invoker not found (optional behavior) - content: - application/json: - schema: { $ref: '#/components/schemas/Error' } - -components: - parameters: - RuleId: - in: path - name: ruleId - required: true - schema: - type: string - description: Server-generated rule identifier - ApiInvokerId: - in: path - name: apiInvokerId - required: true - schema: - type: string - description: CAPIF API Invoker identifier - - schemas: - # ---------- Core Rule Schemas ---------- - RuleCreateRequest: - type: object - required: [providerSelector, default_access] - properties: - providerSelector: - $ref: '#/components/schemas/ProviderSelector' - invokerExceptions: - $ref: '#/components/schemas/InvokerSelector' - default_access: - type: string - enum: [ALLOW, DENY] - enabled: - type: boolean - default: true - startsAt: - type: string - format: date-time - endsAt: - type: string - format: date-time - notes: - type: string - description: | - Create a new rule. Provider selector is mandatory and must include at least one field. - If both startsAt and endsAt are present, endsAt must be greater than startsAt. - - RulePatchRequest: - type: object - properties: - providerSelector: - $ref: '#/components/schemas/PatchProviderSelector' - invokerExceptions: - $ref: '#/components/schemas/InvokerSelector' - default_access: - type: string - enum: [ALLOW, DENY] - enabled: - type: boolean - startsAt: - type: string - format: date-time - endsAt: - type: string - format: date-time - notes: - type: string - description: Partial update. Any omitted field remains unchanged. - - Rule: - type: object - properties: - ruleId: - type: string - providerSelector: - $ref: '#/components/schemas/ProviderSelector' - invokerExceptions: - $ref: '#/components/schemas/InvokerSelector' - default_access: - type: string - enum: [ALLOW, DENY] - enabled: - type: boolean - default: true - startsAt: - type: string - format: date-time - endsAt: - type: string - format: date-time - notes: - type: string - updatedAt: - type: string - format: date-time - updatedBy: - type: string - required: [ruleId, providerSelector, default_access] - - PatchProviderSelector: - type: object - description: | - Patch Provider-side selector. - properties: - apiProviderId: - type: array - items: { type: string } - minItems: 0 - uniqueItems: true - apiName: - type: array - items: { type: string } - minItems: 0 - uniqueItems: true - apiId: - type: array - items: { type: string } - minItems: 0 - uniqueItems: true - aefId: - type: array - items: { type: string } - minItems: 0 - uniqueItems: true - userName: - type: string - minLength: 1 - additionalProperties: false - - ProviderSelector: - type: object - description: | - Provider-side selector. Arrays apply OR within the field; AND across fields. - At least one of these fields must be present. - required: - - userName - properties: - userName: - type: string - minLength: 1 - apiProviderId: - type: array - items: { type: string } - minItems: 0 - uniqueItems: true - apiName: - type: array - items: { type: string } - minItems: 0 - uniqueItems: true - apiId: - type: array - items: { type: string } - minItems: 0 - uniqueItems: true - aefId: - type: array - items: { type: string } - minItems: 0 - uniqueItems: true - additionalProperties: false - - InvokerSelector: - type: object - description: Invoker-side selector used for exceptions. Optional; arrays use OR within the field; AND across fields. - properties: - invokerOnboardedByUser: - type: array - items: { type: string } - minItems: 0 - uniqueItems: true - apiInvokerId: - type: array - items: { type: string } - minItems: 0 - uniqueItems: true - additionalProperties: false - - # ---------- Decision Schemas (3GPP Based) ---------- - DiscoveredAPIs: - type: object - properties: - serviceAPIDescriptions: - type: array - items: - $ref: '#/components/schemas/ServiceAPIDescription' - minItems: 1 - suppFeat: - $ref: '#/components/schemas/SupportedFeatures' - - ServiceAPIDescription: - type: object - required: [apiName] - properties: - apiName: { type: string } - apiId: { type: string } - apiStatus: { $ref: '#/components/schemas/ApiStatus' } - aefProfiles: - type: array - items: { $ref: '#/components/schemas/AefProfile' } - minItems: 1 - description: { type: string } - supportedFeatures: { $ref: '#/components/schemas/SupportedFeatures' } - shareableInfo: { $ref: '#/components/schemas/ShareableInformation' } - serviceAPICategory: { type: string } - apiSuppFeats: { $ref: '#/components/schemas/SupportedFeatures' } - pubApiPath: { $ref: '#/components/schemas/PublishedApiPath' } - ccfId: { type: string } - apiProvName: { type: string } - #apiProvName is apiProviderId? - - - AefProfile: - type: object - required: [aefId, versions] - properties: - aefId: { type: string } - versions: - type: array - items: { $ref: '#/components/schemas/Version' } - minItems: 1 - protocol: { $ref: '#/components/schemas/Protocol' } - dataFormat: { $ref: '#/components/schemas/DataFormat' } - securityMethods: - type: array - items: { $ref: '#/components/schemas/SecurityMethod' } - grantTypes: - type: array - items: { $ref: '#/components/schemas/OAuthGrantType' } - domainName: { type: string } - interfaceDescriptions: - type: array - items: { $ref: '#/components/schemas/InterfaceDescription' } - aefLocation: { $ref: '#/components/schemas/AefLocation' } - serviceKpis: { $ref: '#/components/schemas/ServiceKpis' } - ueIpRange: { $ref: '#/components/schemas/IpAddrRange' } - - Version: - type: object - required: [apiVersion] - properties: - apiVersion: { type: string } - expiry: { type: string, format: date-time } - resources: - type: array - items: { $ref: '#/components/schemas/Resource' } - custOperations: - type: array - items: { $ref: '#/components/schemas/CustomOperation' } - - Resource: - type: object - required: [commType, resourceName, uri] - properties: - resourceName: { type: string } - commType: { $ref: '#/components/schemas/CommunicationType' } - uri: { type: string } - custOpName: { type: string } - operations: - type: array - items: { $ref: '#/components/schemas/Operation' } - description: { type: string } - - CustomOperation: - type: object - required: [commType, custOpName] - properties: - commType: { $ref: '#/components/schemas/CommunicationType' } - custOpName: { type: string } - operations: - type: array - items: { $ref: '#/components/schemas/Operation' } - description: { type: string } - - ApiStatus: - type: object - required: [aefIds] - properties: - aefIds: - type: array - items: { type: string } - - InterfaceDescription: - type: object - properties: - ipv4Addr: { type: string } - ipv6Addr: { type: string } - fqdn: { type: string } - port: { type: integer } - apiPrefix: { type: string } - securityMethods: - type: array - items: { $ref: '#/components/schemas/SecurityMethod' } - grantTypes: - type: array - items: { $ref: '#/components/schemas/OAuthGrantType' } - - # ---------- Supporting 3GPP Types ---------- - SupportedFeatures: - type: string - pattern: "^[A-Fa-f0-9]*$" - CommunicationType: - type: string - enum: [REQUEST_RESPONSE, SUBSCRIBE_NOTIFY] - Protocol: - type: string - enum: [HTTP_1_1, HTTP_2, MQTT, WEBSOCKET] - DataFormat: - type: string - enum: [JSON, XML, PROTOBUF3] - Operation: - type: string - enum: [GET, POST, PUT, PATCH, DELETE] - SecurityMethod: - type: string - enum: [PSK, PKI, OAUTH] - OAuthGrantType: - type: string - enum: [CLIENT_CREDENTIALS, AUTHORIZATION_CODE, AUTHORIZATION_CODE_WITH_PKCE] - - ShareableInformation: - type: object - required: [isShareable] - properties: - isShareable: { type: boolean } - capifProvDoms: - type: array - items: { type: string } - - PublishedApiPath: - type: object - properties: - ccfIds: - type: array - items: { type: string } - - AefLocation: - type: object - properties: - dcId: { type: string } - # Simplified for brevity, you can add GeographicArea/CivicAddress if needed - - ServiceKpis: - type: object - properties: - maxReqRate: { type: integer } - maxRestime: { type: integer } - availability: { type: integer } - avalComp: { type: string } - avalMem: { type: string } - avalStor: { type: string } - - IpAddrRange: - type: object - properties: - ueIpv4AddrRanges: - type: array - items: - type: object - properties: - start: { type: string } - end: { type: string } - - # ---------- Errors ---------- - Error: - type: object - required: [code, message] - properties: - code: { type: string } - message: { type: string } - details: - type: object - additionalProperties: true \ No newline at end of file diff --git a/services/helper/helper_service/services/visibility_control/controllers/decision_controller.py b/services/helper/helper_service/services/visibility_control/controllers/decision_controller.py index 4bfdeaff5274e20158f3dbcc47cd761c8fe94f53..a5fcb47029854c0f1a75c185890956cff0d141ee 100644 --- a/services/helper/helper_service/services/visibility_control/controllers/decision_controller.py +++ b/services/helper/helper_service/services/visibility_control/controllers/decision_controller.py @@ -1,21 +1,57 @@ import connexion +import logging from typing import Dict from typing import Tuple from typing import Union +from visibility_control.auth import cert_validation from visibility_control.models.discovered_apis import DiscoveredAPIs # noqa: E501 +from visibility_control.models.discovery_request import DiscoveryRequest # noqa: E501 from visibility_control.models.error import Error # noqa: E501 from visibility_control import util +from visibility_control.core import visibility_control_core +logger = logging.getLogger(__name__) -def decision_invokers_api_invoker_id_discoverable_apis_get(api_invoker_id): # noqa: E501 +#@cert_validation() +def decision_invokers_api_invoker_id_discoverable_apis_post(api_invoker_id, body=None): # noqa: E501 """Get discoverable APIs filter for an invoker (global scope) Returns a filtered list of APIs for the API Invoker. # noqa: E501 :param api_invoker_id: CAPIF API Invoker identifier :type api_invoker_id: str + :param body: List of all discovered APIs to be filtered + :type body: DiscoveryRequest :rtype: Union[DiscoveredAPIs, Tuple[DiscoveredAPIs, int], Tuple[DiscoveredAPIs, int, Dict[str, str]] """ - return 'do some magic!' + # Handle body parameter - it may come as None from Connexion + if body is None: + if connexion.request.is_json: + body = connexion.request.get_json() + else: + return {'code': 400, 'message': 'JSON body required'}, 400 + + if body is None or body.get('serviceAPIDescriptions') is None: + return {'code': 400, 'message': 'Missing serviceAPIDescriptions in request body'}, 400 + + logger.debug( + f"Visibility decision request for invoker {api_invoker_id}, request body keys: {list(body.keys())}" + ) + + try: + apis_list = body.get('serviceAPIDescriptions', []) + logger.debug( + f"Visibility decision input API count: {len(apis_list)} for invoker {api_invoker_id}" + ) + result = visibility_control_core.get_discoverable_apis(api_invoker_id, apis_list) + logger.debug( + f"Visibility decision returned {len(result)} APIs for invoker {api_invoker_id}" + ) + return {"serviceAPIDescriptions": result}, 200 + except Exception as e: + logger.error( + f"Visibility decision processing failed for invoker {api_invoker_id}: {str(e)}" + ) + return {"error": str(e)}, 400 diff --git a/services/helper/helper_service/services/visibility_control/core/visibility_control_core.py b/services/helper/helper_service/services/visibility_control/core/visibility_control_core.py index 7a19f618057eecbc812aa887017664b5021f141b..0aa12b46bad278011c9953e8a325603b583dd0a2 100644 --- a/services/helper/helper_service/services/visibility_control/core/visibility_control_core.py +++ b/services/helper/helper_service/services/visibility_control/core/visibility_control_core.py @@ -4,8 +4,10 @@ from db.db import get_mongo from config import Config from flask import request +import logging from visibility_control.core.validate_user import ControlAccess +logger = logging.getLogger(__name__) valid_user = ControlAccess() @@ -267,4 +269,267 @@ def update_rule_patch(rule_id, body): # Return the fully updated object (excluding Mongo's internal _id) updated_rule = col.find_one({"ruleId": rule_id}, {"_id": 0}) - return updated_rule, 200 \ No newline at end of file + return updated_rule, 200 + + +def get_discoverable_apis(api_invoker_id, all_apis): + """ + Filter APIs based on visibility rules for a specific invoker. + + :param api_invoker_id: The ID of the API invoker + :param all_apis: List of all discovered APIs (as dict objects) + :return: List of APIs that the invoker is allowed to see + """ + logger.debug( + f"get_discoverable_apis called for invoker {api_invoker_id} with {len(all_apis)} discovered APIs" + ) + db = get_mongo() + rules_col = db.get_col_by_name("visibility_rules") + + rules = list(rules_col.find({ + "$or": [ + {"enabled": True}, + {"enabled": {"$exists": False}} + ] + }, {"_id": 0})) + active_rules = [rule for rule in rules if _rule_is_active(rule)] + + logger.debug( + f"Found {len(active_rules)} active visibility rules for invoker {api_invoker_id}" + ) + + if not active_rules: + logger.debug( + f"No active visibility rules for invoker {api_invoker_id}; default ALLOW applies" + ) + return all_apis + + # Note_CCG: The rule can have ALLOW or DENY as default access. + # The point is to check the exceptions for the final decision (filtered APIs) + # I think we don´t need this part ´has_allow_rules´ (To be checked) + has_allow_rules = any( + rule.get('default_access', rule.get('defaultAccess', 'ALLOW')) == 'ALLOW' + for rule in active_rules + ) + + # Filter APIs based on rules + discoverable_apis = [] + for api in all_apis: + if _invoker_allowed_for_api( + api_invoker_id, + api, + active_rules, + default_allow_no_match=not has_allow_rules + ): + discoverable_apis.append(api) + + return discoverable_apis + + +def _invoker_allowed_for_api(api_invoker_id, api, rules, default_allow_no_match=True): + """ + Note_CCG: Here we should only call the _rule_specificity (the list + with the API and the winner rule). Before (out of this method) execute the logic to match rules + with APIs and with the invoker. Then, we should check if the invoker is allowed or denied to discover + each API based on the default access of the winner rule and the invoker exception. Note than even + if a rule does not explicitly mention invoker_id, it can affect the Invoker. + Check if an invoker is allowed to see an API based on the winner rule. + + :param api_invoker_id: The invoker ID + :param api: The API description (dict) + :param rules: List of active rules + :param default_allow_no_match: Whether to allow APIs that do not match any rule + :return: True if allowed, False otherwise + """ + matching_rules = [rule for rule in rules if _rule_matches_api(rule, api)] + if not matching_rules: + logger.debug( + f"No matching visibility rules for API {api.get('apiId') or api.get('api_id')} and invoker {api_invoker_id}; default_allow_no_match={default_allow_no_match}" + ) + return default_allow_no_match + + #winner_rule = max(matching_rules, key=lambda rule: _rule_specificity(rule)) + winner_rule = max( + matching_rules, + key=lambda rule: ( + _rule_specificity(rule), + _parse_datetime(rule.get('updatedAt', rule.get('startsAt', '1970-01-01T00:00:00Z'))) + ) + ) + default_access = _rule_default_access(winner_rule) + allowed = default_access == 'ALLOW' + invoker_exception = _rule_matches_invoker_exception(winner_rule, api_invoker_id) + + logger.debug( + f"Winner rule for API {api.get('apiId') or api.get('api_id')} and invoker {api_invoker_id}: ruleId={winner_rule.get('ruleId')} default_access={default_access} invoker_exception={invoker_exception}" + ) + + if invoker_exception: + return not allowed + + return allowed + + +def _rule_matches_api(rule, api): + """ + Note_CCG: here we should have as result, for each API, the corresponding rules (zero, one or more) + + Check if a rule matches an API based on provider selector. + + :param rule: The visibility rule + :param api: The API description (dict) + :return: True if the rule applies to this API + """ + provider_selector = rule.get('providerSelector', {}) + logger.debug( + f"Checking API rule match for API {api.get('apiId') or api.get('api_id')} against rule {rule.get('ruleId')} selector={provider_selector}" + ) + + if not provider_selector: + return True # No selector = matches all APIs + + # Check apiProviderId against apiProvName + if 'apiProviderId' in provider_selector: + api_provider_ids = _as_list(provider_selector['apiProviderId']) + api_provider_id = _get_first(api, 'apiProvName', 'api_prov_name') + if not _matches_any(api_provider_id, api_provider_ids): + return False + + # Check userName against apiProvName or provider username if present + # if 'userName' in provider_selector: + # user_names = _as_list(provider_selector['userName']) + # api_user_name = _get_first(api, 'apiProvName', 'api_prov_name') + # if not _matches_any(api_user_name, user_names): + # return False + + # Check apiName + if 'apiName' in provider_selector: + api_names = _as_list(provider_selector['apiName']) + api_name = _get_first(api, 'apiName', 'api_name') + if not _matches_any(api_name, api_names): + return False + + # Check apiId + if 'apiId' in provider_selector: + api_ids = _as_list(provider_selector['apiId']) + api_id = _get_first(api, 'apiId', 'api_id') + if not _matches_any(api_id, api_ids): + return False + + # Check aefId in nested profiles + if 'aefId' in provider_selector: + aef_ids = _as_list(provider_selector['aefId']) + aef_profiles = _get_first(api, 'aefProfiles', 'aef_profiles') or [] + aef_match = False + for profile in aef_profiles: + if _matches_any(_get_first(profile, 'aefId', 'aef_id'), aef_ids): + aef_match = True + break + if not aef_match: + return False + + return True + + +def _rule_matches_invoker_exception(rule, api_invoker_id): + """ + Note_CCG: Here we should call _rule_matches_api and filter, before choosing the winner rule, + the rules that match with the Invoker. + Consider that a rule can apply to an invoker even if the id is not explictly mentioned. + + :param rule: The visibility rule + :param api_invoker_id: The invoker ID + :return: True if the invoker is in the exception selector + """ + invoker_selector = rule.get('invokerExceptions') or rule.get('invokerSelector') or {} + logger.debug( + f"Checking invoker exception for invoker {api_invoker_id} on rule {rule.get('ruleId')} selector={invoker_selector}" + ) + + if not invoker_selector: + return False + + invoker_ids = ( + invoker_selector.get('apiInvokerId') or + invoker_selector.get('api_invoker_id') or + invoker_selector.get('invokerId') + ) + if invoker_ids is None: + return False + + return _matches_any(api_invoker_id, _as_list(invoker_ids)) + + +def _rule_specificity(rule): + """ + Note_CCG: to call here _rule_matches_invoker and use the result as an output to execute the specificity + function. For each API, select the winner rule + (the one with higher specificity). + ---- + Calculate rule specificity for ordering. + Higher specificity = more specific selectors = higher priority. + + :param rule: The visibility rule + :return: Specificity score + """ + specificity = 0 + provider_selector = rule.get('providerSelector', {}) + + # Each selector adds to specificity + for selector in ['apiProviderId', 'userName', 'apiName', 'aefId', 'apiId']: + if selector in provider_selector: + specificity += len(_as_list(provider_selector[selector])) + + return specificity + + +def _rule_is_active(rule): + now = datetime.now(timezone.utc) + starts_at = _parse_datetime(rule.get('startsAt')) + ends_at = _parse_datetime(rule.get('endsAt')) + + if starts_at and starts_at > now: + return False + if ends_at and ends_at < now: + return False + return True + + +def _parse_datetime(value): + if not value: + return None + if isinstance(value, datetime): + parsed = value + else: + try: + parsed = datetime.fromisoformat(str(value).replace('Z', '+00:00')) + except ValueError: + return None + if parsed.tzinfo is None: + return parsed.replace(tzinfo=timezone.utc) + return parsed.astimezone(timezone.utc) + + +def _rule_default_access(rule): + return rule.get('default_access', rule.get('defaultAccess', 'ALLOW')) + + +def _get_first(source, *keys): + for key in keys: + if key in source: + return source[key] + return None + + +def _as_list(value): + if value is None: + return [] + if isinstance(value, list): + return value + return [value] + + +def _matches_any(value, allowed_values): + if '*' in allowed_values: + return True + return value is not None and value in allowed_values diff --git a/services/helper/helper_service/services/visibility_control/models/discovery_request.py b/services/helper/helper_service/services/visibility_control/models/discovery_request.py new file mode 100644 index 0000000000000000000000000000000000000000..0fbd9c6e43d9cde964ab338bbac316448a40cfdd --- /dev/null +++ b/services/helper/helper_service/services/visibility_control/models/discovery_request.py @@ -0,0 +1,3 @@ +class DiscoveryRequest: + def __init__(self, *args, **kwargs): + pass diff --git a/services/helper/helper_service/services/visibility_control/openapi/openapi.yaml b/services/helper/helper_service/services/visibility_control/openapi/openapi.yaml index 1ca091427c259dc6d2783a0de9ff1b21cfc0b69b..1f5e085e38338adbee36334908de6b0e2df97237 100644 --- a/services/helper/helper_service/services/visibility_control/openapi/openapi.yaml +++ b/services/helper/helper_service/services/visibility_control/openapi/openapi.yaml @@ -27,10 +27,10 @@ tags: name: Decision paths: /decision/invokers/{apiInvokerId}/discoverable-apis: - get: + post: description: | Returns a filtered list of APIs for the API Invoker. - operationId: decision_invokers_api_invoker_id_discoverable_apis_get + operationId: decision_invokers_api_invoker_id_discoverable_apis_post parameters: - description: CAPIF API Invoker identifier explode: false diff --git a/tests/features/Helper/Visibility Control Api/visibility_control.robot b/tests/features/Helper/Visibility Control Api/visibility_control.robot index e1436d9d692d6f71b342c1d97bfe749e476de4ff..7d191c4f2a949ca32f4e7f8854f62089114cd8fb 100644 --- a/tests/features/Helper/Visibility Control Api/visibility_control.robot +++ b/tests/features/Helper/Visibility Control Api/visibility_control.robot @@ -207,7 +207,7 @@ Create Update and Delete Visibility Control Rule by AMF Provider ${body}= Create Visibility Control Rule body 2 - ${resp}= Patch Request Capif + ${resp}= Patch Request Capif ... /helper/visibility-control/rules/${rule_id} ... server=${CAPIF_HTTPS_URL} ... verify=ca.crt @@ -283,3 +283,359 @@ Create and Get Specific Visibility Control Rule # After deletion, the server must return 404 Not Found. # This is the correct way to confirm the resource is gone. Status Should Be 404 ${resp} + + +# ==================== DECISION ENDPOINT TESTS ==================== +Discover Published service APIs by Unauthorised API Invoker Visibility Control + [Tags] visibility_control-9 + # Register APF + ${register_user_info}= Provider Default Registration + + # Publish one api + ${service_api_description_published} ${resource_url} ${request_body}= Publish Service Api + ... ${register_user_info} + + # Default Invoker Registration and Onboarding + ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding + ${invoker_id}= Set Variable ${register_user_info_invoker['api_invoker_id']} + + # Test + ${resp}= Get Request Capif + ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']}&aef-id=${register_user_info['aef_id']} + ... server=${CAPIF_HTTPS_URL} + ... verify=ca.crt + ... username=${INVOKER_USERNAME} + + Check Response Variable Type And Values ${resp} 200 DiscoveredAPIs + + # Check Results + Dictionary Should Contain Key ${resp.json()} serviceAPIDescriptions + Should Not Be Empty ${resp.json()['serviceAPIDescriptions']} + Length Should Be ${resp.json()['serviceAPIDescriptions']} 1 + List Should Contain Value ${resp.json()['serviceAPIDescriptions']} ${service_api_description_published} + + # Build provider selector from published API and provider registration + ${aef_list}= Create List ${register_user_info['aef_id']} + ${apiId_list}= Create List ${service_api_description_published['apiId']} + ${apiName_list}= Create List ${service_api_description_published['apiName']} + + ${provider_selector}= Create Dictionary aefId=${aef_list} apiId=${apiId_list} apiName=${apiName_list} userName=${register_user_info['apf_username']} + + # Prepare the request body + ${body}= Create Visibility Control Rule Body 3 ${invoker_id} ${provider_selector} + + # Create a new rule using superadmin + ${resp}= Post Request Capif + ... /helper/visibility-control/rules + ... server=${CAPIF_HTTPS_URL} + ... verify=ca.crt + ... username=${SUPERADMIN_USERNAME} + ... json=${body} + + # Verify creation was successful (201 Created) + Status Should Be 201 ${resp} + ${rule_id}= Set Variable ${resp.json()['ruleId']} + Dictionary Should Contain Key ${resp.json()} providerSelector + Should Not Be Empty ${resp.json()['providerSelector']} + + # Test + ${resp}= Get Request Capif + ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']}&aef-id=${register_user_info['aef_id']} + ... server=${CAPIF_HTTPS_URL} + ... verify=ca.crt + ... username=${INVOKER_USERNAME} + + Check Response Variable Type And Values ${resp} 404 ProblemDetails + Dictionary Should Contain Key ${resp.json()} detail + Should Be Equal As Strings ${resp.json()['detail']} API Invoker ${invoker_id} has no visible APIs after applying visibility rules + + ${resp}= Delete Request Capif + ... /helper/visibility-control/rules/${rule_id} + ... server=${CAPIF_HTTPS_URL} + ... verify=ca.crt + ... username=${SUPERADMIN_USERNAME} + + Status Should Be 204 ${resp} + + # Check empty list + ${resp}= Get Request Capif + ... /helper/visibility-control/rules + ... server=${CAPIF_HTTPS_URL} + ... verify=ca.crt + ... username=${SUPERADMIN_USERNAME} + + Length Should Be ${resp.json()['rules']} 0 + +# ========== +Discover Published service APIs by Unauthorised API Invoker Visibility Control (two APIs) + [Tags] visibility_control-10 + + # Register APF + ${register_user_info}= Provider Default Registration + + # Publish two APIs from the same provider + ${service_api_description_allowed} ${resource_url_allowed} ${request_body}= Publish Service Api + ... ${register_user_info} + ... service_1 + + ${service_api_description_denied} ${resource_url_denied} ${request_body}= Publish Service Api + ... ${register_user_info} + ... service_2 + + # Check Published APIs by APF + ${resp}= Get Request Capif + ... /published-apis/v1/${register_user_info['apf_id']}/service-apis + ... server=${CAPIF_HTTPS_URL} + ... verify=ca.crt + ... username=${register_user_info['apf_username']} + + Status Should Be 200 ${resp} + Should Not Be Empty ${resp.json()} + Length Should Be ${resp.json()} 2 + + # Default Invoker Registration and Onboarding + ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding + ${invoker_id}= Set Variable ${register_user_info_invoker['api_invoker_id']} + + # Before rules, both APIs are discoverable + ${resp}= Get Request Capif + ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']}&aef-id=${register_user_info['aef_id']} + ... server=${CAPIF_HTTPS_URL} + ... verify=ca.crt + ... username=${INVOKER_USERNAME} + + Check Response Variable Type And Values ${resp} 200 DiscoveredAPIs + Dictionary Should Contain Key ${resp.json()} serviceAPIDescriptions + Length Should Be ${resp.json()['serviceAPIDescriptions']} 2 + + # Restrict visibility so that only the selected API remains visible. + # With an ALLOW rule for the allowed API, any non-matching API is denied by default. + ${aef_list}= Create List ${register_user_info['aef_id']} + ${apiId_list}= Create List ${service_api_description_allowed['apiId']} + ${apiName_list}= Create List ${service_api_description_allowed['apiName']} + ${provider_selector}= Create Dictionary aefId=${aef_list} apiId=${apiId_list} apiName=${apiName_list} userName=${register_user_info['apf_username']} + + ${body}= Create Visibility Control Rule Body 3 ${EMPTY} ${provider_selector} + + ${resp}= Post Request Capif + ... /helper/visibility-control/rules + ... server=${CAPIF_HTTPS_URL} + ... verify=ca.crt + ... username=${SUPERADMIN_USERNAME} + ... json=${body} + + Status Should Be 201 ${resp} + ${rule_id}= Set Variable ${resp.json()['ruleId']} + + # Only the selected API should remain visible for this invoker + ${resp}= Get Request Capif + ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']}&aef-id=${register_user_info['aef_id']} + ... server=${CAPIF_HTTPS_URL} + ... verify=ca.crt + ... username=${INVOKER_USERNAME} + + Check Response Variable Type And Values ${resp} 200 DiscoveredAPIs + Dictionary Should Contain Key ${resp.json()} serviceAPIDescriptions + Length Should Be ${resp.json()['serviceAPIDescriptions']} 1 + List Should Contain Value ${resp.json()['serviceAPIDescriptions']} ${service_api_description_allowed} + List Should Not Contain Value ${resp.json()['serviceAPIDescriptions']} ${service_api_description_denied} + + ${resp}= Delete Request Capif + ... /helper/visibility-control/rules/${rule_id} + ... server=${CAPIF_HTTPS_URL} + ... verify=ca.crt + ... username=${SUPERADMIN_USERNAME} + + Status Should Be 204 ${resp} + +# ========== +Discover Published service APIs by Unauthorised API Invoker Visibility Control (having several rules) + [Tags] visibility_control-11 + # Register APF + ${register_user_info}= Provider Default Registration + + # Publish one api + ${service_api_description_published} ${resource_url} ${request_body}= Publish Service Api + ... ${register_user_info} + + # Default Invoker Registration and Onboarding + ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding + ${invoker_id}= Set Variable ${register_user_info_invoker['api_invoker_id']} + + # Test + ${resp}= Get Request Capif + ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']}&aef-id=${register_user_info['aef_id']} + ... server=${CAPIF_HTTPS_URL} + ... verify=ca.crt + ... username=${INVOKER_USERNAME} + + Check Response Variable Type And Values ${resp} 200 DiscoveredAPIs + + # Check Results + Dictionary Should Contain Key ${resp.json()} serviceAPIDescriptions + Should Not Be Empty ${resp.json()['serviceAPIDescriptions']} + Length Should Be ${resp.json()['serviceAPIDescriptions']} 1 + List Should Contain Value ${resp.json()['serviceAPIDescriptions']} ${service_api_description_published} + + # Build provider selector from published API and provider registration + ${aef_list}= Create List ${register_user_info['aef_id']} + ${apiId_list}= Create List ${service_api_description_published['apiId']} + ${apiName_list}= Create List ${service_api_description_published['apiName']} + ${provider_selector}= Create Dictionary aefId=${aef_list} apiId=${apiId_list} apiName=${apiName_list} userName=${register_user_info['apf_username']} + + # Prepare the request body + ${body}= Create Visibility Control Rule Body 3 ${invoker_id} ${provider_selector} + + # Create a new rule using superadmin + ${resp}= Post Request Capif + ... /helper/visibility-control/rules + ... server=${CAPIF_HTTPS_URL} + ... verify=ca.crt + ... username=${SUPERADMIN_USERNAME} + ... json=${body} + + # Verify creation was successful (201 Created) + Status Should Be 201 ${resp} + ${rule_id}= Set Variable ${resp.json()['ruleId']} + Dictionary Should Contain Key ${resp.json()} providerSelector + Should Not Be Empty ${resp.json()['providerSelector']} + + # Test + ${resp}= Get Request Capif + ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']}&aef-id=${register_user_info['aef_id']} + ... server=${CAPIF_HTTPS_URL} + ... verify=ca.crt + ... username=${INVOKER_USERNAME} + + Check Response Variable Type And Values ${resp} 404 ProblemDetails + Dictionary Should Contain Key ${resp.json()} detail + Should Be Equal As Strings ${resp.json()['detail']} API Invoker ${invoker_id} has no visible APIs after applying visibility rules + + # Build provider selector from published API and provider registration + ${aef_list}= Create List ${register_user_info['aef_id']} + ${apiId_list}= Create List ${service_api_description_published['apiId']} + ${apiName_list}= Create List ${service_api_description_published['apiName']} + + ${provider_selector}= Create Dictionary aefId=${aef_list} apiId=${apiId_list} apiName=${apiName_list} userName=${register_user_info['apf_username']} + + # Prepare the request body + ${body}= Create Visibility Control Rule Body 5 ${invoker_id} ${provider_selector} + + # Create a new rule using superadmin + ${resp}= Post Request Capif + ... /helper/visibility-control/rules + ... server=${CAPIF_HTTPS_URL} + ... verify=ca.crt + ... username=${SUPERADMIN_USERNAME} + ... json=${body} + + # Verify creation was successful (201 Created) + Status Should Be 201 ${resp} + ${rule_id}= Set Variable ${resp.json()['ruleId']} + Dictionary Should Contain Key ${resp.json()} providerSelector + Should Not Be Empty ${resp.json()['providerSelector']} + + # Test + ${resp}= Get Request Capif + ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']}&aef-id=${register_user_info['aef_id']} + ... server=${CAPIF_HTTPS_URL} + ... verify=ca.crt + ... username=${INVOKER_USERNAME} + + Check Response Variable Type And Values ${resp} 200 DiscoveredAPIs + + # Check Results + Dictionary Should Contain Key ${resp.json()} serviceAPIDescriptions + Should Not Be Empty ${resp.json()['serviceAPIDescriptions']} + Length Should Be ${resp.json()['serviceAPIDescriptions']} 1 + List Should Contain Value ${resp.json()['serviceAPIDescriptions']} ${service_api_description_published} + +# ========== +Discover Published service APIs by Unauthorised API Invoker Visibility Control (update the rule and see changes in the discovery process) + [Tags] visibility_control-12 + + # Register APF + ${register_user_info}= Provider Default Registration + + # Publish one api + ${service_api_description_published} ${resource_url} ${request_body}= Publish Service Api + ... ${register_user_info} + + # Default Invoker Registration and Onboarding + ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding + ${invoker_id}= Set Variable ${register_user_info_invoker['api_invoker_id']} + + # Before rules, the API is discoverable + ${resp}= Get Request Capif + ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']}&aef-id=${register_user_info['aef_id']} + ... server=${CAPIF_HTTPS_URL} + ... verify=ca.crt + ... username=${INVOKER_USERNAME} + + Check Response Variable Type And Values ${resp} 200 DiscoveredAPIs + Dictionary Should Contain Key ${resp.json()} serviceAPIDescriptions + Length Should Be ${resp.json()['serviceAPIDescriptions']} 1 + List Should Contain Value ${resp.json()['serviceAPIDescriptions']} ${service_api_description_published} + + # Build provider selector from published API and provider registration + ${aef_list}= Create List ${register_user_info['aef_id']} + ${apiId_list}= Create List ${service_api_description_published['apiId']} + ${apiName_list}= Create List ${service_api_description_published['apiName']} + ${provider_selector}= Create Dictionary aefId=${aef_list} apiId=${apiId_list} apiName=${apiName_list} userName=${register_user_info['apf_username']} + + # Create a rule that hides the API for the invoker while enabled + ${body}= Create Visibility Control Rule Body 3 ${invoker_id} ${provider_selector} + + ${resp}= Post Request Capif + ... /helper/visibility-control/rules + ... server=${CAPIF_HTTPS_URL} + ... verify=ca.crt + ... username=${SUPERADMIN_USERNAME} + ... json=${body} + + Status Should Be 201 ${resp} + ${rule_id}= Set Variable ${resp.json()['ruleId']} + + # With the rule enabled, the API should not be discoverable + ${resp}= Get Request Capif + ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']}&aef-id=${register_user_info['aef_id']} + ... server=${CAPIF_HTTPS_URL} + ... verify=ca.crt + ... username=${INVOKER_USERNAME} + + Check Response Variable Type And Values ${resp} 404 ProblemDetails + Dictionary Should Contain Key ${resp.json()} detail + Should Be Equal As Strings ${resp.json()['detail']} API Invoker ${invoker_id} has no visible APIs after applying visibility rules + + # Disable the rule so the API becomes visible again + ${enabled_value}= Evaluate False + ${body}= Create Dictionary enabled=${enabled_value} + + ${resp}= Patch Request Capif + ... /helper/visibility-control/rules/${rule_id} + ... server=${CAPIF_HTTPS_URL} + ... verify=ca.crt + ... username=${SUPERADMIN_USERNAME} + ... json=${body} + + Status Should Be 200 ${resp} + + # After disabling the rule, the API should be discoverable again + ${resp}= Get Request Capif + ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']}&aef-id=${register_user_info['aef_id']} + ... server=${CAPIF_HTTPS_URL} + ... verify=ca.crt + ... username=${INVOKER_USERNAME} + + Check Response Variable Type And Values ${resp} 200 DiscoveredAPIs + Dictionary Should Contain Key ${resp.json()} serviceAPIDescriptions + Length Should Be ${resp.json()['serviceAPIDescriptions']} 1 + List Should Contain Value ${resp.json()['serviceAPIDescriptions']} ${service_api_description_published} + + ${resp}= Delete Request Capif + ... /helper/visibility-control/rules/${rule_id} + ... server=${CAPIF_HTTPS_URL} + ... verify=ca.crt + ... username=${SUPERADMIN_USERNAME} + + Status Should Be 204 ${resp} diff --git a/tests/libraries/helper_service/bodyRequests.py b/tests/libraries/helper_service/bodyRequests.py index 31d737e20dfd899773b982b70072f3511f95cf38..32d738bbed17476aabfa28f097c9defe6a6533c4 100644 --- a/tests/libraries/helper_service/bodyRequests.py +++ b/tests/libraries/helper_service/bodyRequests.py @@ -43,6 +43,159 @@ def create_visibility_control_rule_body_2(): } } +def create_visibility_control_rule_body_3(invoker_ids=None, provider_selector=None): + if invoker_ids is None: + invoker_ids = [] + elif isinstance(invoker_ids, str): + invoker_ids = [invoker_ids] + + if provider_selector is None: + provider_selector = { + "aefId": ["aef-002"], + "apiId": ["apiId-999"], + "apiName": ["api-test-cli"], + "apiProviderId": ["capif-prov-01"], + "userName": "AMF_ROBOT_TESTING_PROVIDER" + } + + return { + "default_access": "ALLOW", + "enabled": True, + "invokerExceptions": { + "apiInvokerId": invoker_ids + }, + "providerSelector": provider_selector or {} + } + +def create_visibility_control_rule_body_4(invoker_ids=None, provider_selector=None): + if invoker_ids is None: + invoker_ids = [] + elif isinstance(invoker_ids, str): + invoker_ids = [invoker_ids] + + if provider_selector is None: + provider_selector = { + "aefId": ["aef-002"], + "apiId": ["apiId-999"], + "apiName": ["api-test-cli"], + "apiProviderId": ["capif-prov-01"], + "userName": "AMF_ROBOT_TESTING_PROVIDER" + } + + return { + "default_access": "ALLOW", + "enabled": True, + "invokerExceptions": { + "apiInvokerId": invoker_ids + }, + "providerSelector": provider_selector or {} + } + +def create_visibility_control_rule_body_5(invoker_ids=None, provider_selector=None): + if invoker_ids is None: + invoker_ids = [] + elif isinstance(invoker_ids, str): + invoker_ids = [invoker_ids] + + if provider_selector is None: + provider_selector = { + "aefId": ["aef-002"], + "apiId": ["apiId-999"], + "apiName": ["api-test-cli"], + "apiProviderId": ["capif-prov-01"], + "userName": "AMF_ROBOT_TESTING_PROVIDER" + } + + return { + "default_access": "DENY", + "enabled": True, + "invokerExceptions": { + "apiInvokerId": invoker_ids + }, + "providerSelector": provider_selector or {} + } + +# def create_test_api_description(api_id="test-api-001", api_name="test-api", provider_name="capif-prov-01", aef_id="aef-001"): +# """Create a test API description for filtering tests""" +# return { +# "apiId": api_id, +# "apiName": api_name, +# "apiProvName": provider_name, +# "apiStatus": { +# "aefIds": [aef_id] +# }, +# "description": "Test API for visibility filtering", +# "aefProfiles": [ +# { +# "aefId": aef_id, +# "protocol": "HTTP_1_1", +# "grantTypes": ["CLIENT_CREDENTIALS"], +# "ueIpRange": { +# "ueIpv4AddrRanges": [ +# {"start": "10.0.0.1", "end": "10.0.0.255"} +# ] +# }, +# "securityMethods": ["PSK"], +# "versions": [ +# { +# "apiVersion": "v1", +# "resources": [ +# { +# "resourceName": "resource1", +# "commType": "REQUEST_RESPONSE", +# "description": "Resource description", +# "custOpName": "cust-op-1", +# "uri": "/resource1", +# "operations": ["GET"] +# } +# ] +# } +# ] +# } +# ], +# "supportedFeatures": "0" +# } + +# def create_discovery_decision_request(apis=None): +# """Create a request body for the decision endpoint""" +# if apis is None: +# apis = [create_test_api_description()] + +# return { +# "serviceAPIDescriptions": apis +# } + +# def create_visibility_control_rule_allow_all(): +# """Create a rule that allows all APIs (default ALLOW without specific selectors)""" +# return { +# "default_access": "ALLOW", +# "enabled": True, +# "providerSelector": { +# "apiName": ["*"] +# } +# } + +# def create_visibility_control_rule_deny_specific_api(): +# """Create a rule that denies access to a specific API""" +# return { +# "default_access": "DENY", +# "enabled": True, +# "providerSelector": { +# "apiId": ["test-api-001"], +# "userName": "capif-prov-01" +# } +# } + +# def create_visibility_control_rule_allow_specific_api(): +# """Create a rule that allows access only to APIs from a specific provider""" +# return { +# "default_access": "ALLOW", +# "enabled": True, +# "providerSelector": { +# "apiProviderId": ["capif-prov-01"], +# "userName": "capif-prov-01" +# } +# } def create_interconnection_request_body(dstProvDom="provider-b.example.com"): return { diff --git a/tools/robot/Dockerfile b/tools/robot/Dockerfile index a7e18f87e260e8ad840cc867c2352c1dacfc4582..aca381ae1b7f1147e2602d47e1b54728b6d946b4 100644 --- a/tools/robot/Dockerfile +++ b/tools/robot/Dockerfile @@ -55,9 +55,14 @@ RUN apt-get install -y --no-install-recommends \ nodejs \ npm -RUN add-apt-repository -y ppa:deadsnakes/ppa -RUN apt-get update -RUN apt-get install -y --fix-missing python3.10 python3.10-venv python3.10-dev +#RUN add-apt-repository -y ppa:deadsnakes/ppa +#RUN apt-get update +#RUN apt-get install -y --fix-missing python3.10 python3.10-venv python3.10-dev +RUN apt-get update && \ + apt-get install -y software-properties-common && \ + add-apt-repository -y ppa:deadsnakes/ppa && \ + apt-get update && \ + apt-get install -y --fix-missing python3.10 python3.10-venv python3.10-dev RUN mkdir /opt/venv RUN python3.10 -m venv /opt/venv