Commit 38f6127b authored by Cesar Cajas's avatar Cesar Cajas
Browse files

OCF176: integrate logic for apis filtering

parent 1a3eb230
Loading
Loading
Loading
Loading
Loading
+38 −38
Original line number Diff line number Diff line

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,42 +120,41 @@ 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)
@@ -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))
+3 −2
Original line number Diff line number Diff line
@@ -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
+99 −49
Original line number Diff line number Diff line
@@ -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:
    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

    # Add other selectors as needed (e.g., invokerName, etc.)

    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]


def _matches_any(value, allowed_values):
    if '*' in allowed_values:
        return True
    return value is not None and value in allowed_values