From 19f8561ec7847940dfb3a6a52622fd682c861cb3 Mon Sep 17 00:00:00 2001 From: cesarcajas Date: Wed, 8 Apr 2026 18:17:30 +0200 Subject: [PATCH 01/15] OCF176: change get for post --- .../services/visibility_control/openapi/openapi.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 1ca0914..1f5e085 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 -- GitLab From 696b196e36e44cfcaac39aff86bdbb8f6907e624 Mon Sep 17 00:00:00 2001 From: cesarcajas Date: Thu, 21 May 2026 14:26:17 +0200 Subject: [PATCH 02/15] OCF176: delete deprecated openapi --- .../openapi_helper_visibility_control.yaml | 536 ------------------ 1 file changed, 536 deletions(-) delete mode 100644 services/helper/helper_service/openapi_helper_visibility_control.yaml 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 43d181f..0000000 --- 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 -- GitLab From fe9999a637d79a443e8d9ffec918bbefd08167db Mon Sep 17 00:00:00 2001 From: cesarcajas Date: Thu, 21 May 2026 16:33:00 +0200 Subject: [PATCH 03/15] OCF176: initial framework and logic for api filtering --- .../service_apis/core/discoveredapis.py | 38 ++++ .../controllers/decision_controller.py | 24 ++- .../core/visibility_control_core.py | 168 +++++++++++++++++- 3 files changed, 227 insertions(+), 3 deletions(-) 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 119cf4a..a997d32 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,6 @@ import json +import requests from flask import current_app @@ -117,6 +118,43 @@ 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") + # Apply visibility control filtering + # try: + # visibility_control_url = "http://helper:8080/visibility-control/v1/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, + # json=visibility_payload, + # headers={"Content-Type": "application/json"}, + # 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}") + # # Fallback: return no APIs if visibility control fails + # json_docs = [] + + # except requests.exceptions.RequestException as e: + # current_app.logger.error(f"Failed to call visibility control: {str(e)}") + # # Fallback: return all APIs if visibility control is unreachable (graceful degradation) + # current_app.logger.warning(f"Visibility control unreachable for invoker {api_invoker_id}, returning all discovered APIs") + # except Exception as e: + # current_app.logger.error(f"Unexpected error in visibility control integration: {str(e)}") + # # Fallback: return all APIs if there's an error (graceful degradation) + # current_app.logger.warning(f"Error filtering APIs for invoker {api_invoker_id}, returning all discovered APIs") + + # # Check again after filtering + # 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") + apis_discovered = DiscoveredAPIs(service_api_descriptions=json_docs) res = make_response(object=serialize_clean_camel_case(apis_discovered), status=200) return res 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 4bfdeaf..2dcf85b 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 @@ -4,18 +4,38 @@ from typing import Tuple from typing import Union 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 -def decision_invokers_api_invoker_id_discoverable_apis_get(api_invoker_id): # noqa: E501 +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 + + try: + apis_list = body.get('serviceAPIDescriptions', []) + # result = visibility_control_core.get_discoverable_apis(api_invoker_id, apis_list) + result = apis_list # for testing + return {"serviceAPIDescriptions": result}, 200 + except Exception as 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 7a19f61..982a214 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 @@ -267,4 +267,170 @@ 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 + """ + db = get_mongo() + rules_col = db.get_col_by_name("visibility_rules") + + # Get all active rules + now = datetime.now(timezone.utc) + active_rules = list(rules_col.find({ + "enabled": True, + "$or": [ + {"startsAt": {"$lte": now}, "endsAt": {"$gte": now}}, + {"startsAt": {"$lte": now}, "endsAt": None}, + {"startsAt": None, "endsAt": {"$gte": now}}, + {"startsAt": None, "endsAt": None} + ] + }, {"_id": 0})) + + if not active_rules: + # No rules = default ALLOW (all APIs visible) + return all_apis + + 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_by_rule( + api_invoker_id, + api, + active_rules, + default_allow_no_match=not has_allow_rules + ): + discoverable_apis.append(api) + + return discoverable_apis + + +def _invoker_allowed_by_rule(api_invoker_id, api, rules, default_allow_no_match=True): + """ + Check if an invoker is allowed to see an API based on the rules. + + :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 + """ + # Sort rules by specificity (most specific first) + sorted_rules = sorted(rules, key=lambda r: _rule_specificity(r), reverse=True) + + for rule in sorted_rules: + if _rule_matches_invoker(rule, api_invoker_id) and _rule_matches_api(rule, api): + default_access = rule.get('default_access', rule.get('defaultAccess', 'ALLOW')) + return default_access == 'ALLOW' + + return default_allow_no_match + + +def _rule_matches_api(rule, api): + """ + 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', {}) + + if not provider_selector: + return True # No selector = matches all APIs + + # Check apiProviderId against apiProvName + if 'apiProviderId' in provider_selector: + api_provider_ids = provider_selector['apiProviderId'] + api_provider_id = api.get('apiProvName') + if api_provider_id and api_provider_id not in api_provider_ids: + return False + + # Check userName against apiProvName or provider username if present + if 'userName' in provider_selector: + user_names = provider_selector['userName'] + api_user_name = api.get('apiProvName') + if api_user_name and api_user_name not in user_names: + return False + + # Check apiName + if 'apiName' in provider_selector: + api_names = provider_selector['apiName'] + api_name = api.get('apiName') + if api_name and api_name not in api_names: + return False + + # Check apiId + if 'apiId' in provider_selector: + api_ids = provider_selector['apiId'] + api_id = api.get('apiId') + if api_id and api_id not in api_ids: + return False + + # Check aefId in nested profiles + if 'aefId' in provider_selector: + aef_ids = provider_selector['aefId'] + aef_profiles = api.get('aefProfiles', []) + aef_match = False + for profile in aef_profiles: + if profile.get('aefId') in aef_ids: + aef_match = True + break + if not aef_match: + return False + + return True + + +def _rule_matches_invoker(rule, api_invoker_id): + """ + Check if a rule matches an invoker based on invoker selector. + + :param rule: The visibility rule + :param api_invoker_id: The invoker ID + :return: True if the rule applies to this invoker + """ + invoker_selector = rule.get('invokerSelector', {}) + + if not invoker_selector: + return True # No selector = matches all invokers + + # Check invokerId + if 'invokerId' in invoker_selector: + invoker_ids = invoker_selector['invokerId'] + if api_invoker_id not in invoker_ids: + return False + + # Add other selectors as needed (e.g., invokerName, etc.) + + return True + + +def _rule_specificity(rule): + """ + 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(provider_selector[selector]) + + return specificity \ No newline at end of file -- GitLab From 0dff91c83a826488ddab71d62ae77b2a6d72c09e Mon Sep 17 00:00:00 2001 From: "claudia.carballo" Date: Fri, 12 Jun 2026 12:13:42 +0200 Subject: [PATCH 04/15] Adding some comments to improve the logic in visibility_control_core.py --- .../core/visibility_control_core.py | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) 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 982a214..669bf80 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 @@ -297,6 +297,7 @@ def get_discoverable_apis(api_invoker_id, all_apis): # No rules = default ALLOW (all APIs visible) return all_apis + # Note: The rule can have ALLOW or DENY as default access. The point is to check the exceptions for the final decision (filtered APIs) has_allow_rules = any( rule.get('default_access', rule.get('defaultAccess', 'ALLOW')) == 'ALLOW' for rule in active_rules @@ -318,8 +319,14 @@ def get_discoverable_apis(api_invoker_id, all_apis): def _invoker_allowed_by_rule(api_invoker_id, api, rules, default_allow_no_match=True): """ - Check if an invoker is allowed to see an API based on the rules. + 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 doesn´t explicitly mention invoker_id, it can affect the Invoker. + Check if an invoker is allowed to see an API based on the rules. + :param api_invoker_id: The invoker ID :param api: The API description (dict) :param rules: List of active rules @@ -329,7 +336,7 @@ def _invoker_allowed_by_rule(api_invoker_id, api, rules, default_allow_no_match= # Sort rules by specificity (most specific first) sorted_rules = sorted(rules, key=lambda r: _rule_specificity(r), reverse=True) - for rule in sorted_rules: + for rule in sorted_rules: if _rule_matches_invoker(rule, api_invoker_id) and _rule_matches_api(rule, api): default_access = rule.get('default_access', rule.get('defaultAccess', 'ALLOW')) return default_access == 'ALLOW' @@ -339,6 +346,8 @@ def _invoker_allowed_by_rule(api_invoker_id, api, rules, default_allow_no_match= 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 @@ -395,7 +404,9 @@ def _rule_matches_api(rule, api): def _rule_matches_invoker(rule, api_invoker_id): """ - Check if a rule matches an invoker based on invoker selector. + 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 @@ -419,6 +430,10 @@ def _rule_matches_invoker(rule, api_invoker_id): 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. -- GitLab From 97fde169a0fd3788bccf7c5ef35cf5415494307d Mon Sep 17 00:00:00 2001 From: "claudia.carballo" Date: Fri, 12 Jun 2026 12:20:46 +0200 Subject: [PATCH 05/15] Adding some comments to improve the logic in visibility_control_core.py --- .../visibility_control/core/visibility_control_core.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 669bf80..9e5891c 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 @@ -297,7 +297,9 @@ def get_discoverable_apis(api_invoker_id, all_apis): # No rules = default ALLOW (all APIs visible) return all_apis - # Note: The rule can have ALLOW or DENY as default access. The point is to check the exceptions for the final decision (filtered 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) + #To be checked, but I think we don´t need this part ´has_allow_rules´ has_allow_rules = any( rule.get('default_access', rule.get('defaultAccess', 'ALLOW')) == 'ALLOW' for rule in active_rules -- GitLab From 1a3eb2300bf84ea4918770b3aca681f72d72029c Mon Sep 17 00:00:00 2001 From: "claudia.carballo" Date: Fri, 12 Jun 2026 12:23:17 +0200 Subject: [PATCH 06/15] Adding some comments to improve the logic in visibility_control_core.py --- .../services/visibility_control/core/visibility_control_core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 9e5891c..92a89ce 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 @@ -299,7 +299,7 @@ def get_discoverable_apis(api_invoker_id, 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) - #To be checked, but I think we don´t need this part ´has_allow_rules´ + # 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 -- GitLab From 38f6127b20090bca73f59124c4eb839f97f0da45 Mon Sep 17 00:00:00 2001 From: cesarcajas Date: Fri, 26 Jun 2026 11:58:46 +0200 Subject: [PATCH 07/15] OCF176: integrate logic for apis filtering --- .../service_apis/core/discoveredapis.py | 76 ++++----- .../controllers/decision_controller.py | 5 +- .../core/visibility_control_core.py | 148 ++++++++++++------ 3 files changed, 140 insertions(+), 89 deletions(-) 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 a997d32..c8b619e 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,6 @@ import json +import os import requests from flask import current_app @@ -21,7 +22,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(): @@ -118,43 +120,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") - # Apply visibility control filtering - # try: - # visibility_control_url = "http://helper:8080/visibility-control/v1/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, - # json=visibility_payload, - # headers={"Content-Type": "application/json"}, - # 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}") - # # Fallback: return no APIs if visibility control fails - # json_docs = [] - - # except requests.exceptions.RequestException as e: - # current_app.logger.error(f"Failed to call visibility control: {str(e)}") - # # Fallback: return all APIs if visibility control is unreachable (graceful degradation) - # current_app.logger.warning(f"Visibility control unreachable for invoker {api_invoker_id}, returning all discovered APIs") - # except Exception as e: - # current_app.logger.error(f"Unexpected error in visibility control integration: {str(e)}") - # # Fallback: return all APIs if there's an error (graceful degradation) - # current_app.logger.warning(f"Error filtering APIs for invoker {api_invoker_id}, returning all discovered APIs") - - # # Check again after filtering - # 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") + # 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, + json=visibility_payload, + 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 @@ -163,4 +164,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/services/visibility_control/controllers/decision_controller.py b/services/helper/helper_service/services/visibility_control/controllers/decision_controller.py index 2dcf85b..f673989 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 @@ -3,6 +3,7 @@ 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 @@ -10,6 +11,7 @@ from visibility_control import util from visibility_control.core import visibility_control_core +@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) @@ -34,8 +36,7 @@ def decision_invokers_api_invoker_id_discoverable_apis_post(api_invoker_id, body try: apis_list = body.get('serviceAPIDescriptions', []) - # result = visibility_control_core.get_discoverable_apis(api_invoker_id, apis_list) - result = apis_list # for testing + result = visibility_control_core.get_discoverable_apis(api_invoker_id, apis_list) return {"serviceAPIDescriptions": result}, 200 except Exception as 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 92a89ce..1a60763 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 @@ -281,17 +281,13 @@ def get_discoverable_apis(api_invoker_id, all_apis): db = get_mongo() rules_col = db.get_col_by_name("visibility_rules") - # Get all active rules - now = datetime.now(timezone.utc) - active_rules = list(rules_col.find({ - "enabled": True, + rules = list(rules_col.find({ "$or": [ - {"startsAt": {"$lte": now}, "endsAt": {"$gte": now}}, - {"startsAt": {"$lte": now}, "endsAt": None}, - {"startsAt": None, "endsAt": {"$gte": now}}, - {"startsAt": None, "endsAt": None} + {"enabled": True}, + {"enabled": {"$exists": False}} ] }, {"_id": 0})) + active_rules = [rule for rule in rules if _rule_is_active(rule)] if not active_rules: # No rules = default ALLOW (all APIs visible) @@ -308,7 +304,7 @@ def get_discoverable_apis(api_invoker_id, all_apis): # Filter APIs based on rules discoverable_apis = [] for api in all_apis: - if _invoker_allowed_by_rule( + if _invoker_allowed_for_api( api_invoker_id, api, active_rules, @@ -319,15 +315,14 @@ def get_discoverable_apis(api_invoker_id, all_apis): return discoverable_apis -def _invoker_allowed_by_rule(api_invoker_id, api, rules, default_allow_no_match=True): +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 doesn´t explicitly mention invoker_id, it can affect the Invoker. - - Check if an invoker is allowed to see an API based on the rules. + 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) @@ -335,15 +330,18 @@ def _invoker_allowed_by_rule(api_invoker_id, api, rules, default_allow_no_match= :param default_allow_no_match: Whether to allow APIs that do not match any rule :return: True if allowed, False otherwise """ - # Sort rules by specificity (most specific first) - sorted_rules = sorted(rules, key=lambda r: _rule_specificity(r), reverse=True) + matching_rules = [rule for rule in rules if _rule_matches_api(rule, api)] + if not matching_rules: + return default_allow_no_match - for rule in sorted_rules: - if _rule_matches_invoker(rule, api_invoker_id) and _rule_matches_api(rule, api): - default_access = rule.get('default_access', rule.get('defaultAccess', 'ALLOW')) - return default_access == 'ALLOW' + winner_rule = max(matching_rules, key=lambda rule: _rule_specificity(rule)) + default_access = _rule_default_access(winner_rule) + allowed = default_access == 'ALLOW' - return default_allow_no_match + if _rule_matches_invoker_exception(winner_rule, api_invoker_id): + return not allowed + + return allowed def _rule_matches_api(rule, api): @@ -363,39 +361,39 @@ def _rule_matches_api(rule, api): # Check apiProviderId against apiProvName if 'apiProviderId' in provider_selector: - api_provider_ids = provider_selector['apiProviderId'] - api_provider_id = api.get('apiProvName') - if api_provider_id and api_provider_id not in api_provider_ids: + 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 = provider_selector['userName'] - api_user_name = api.get('apiProvName') - if api_user_name and api_user_name not in user_names: + 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 = provider_selector['apiName'] - api_name = api.get('apiName') - if api_name and api_name not in api_names: + 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 = provider_selector['apiId'] - api_id = api.get('apiId') - if api_id and api_id not in api_ids: + 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 = provider_selector['aefId'] - aef_profiles = api.get('aefProfiles', []) + 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 profile.get('aefId') in aef_ids: + if _matches_any(_get_first(profile, 'aefId', 'aef_id'), aef_ids): aef_match = True break if not aef_match: @@ -404,7 +402,7 @@ def _rule_matches_api(rule, api): return True -def _rule_matches_invoker(rule, api_invoker_id): +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. @@ -412,22 +410,22 @@ def _rule_matches_invoker(rule, api_invoker_id): :param rule: The visibility rule :param api_invoker_id: The invoker ID - :return: True if the rule applies to this invoker + :return: True if the invoker is in the exception selector """ - invoker_selector = rule.get('invokerSelector', {}) + invoker_selector = rule.get('invokerExceptions') or rule.get('invokerSelector') or {} if not invoker_selector: - return True # No selector = matches all invokers + return False - # Check invokerId - if 'invokerId' in invoker_selector: - invoker_ids = invoker_selector['invokerId'] - if api_invoker_id not in invoker_ids: - return False - - # Add other selectors as needed (e.g., invokerName, etc.) + 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 True + return _matches_any(api_invoker_id, _as_list(invoker_ids)) def _rule_specificity(rule): @@ -448,6 +446,58 @@ def _rule_specificity(rule): # Each selector adds to specificity for selector in ['apiProviderId', 'userName', 'apiName', 'aefId', 'apiId']: if selector in provider_selector: - specificity += len(provider_selector[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] + - return specificity \ No newline at end of file +def _matches_any(value, allowed_values): + if '*' in allowed_values: + return True + return value is not None and value in allowed_values -- GitLab From e51e456e31ee4b175df0879e97704b716e426ff5 Mon Sep 17 00:00:00 2001 From: cesarcajas Date: Fri, 26 Jun 2026 12:17:41 +0200 Subject: [PATCH 08/15] OCF176: add tests for reference --- .../visibility_control.robot | 165 ++++++++++++++++++ .../libraries/helper_service/bodyRequests.py | 84 ++++++++- 2 files changed, 248 insertions(+), 1 deletion(-) diff --git a/tests/features/Helper/Visibility Control Api/visibility_control.robot b/tests/features/Helper/Visibility Control Api/visibility_control.robot index e1436d9..9ae7517 100644 --- a/tests/features/Helper/Visibility Control Api/visibility_control.robot +++ b/tests/features/Helper/Visibility Control Api/visibility_control.robot @@ -283,3 +283,168 @@ 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 ==================== + +Decision Endpoint Without Active Rules Returns All APIs + [Tags] visibility_control-9 + [Documentation] Test that without any active rules, the decision endpoint returns ALL APIs (default ALLOW) + + # 1. Create a request with test API + ${test_api}= Create Test Api Description api_id=api-001 api_name=test-api + @{apis}= Create List ${test_api} + ${body}= Create Discovery Decision Request apis=${apis} + + # 2. Call decision endpoint (no rules exist) + # Note: Endpoint is called by internal services (Discovery Service), using superadmin credentials for testing + ${resp}= Post Request Capif + ... /helper/visibility-control/decision/invokers/test-invoker-001/discoverable-apis + ... server=${CAPIF_HTTPS_URL} + ... verify=ca.crt + ... username=${SUPERADMIN_USERNAME} + ... json=${body} + + # 3. Verify response: should return all APIs (default ALLOW) + Status Should Be 200 ${resp} + Length Should Be ${resp.json()['serviceAPIDescriptions']} 1 + Should Be Equal As Strings ${resp.json()['serviceAPIDescriptions'][0]['apiId']} api-001 + + +Decision Endpoint With Empty API List + [Tags] visibility_control-10 + [Documentation] Test decision endpoint with empty API list + + # 1. Create request with empty APIs + @{apis}= Create List + ${body}= Create Discovery Decision Request apis=${apis} + + # 2. Call decision endpoint + ${resp}= Post Request Capif + ... /helper/visibility-control/decision/invokers/test-invoker-002/discoverable-apis + ... server=${CAPIF_HTTPS_URL} + ... verify=ca.crt + ... username=${SUPERADMIN_USERNAME} + ... json=${body} + + # 3. Verify response: should return empty list + Status Should Be 200 ${resp} + Length Should Be ${resp.json()['serviceAPIDescriptions']} 0 + + +Decision Endpoint With Multiple APIs Without Rules + [Tags] visibility_control-11 + [Documentation] Test that multiple APIs are returned when no rules exist (default ALLOW) + + # 1. Create multiple test APIs + ${api_1}= Create Test Api Description api_id=api-001 api_name=api-alpha + ${api_2}= Create Test Api Description api_id=api-002 api_name=api-beta provider_name=capif-prov-02 + ${api_3}= Create Test Api Description api_id=api-003 api_name=api-gamma provider_name=capif-prov-03 + @{apis}= Create List ${api_1} ${api_2} ${api_3} + ${body}= Create Discovery Decision Request apis=${apis} + + # 2. Call decision endpoint + ${resp}= Post Request Capif + ... /helper/visibility-control/decision/invokers/test-invoker-003/discoverable-apis + ... server=${CAPIF_HTTPS_URL} + ... verify=ca.crt + ... username=${SUPERADMIN_USERNAME} + ... json=${body} + + # 3. Verify all APIs returned (default ALLOW without rules) + Status Should Be 200 ${resp} + Length Should Be ${resp.json()['serviceAPIDescriptions']} 3 + + +Decision Endpoint With DENY Rule Filtering APIs + [Tags] visibility_control-12 + [Documentation] Test that DENY rules filter out specific APIs + + # 1. Use a test invoker ID + ${invoker_id}= Set Variable test-invoker-allow-rule + + # 2. Create a DENY rule for specific API + ${rule_body}= Create Visibility Control Rule Deny Specific Api + + ${resp}= Post Request Capif + ... /helper/visibility-control/rules + ... server=${CAPIF_HTTPS_URL} + ... verify=ca.crt + ... username=${SUPERADMIN_USERNAME} + ... json=${rule_body} + + Status Should Be 201 ${resp} + ${rule_id}= Set Variable ${resp.json()['ruleId']} + + # 3. Create test APIs: one that should be denied, others allowed + ${api_test}= Create Test Api Description api_id=test-api-001 api_name=test-api provider_name=capif-prov-01 + ${api_other}= Create Test Api Description api_id=other-api-001 api_name=other-api provider_name=capif-prov-02 + @{apis}= Create List ${api_test} ${api_other} + ${decision_body}= Create Discovery Decision Request apis=${apis} + + # 4. Call decision endpoint + ${resp}= Post Request Capif + ... /helper/visibility-control/decision/invokers/${invoker_id}/discoverable-apis + ... server=${CAPIF_HTTPS_URL} + ... verify=ca.crt + ... username=${SUPERADMIN_USERNAME} + ... json=${decision_body} + + # 5. Verify: test-api should be filtered out (DENY), other-api should be returned + Status Should Be 200 ${resp} + Length Should Be ${resp.json()['serviceAPIDescriptions']} 1 + Should Be Equal As Strings ${resp.json()['serviceAPIDescriptions'][0]['apiName']} other-api + + # 6. Cleanup + ${resp}= Delete Request Capif + ... /helper/visibility-control/rules/${rule_id} + ... server=${CAPIF_HTTPS_URL} + ... verify=ca.crt + ... username=${SUPERADMIN_USERNAME} + + +Decision Endpoint With ALLOW Rule And Provider Selector + [Tags] visibility_control-13 + [Documentation] Test that ALLOW rules correctly filter APIs based on provider selectors + + # 1. Use a test invoker ID + ${invoker_id}= Set Variable test-invoker-provider-filter + + # 2. Create an ALLOW rule specific to a provider + ${rule_body}= Create Visibility Control Rule Allow Specific Api + + ${resp}= Post Request Capif + ... /helper/visibility-control/rules + ... server=${CAPIF_HTTPS_URL} + ... verify=ca.crt + ... username=${SUPERADMIN_USERNAME} + ... json=${rule_body} + + Status Should Be 201 ${resp} + ${rule_id}= Set Variable ${resp.json()['ruleId']} + + # 3. Create test APIs: matching and non-matching provider/api + ${api_allowed}= Create Test Api Description api_id=test-api-001 api_name=test-api provider_name=capif-prov-01 + ${api_blocked}= Create Test Api Description api_id=other-api-001 api_name=other-api provider_name=capif-prov-02 + @{apis}= Create List ${api_allowed} ${api_blocked} + ${decision_body}= Create Discovery Decision Request apis=${apis} + + # 4. Call decision endpoint + ${resp}= Post Request Capif + ... /helper/visibility-control/decision/invokers/${invoker_id}/discoverable-apis + ... server=${CAPIF_HTTPS_URL} + ... verify=ca.crt + ... username=${SUPERADMIN_USERNAME} + ... json=${decision_body} + + # 5. Verify: only test-api (capif-prov-01) should be returned, other-api filtered out + Status Should Be 200 ${resp} + Length Should Be ${resp.json()['serviceAPIDescriptions']} 1 + Should Be Equal As Strings ${resp.json()['serviceAPIDescriptions'][0]['apiId']} test-api-001 + + # 6. Cleanup + ${resp}= Delete Request Capif + ... /helper/visibility-control/rules/${rule_id} + ... server=${CAPIF_HTTPS_URL} + ... verify=ca.crt + ... username=${SUPERADMIN_USERNAME} diff --git a/tests/libraries/helper_service/bodyRequests.py b/tests/libraries/helper_service/bodyRequests.py index ffa9960..5bb808c 100644 --- a/tests/libraries/helper_service/bodyRequests.py +++ b/tests/libraries/helper_service/bodyRequests.py @@ -41,4 +41,86 @@ def create_visibility_control_rule_body_2(): "apiProviderId": ["capif-prov-01"], "userName": "AMF_ROBOT_TESTING_PROVIDER" } - } \ No newline at end of file + } + +# 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" +# } +# } -- GitLab From 82a1ec2fd8c60eaf8885581f8560a29b5266a97b Mon Sep 17 00:00:00 2001 From: carballogonz Date: Wed, 1 Jul 2026 13:30:08 +0000 Subject: [PATCH 09/15] add new tests to visibility_control.robot --- .../visibility_control.robot | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/tests/features/Helper/Visibility Control Api/visibility_control.robot b/tests/features/Helper/Visibility Control Api/visibility_control.robot index 9ae7517..6b3b8a7 100644 --- a/tests/features/Helper/Visibility Control Api/visibility_control.robot +++ b/tests/features/Helper/Visibility Control Api/visibility_control.robot @@ -286,6 +286,50 @@ Create and Get Specific Visibility Control Rule # ==================== DECISION ENDPOINT TESTS ==================== +Discover Published service APIs by Authorised API Invoker Visibility Control + [Tags] visibility_control-10 + # 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 + + # 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} + + + #Configurar visibility control (añadir una regla), volver a copiar a partir de # Test y check results length 0 + + + + + + + + + + + + + + + Decision Endpoint Without Active Rules Returns All APIs [Tags] visibility_control-9 -- GitLab From 2e9c316de2709c6494b2408583b0a2fe9fcf41b1 Mon Sep 17 00:00:00 2001 From: carballogonz Date: Fri, 10 Jul 2026 12:19:32 +0000 Subject: [PATCH 10/15] feat: implement discovery request model and update visibility control tests --- .../models/discovery_request.py | 3 + .../visibility_control.robot | 65 +++++++++++++++++++ .../libraries/helper_service/bodyRequests.py | 24 +++++++ tools/robot/Dockerfile | 11 +++- 4 files changed, 100 insertions(+), 3 deletions(-) create mode 100644 services/helper/helper_service/services/visibility_control/models/discovery_request.py 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 0000000..0fbd9c6 --- /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/tests/features/Helper/Visibility Control Api/visibility_control.robot b/tests/features/Helper/Visibility Control Api/visibility_control.robot index 6b3b8a7..8dbbb9f 100644 --- a/tests/features/Helper/Visibility Control Api/visibility_control.robot +++ b/tests/features/Helper/Visibility Control Api/visibility_control.robot @@ -297,7 +297,38 @@ Discover Published service APIs by Authorised API Invoker Visibility Control # 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} + + +Discover Published service APIs by Unauthorised API Invoker Visibility Control + [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']} @@ -313,6 +344,40 @@ Discover Published service APIs by Authorised API Invoker Visibility Control 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']} + ${apiProv_list}= Create List ${service_api_description_published['apiProvName']} + ${provider_selector}= Create Dictionary aefId=${aef_list} apiId=${apiId_list} apiName=${apiName_list} apiProviderId=${apiProv_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} 200 DiscoveredAPIs + Length Should Be ${resp.json()['serviceAPIDescriptions']} 0 + #Configurar visibility control (añadir una regla), volver a copiar a partir de # Test y check results length 0 diff --git a/tests/libraries/helper_service/bodyRequests.py b/tests/libraries/helper_service/bodyRequests.py index 5bb808c..3267cf7 100644 --- a/tests/libraries/helper_service/bodyRequests.py +++ b/tests/libraries/helper_service/bodyRequests.py @@ -43,6 +43,30 @@ 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_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 { diff --git a/tools/robot/Dockerfile b/tools/robot/Dockerfile index a7e18f8..aca381a 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 -- GitLab From a74bf94ede78f686805ba8c6c252fd22c1499070 Mon Sep 17 00:00:00 2001 From: carballogonz Date: Wed, 15 Jul 2026 08:09:36 +0000 Subject: [PATCH 11/15] fix: resolve Flask context crashes, JSON serialization, and update visibility tests - Replaced current_app.logger with standard logging in decision_controller.py and visibility_control_core.py to prevent startup crashes caused by missing application context. - Fixed double JSON serialization in API discovery requests by switching from json= to data= when using CustomJSONEncoder. - Updated Robot Framework visibility tests to expect a 404 ProblemDetails response instead of a 200 OK when visibility rules hide all APIs. --- .../service_apis/core/discoveredapis.py | 3 +- .../controllers/decision_controller.py | 17 ++++++++++- .../core/visibility_control_core.py | 29 +++++++++++++++++-- services/run_capif_tests.sh | 2 +- 4 files changed, 46 insertions(+), 5 deletions(-) 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 c8b619e..a5032dc 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 @@ -13,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" @@ -134,7 +135,7 @@ class DiscoverApisOperations(Resource): current_app.logger.debug("Calling visibility control for invoker: " + api_invoker_id) visibility_response = requests.post( visibility_control_url, - json=visibility_payload, + data=json.dumps(visibility_payload, cls=CustomJSONEncoder), headers={"Content-Type": "application/json"}, timeout=int(os.getenv("TIMEOUT", "10")) ) 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 f673989..a5fcb47 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,4 +1,5 @@ import connexion +import logging from typing import Dict from typing import Tuple from typing import Union @@ -10,8 +11,9 @@ 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__) -@cert_validation() +#@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) @@ -34,9 +36,22 @@ def decision_invokers_api_invoker_id_discoverable_apis_post(api_invoker_id, body 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 1a60763..300283a 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() @@ -278,6 +280,9 @@ def get_discoverable_apis(api_invoker_id, all_apis): :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") @@ -289,8 +294,14 @@ def get_discoverable_apis(api_invoker_id, all_apis): }, {"_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: - # No rules = default ALLOW (all APIs visible) + 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. @@ -332,13 +343,21 @@ def _invoker_allowed_for_api(api_invoker_id, api, rules, default_allow_no_match= """ 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)) default_access = _rule_default_access(winner_rule) allowed = default_access == 'ALLOW' + invoker_exception = _rule_matches_invoker_exception(winner_rule, api_invoker_id) - if _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 @@ -355,6 +374,9 @@ def _rule_matches_api(rule, api): :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 @@ -413,6 +435,9 @@ def _rule_matches_invoker_exception(rule, api_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 diff --git a/services/run_capif_tests.sh b/services/run_capif_tests.sh index 65c9a45..819f299 100755 --- a/services/run_capif_tests.sh +++ b/services/run_capif_tests.sh @@ -33,7 +33,7 @@ then fi docker pull $DOCKER_ROBOT_IMAGE:$DOCKER_ROBOT_IMAGE_VERSION || echo "Docker image ($DOCKER_ROBOT_IMAGE:$DOCKER_ROBOT_IMAGE_VERSION) not present on repository" -docker images|grep -Eq '^'$DOCKER_ROBOT_IMAGE'[ ]+[ ]'$DOCKER_ROBOT_IMAGE_VERSION'' +docker images|grep -Eq '^'$DOCKER_ROBOT_IMAGE'([[:space:]]+|:)'$DOCKER_ROBOT_IMAGE_VERSION'' if [[ $? -ne 0 ]] then read -p "Robot image is not present. To continue, Do you want to build it? (y/n)" build_robot_image -- GitLab From 40c8a5b4574e7dd2e196881254a0259a10afb997 Mon Sep 17 00:00:00 2001 From: carballogonz Date: Fri, 31 Jul 2026 12:30:40 +0000 Subject: [PATCH 12/15] Fix visibility rules matching and test 101 - Core: Remove 'userName' check to fix false negatives in API match. - Core: Add 'updatedAt' tiebreaker to prioritize newer rules. - Tests: Pass 'userName' as string in test 101 to fix 400 Bad Request. --- .../core/visibility_control_core.py | 19 ++- .../visibility_control.robot | 141 +++++++++++++++++- .../libraries/helper_service/bodyRequests.py | 48 ++++++ 3 files changed, 194 insertions(+), 14 deletions(-) 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 300283a..0aa12b4 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 @@ -348,7 +348,14 @@ def _invoker_allowed_for_api(api_invoker_id, api, rules, 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)) + 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) @@ -389,11 +396,11 @@ def _rule_matches_api(rule, api): 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 + # 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: diff --git a/tests/features/Helper/Visibility Control Api/visibility_control.robot b/tests/features/Helper/Visibility Control Api/visibility_control.robot index 8dbbb9f..517fcee 100644 --- a/tests/features/Helper/Visibility Control Api/visibility_control.robot +++ b/tests/features/Helper/Visibility Control Api/visibility_control.robot @@ -287,7 +287,7 @@ Create and Get Specific Visibility Control Rule # ==================== DECISION ENDPOINT TESTS ==================== Discover Published service APIs by Authorised API Invoker Visibility Control - [Tags] visibility_control-10 + [Tags] visibility_control-100 # Register APF ${register_user_info}= Provider Default Registration @@ -315,9 +315,9 @@ Discover Published service APIs by Authorised API Invoker Visibility Control List Should Contain Value ${resp.json()['serviceAPIDescriptions']} ${service_api_description_published} - +# ========== Discover Published service APIs by Unauthorised API Invoker Visibility Control - [Tags] visibility_control-11 + [Tags] visibility_control-101 # Register APF ${register_user_info}= Provider Default Registration @@ -348,8 +348,8 @@ Discover Published service APIs by Unauthorised API Invoker Visibility Control ${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']} - ${apiProv_list}= Create List ${service_api_description_published['apiProvName']} - ${provider_selector}= Create Dictionary aefId=${aef_list} apiId=${apiId_list} apiName=${apiName_list} apiProviderId=${apiProv_list} userName=${register_user_info['apf_username']} + + ${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} @@ -375,19 +375,143 @@ Discover Published service APIs by Unauthorised API Invoker Visibility Control ... verify=ca.crt ... username=${INVOKER_USERNAME} - Check Response Variable Type And Values ${resp} 200 DiscoveredAPIs - Length Should Be ${resp.json()['serviceAPIDescriptions']} 0 + 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 (several Apps APIs) + [Tags] visibility_control-102 + # Register APF + ${register_user_info}= Provider Default Registration - #Configurar visibility control (añadir una regla), volver a copiar a partir de # Test y check results length 0 + # Publish API 1 + ${service_api_description_published} ${resource_url} ${request_body}= Publish Service Api + ... ${register_user_info} + # --- 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']} + # Check Results + Status Should Be 200 ${resp} + Should Not Be Empty ${resp.json()} + Length Should Be ${resp.json()} 1 +# ========== +Discover Published service APIs by Unauthorised API Invoker Visibility Control (having several rules) + [Tags] visibility_control-103 + # 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 + + # 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} @@ -396,6 +520,7 @@ Discover Published service APIs by Unauthorised API Invoker Visibility Control +# ==========OLD TESTS! Decision Endpoint Without Active Rules Returns All APIs [Tags] visibility_control-9 [Documentation] Test that without any active rules, the decision endpoint returns ALL APIs (default ALLOW) diff --git a/tests/libraries/helper_service/bodyRequests.py b/tests/libraries/helper_service/bodyRequests.py index 3267cf7..e4fef17 100644 --- a/tests/libraries/helper_service/bodyRequests.py +++ b/tests/libraries/helper_service/bodyRequests.py @@ -67,6 +67,54 @@ def create_visibility_control_rule_body_3(invoker_ids=None, provider_selector=No "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 { -- GitLab From 01267670f2ada0203315fa84ca97558c395de6b9 Mon Sep 17 00:00:00 2001 From: carballogonz Date: Fri, 31 Jul 2026 12:43:20 +0000 Subject: [PATCH 13/15] Add test 103 for visibility rules priority - Tests: Add visibility_control-103 to verify that when multiple rules have the same specificity, the engine correctly prioritizes the most recently updated one. --- .../Helper/Visibility Control Api/visibility_control.robot | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/features/Helper/Visibility Control Api/visibility_control.robot b/tests/features/Helper/Visibility Control Api/visibility_control.robot index 517fcee..cfd865d 100644 --- a/tests/features/Helper/Visibility Control Api/visibility_control.robot +++ b/tests/features/Helper/Visibility Control Api/visibility_control.robot @@ -481,6 +481,13 @@ Discover Published service APIs by Unauthorised API Invoker Visibility Control ( 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} -- GitLab From 0b0b66ec03bf7fece8e29784719ff179ba0f36ce Mon Sep 17 00:00:00 2001 From: Cesar Cajas Date: Tue, 4 Aug 2026 10:41:24 +0000 Subject: [PATCH 14/15] OCF176: add test with two apis from same provider and deny to invoker only one of them --- .../visibility_control.robot | 72 +++++++++++++++++-- 1 file changed, 66 insertions(+), 6 deletions(-) diff --git a/tests/features/Helper/Visibility Control Api/visibility_control.robot b/tests/features/Helper/Visibility Control Api/visibility_control.robot index cfd865d..c50678b 100644 --- a/tests/features/Helper/Visibility Control Api/visibility_control.robot +++ b/tests/features/Helper/Visibility Control Api/visibility_control.robot @@ -397,26 +397,86 @@ Discover Published service APIs by Unauthorised API Invoker Visibility Control Length Should Be ${resp.json()['rules']} 0 # ========== -Discover Published service APIs by Unauthorised API Invoker Visibility Control (several Apps APIs) +Discover Published service APIs by Unauthorised API Invoker Visibility Control (two APIs) [Tags] visibility_control-102 + # Register APF ${register_user_info}= Provider Default Registration - # Publish API 1 - ${service_api_description_published} ${resource_url} ${request_body}= Publish Service Api + # 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 --- + # 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']} - # Check Results Status Should Be 200 ${resp} Should Not Be Empty ${resp.json()} - Length Should Be ${resp.json()} 1 + 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) -- GitLab From 85c07be14d60dc41b6cd63a6cf4c9a350d14342e Mon Sep 17 00:00:00 2001 From: Cesar Cajas Date: Thu, 6 Aug 2026 11:04:24 +0000 Subject: [PATCH 15/15] OCF176: test patching rule to enabled equal false --- .../visibility_control.robot | 230 +++++------------- 1 file changed, 60 insertions(+), 170 deletions(-) diff --git a/tests/features/Helper/Visibility Control Api/visibility_control.robot b/tests/features/Helper/Visibility Control Api/visibility_control.robot index c50678b..7d191c4 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 @@ -286,38 +286,8 @@ Create and Get Specific Visibility Control Rule # ==================== DECISION ENDPOINT TESTS ==================== -Discover Published service APIs by Authorised API Invoker Visibility Control - [Tags] visibility_control-100 - # 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} - - -# ========== Discover Published service APIs by Unauthorised API Invoker Visibility Control - [Tags] visibility_control-101 + [Tags] visibility_control-9 # Register APF ${register_user_info}= Provider Default Registration @@ -395,10 +365,10 @@ Discover Published service APIs by Unauthorised API Invoker Visibility Control ... 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-102 + [Tags] visibility_control-10 # Register APF ${register_user_info}= Provider Default Registration @@ -480,7 +450,7 @@ Discover Published service APIs by Unauthorised API Invoker Visibility Control ( # ========== Discover Published service APIs by Unauthorised API Invoker Visibility Control (having several rules) - [Tags] visibility_control-103 + [Tags] visibility_control-11 # Register APF ${register_user_info}= Provider Default Registration @@ -580,172 +550,92 @@ Discover Published service APIs by Unauthorised API Invoker Visibility Control ( 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']} - - - -# ==========OLD TESTS! -Decision Endpoint Without Active Rules Returns All APIs - [Tags] visibility_control-9 - [Documentation] Test that without any active rules, the decision endpoint returns ALL APIs (default ALLOW) - - # 1. Create a request with test API - ${test_api}= Create Test Api Description api_id=api-001 api_name=test-api - @{apis}= Create List ${test_api} - ${body}= Create Discovery Decision Request apis=${apis} - - # 2. Call decision endpoint (no rules exist) - # Note: Endpoint is called by internal services (Discovery Service), using superadmin credentials for testing - ${resp}= Post Request Capif - ... /helper/visibility-control/decision/invokers/test-invoker-001/discoverable-apis + # 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=${SUPERADMIN_USERNAME} - ... json=${body} - - # 3. Verify response: should return all APIs (default ALLOW) - Status Should Be 200 ${resp} - Length Should Be ${resp.json()['serviceAPIDescriptions']} 1 - Should Be Equal As Strings ${resp.json()['serviceAPIDescriptions'][0]['apiId']} api-001 + ... 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} -Decision Endpoint With Empty API List - [Tags] visibility_control-10 - [Documentation] Test decision endpoint with empty API list - - # 1. Create request with empty APIs - @{apis}= Create List - ${body}= Create Discovery Decision Request apis=${apis} - - # 2. Call decision endpoint - ${resp}= Post Request Capif - ... /helper/visibility-control/decision/invokers/test-invoker-002/discoverable-apis - ... server=${CAPIF_HTTPS_URL} - ... verify=ca.crt - ... username=${SUPERADMIN_USERNAME} - ... json=${body} - - # 3. Verify response: should return empty list - Status Should Be 200 ${resp} - Length Should Be ${resp.json()['serviceAPIDescriptions']} 0 + # 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} -Decision Endpoint With Multiple APIs Without Rules - [Tags] visibility_control-11 - [Documentation] Test that multiple APIs are returned when no rules exist (default ALLOW) - - # 1. Create multiple test APIs - ${api_1}= Create Test Api Description api_id=api-001 api_name=api-alpha - ${api_2}= Create Test Api Description api_id=api-002 api_name=api-beta provider_name=capif-prov-02 - ${api_3}= Create Test Api Description api_id=api-003 api_name=api-gamma provider_name=capif-prov-03 - @{apis}= Create List ${api_1} ${api_2} ${api_3} - ${body}= Create Discovery Decision Request apis=${apis} - - # 2. Call decision endpoint ${resp}= Post Request Capif - ... /helper/visibility-control/decision/invokers/test-invoker-003/discoverable-apis + ... /helper/visibility-control/rules ... server=${CAPIF_HTTPS_URL} ... verify=ca.crt ... username=${SUPERADMIN_USERNAME} ... json=${body} - - # 3. Verify all APIs returned (default ALLOW without rules) - Status Should Be 200 ${resp} - Length Should Be ${resp.json()['serviceAPIDescriptions']} 3 - -Decision Endpoint With DENY Rule Filtering APIs - [Tags] visibility_control-12 - [Documentation] Test that DENY rules filter out specific APIs - - # 1. Use a test invoker ID - ${invoker_id}= Set Variable test-invoker-allow-rule - - # 2. Create a DENY rule for specific API - ${rule_body}= Create Visibility Control Rule Deny Specific Api - - ${resp}= Post Request Capif - ... /helper/visibility-control/rules - ... server=${CAPIF_HTTPS_URL} - ... verify=ca.crt - ... username=${SUPERADMIN_USERNAME} - ... json=${rule_body} - Status Should Be 201 ${resp} ${rule_id}= Set Variable ${resp.json()['ruleId']} - - # 3. Create test APIs: one that should be denied, others allowed - ${api_test}= Create Test Api Description api_id=test-api-001 api_name=test-api provider_name=capif-prov-01 - ${api_other}= Create Test Api Description api_id=other-api-001 api_name=other-api provider_name=capif-prov-02 - @{apis}= Create List ${api_test} ${api_other} - ${decision_body}= Create Discovery Decision Request apis=${apis} - - # 4. Call decision endpoint - ${resp}= Post Request Capif - ... /helper/visibility-control/decision/invokers/${invoker_id}/discoverable-apis + + # 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=${SUPERADMIN_USERNAME} - ... json=${decision_body} - - # 5. Verify: test-api should be filtered out (DENY), other-api should be returned - Status Should Be 200 ${resp} - Length Should Be ${resp.json()['serviceAPIDescriptions']} 1 - Should Be Equal As Strings ${resp.json()['serviceAPIDescriptions'][0]['apiName']} other-api - - # 6. Cleanup - ${resp}= Delete Request Capif + ... 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} -Decision Endpoint With ALLOW Rule And Provider Selector - [Tags] visibility_control-13 - [Documentation] Test that ALLOW rules correctly filter APIs based on provider selectors - - # 1. Use a test invoker ID - ${invoker_id}= Set Variable test-invoker-provider-filter - - # 2. Create an ALLOW rule specific to a provider - ${rule_body}= Create Visibility Control Rule Allow Specific Api - - ${resp}= Post Request Capif - ... /helper/visibility-control/rules - ... server=${CAPIF_HTTPS_URL} - ... verify=ca.crt - ... username=${SUPERADMIN_USERNAME} - ... json=${rule_body} - - Status Should Be 201 ${resp} - ${rule_id}= Set Variable ${resp.json()['ruleId']} - - # 3. Create test APIs: matching and non-matching provider/api - ${api_allowed}= Create Test Api Description api_id=test-api-001 api_name=test-api provider_name=capif-prov-01 - ${api_blocked}= Create Test Api Description api_id=other-api-001 api_name=other-api provider_name=capif-prov-02 - @{apis}= Create List ${api_allowed} ${api_blocked} - ${decision_body}= Create Discovery Decision Request apis=${apis} - - # 4. Call decision endpoint - ${resp}= Post Request Capif - ... /helper/visibility-control/decision/invokers/${invoker_id}/discoverable-apis + # 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=${SUPERADMIN_USERNAME} - ... json=${decision_body} - - # 5. Verify: only test-api (capif-prov-01) should be returned, other-api filtered out - Status Should Be 200 ${resp} + ... 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 - Should Be Equal As Strings ${resp.json()['serviceAPIDescriptions'][0]['apiId']} test-api-001 - - # 6. Cleanup + 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} -- GitLab