Commit 9ac7567b authored by Jorge Moratinos's avatar Jorge Moratinos
Browse files

Creation of security Context on remote CCF and OAUTH token request for invoker...

Creation of security Context on remote CCF and OAUTH token request for invoker in a CCF different than provider's one
parent 942430b1
Loading
Loading
Loading
Loading
Loading
+115 −31
Changes for services/TS29222_CAPIF_Security_API/capif_security/core/servicesecurity.py: 115 added lines, 31 removed lines.
Original line number Diff line number Diff line
@@ -71,7 +71,7 @@ class SecurityOperations(Resource):
            if header != "3gpp":
                current_app.logger.warning("Bad format scope")
                token_error = AccessTokenErr(error="invalid_scope", error_description="The first characters must be '3gpp'")
                return make_response(object=clean_empty(token_error.to_dict()), status=400)
                return False, make_response(object=clean_empty(token_error.to_dict()), status=400)

            _, body = scope.split("#")

@@ -86,7 +86,7 @@ class SecurityOperations(Resource):
                if aef_id not in aef_security_context:
                    current_app.logger.warning("Bad format Scope, not valid aef id ")
                    token_error = AccessTokenErr(error="invalid_scope", error_description="One of aef_id not belongs of your security context")
                    return make_response(object=clean_empty(token_error.to_dict()), status=400)
                    return False, make_response(object=clean_empty(token_error.to_dict()), status=400)

                api_names = api_names.split(",")
                for api_name in api_names:
@@ -97,14 +97,17 @@ class SecurityOperations(Resource):
                        token_error = AccessTokenErr(
                            error="invalid_scope",
                            error_description="One of the api names does not exist or is not associated with the aef id provided")
                        return make_response(object=clean_empty(token_error.to_dict()), status=400)
                        return False, make_response(object=clean_empty(token_error.to_dict()), status=400)
                    if "CCF" in service_api_description.get("apf_id"):
                        current_app.logger.debug(f"Found CCF APF ID in service API description: {service_api_description.get('apf_id')}, request must be forwarded to provider CCF")
                        return True, service_api_description.get("apf_id")

            return None
            return True, None

        except Exception as e:
            current_app.logger.error("Bad format Scope: " + e)
            current_app.logger.exception(f"Bad format Scope: {str(e)}")
            token_error = AccessTokenErr(error="invalid_scope", error_description="malformed scope")
            return make_response(object=clean_empty(token_error.to_dict()), status=400)
            return False, make_response(object=clean_empty(token_error.to_dict()), status=400)
    

    def __derive_psk(self, master_key:str, session_id:str, interface:dict): 
@@ -249,7 +252,7 @@ class SecurityOperations(Resource):
                return res
        except Exception as e:
            exception = "An exception occurred in get security info"
            current_app.logger.error(exception + "::" + str(e))
            current_app.logger.exception(f"{exception}::{str(e)}")
            return internal_server_error(detail=exception, cause=str(e))

    def create_servicesecurity(self, api_invoker_id, service_security):
@@ -258,10 +261,20 @@ class SecurityOperations(Resource):

        try:
            if "CCF" in g.cert_cn:
                service_security_mapped = copy.deepcopy(service_security)
                for security_info in service_security_mapped.security_info:
                    remote_api_id = security_info.api_id
                    local_api_id = self.get_local_api_id(g.cert_cn, remote_api_id)
                    security_info.api_id = local_api_id
                    current_app.logger.debug(f"Mapped remote API ID {remote_api_id} to local API ID {local_api_id}")

                current_app.logger.debug("CCF certificate detected, store security context accordingly")
                on_insert = service_security.to_dict().copy()
                on_insert.pop('security_info', None)

                mycol.find_one_and_update({'api_invoker_id': api_invoker_id},
                    {"$setOnInsert": on_insert,
                    "$push": {"security_info": {"$each": [sec.to_dict() for sec in service_security.security_info]}}},
                    "$push": {"security_info": {"$each": [sec.to_dict() for sec in service_security_mapped.security_info]}}},
                    upsert=True ,
                    return_document=ReturnDocument.AFTER,
                    projection={'_id': 0, 'api_invoker_id': 0}
@@ -330,7 +343,7 @@ class SecurityOperations(Resource):
                    if "CCF" in service_api_description.get('apf_id', ''):
                        if interconnection_security_info.get(service_api_description.get('apf_id')) is None:
                            interconnection_security_info[service_api_description.get('apf_id')] = list()
                        interconnection_security_info[service_api_description.get('apf_id')].append(service_instance)
                        interconnection_security_info[service_api_description.get('apf_id')].append(service_instance.to_dict())

                    # We obtain the interface security methods
                    # We need to go deeper here, because the interface description is an array
@@ -398,7 +411,7 @@ class SecurityOperations(Resource):
                    if "CCF" in service_api_description.get('apf_id', ''):
                        if interconnection_security_info.get(service_api_description.get('apf_id')) is None:
                            interconnection_security_info[service_api_description.get('apf_id')] = list()
                        interconnection_security_info[service_api_description.get('apf_id')].append(service_instance)
                        interconnection_security_info[service_api_description.get('apf_id')].append(service_instance.to_dict())
                    
                    # We obtain all the security methods available for the given aef_id
                    valid_security_methods = set()
@@ -504,10 +517,15 @@ class SecurityOperations(Resource):
            # The security Context should be created in the source CCF where the provider published the API
            for ccf_apf_id, security_info in interconnection_security_info.items():
                current_app.logger.debug(f"Processing interconnection security for CCF APF ID: {ccf_apf_id}")
                base_service_security = copy.deepcopy(service_security)
                base_service_security.set("security_info", copy.deepcopy(security_info))
                base_service_security = copy.deepcopy(service_security.to_dict())
                base_service_security["security_info"] = copy.deepcopy(security_info)
                # No return is checked for the interconnection security context creation population.
                self.populate_security_context(api_invoker_id, ccf_apf_id, base_service_security)
                success, response = self.populate_security_context(api_invoker_id, ccf_apf_id, base_service_security)
                if not success:
                    current_app.logger.error(f"Failed to populate interconnection security context for CCF APF ID: {ccf_apf_id}, response: {response}")
                    return response
                else:
                    current_app.logger.debug(f"Successfully populated interconnection security context for CCF APF ID: {ccf_apf_id}")


            # Send response
@@ -518,7 +536,7 @@ class SecurityOperations(Resource):

        except Exception as e:
            exception = "An exception occurred in create security info"
            current_app.logger.error(exception + "::" + str(e))
            current_app.logger.exception(f"{exception}::{str(e)}")
            return internal_server_error(detail=exception, cause=str(e))

    def delete_servicesecurity(self, api_invoker_id):
@@ -552,8 +570,8 @@ class SecurityOperations(Resource):
                return make_response(out, status=204)

        except Exception as e:
            exception = "An exception occurred in create security info"
            current_app.logger.error(exception + "::" + str(e))
            exception = "An exception occurred in delete security info"
            current_app.logger.exception(f"{exception}::{str(e)}")
            return internal_server_error(detail=exception, cause=str(e))

    def delete_intern_servicesecurity(self, api_invoker_id):
@@ -572,6 +590,9 @@ class SecurityOperations(Resource):

            invokers_col = self.db.get_col_by_name(self.db.capif_invokers)

            if "CCF" in g.cert_cn:
                current_app.logger.debug("CCF certificate detected, invoker registered in other CCF")
            else:
                current_app.logger.debug(
                    "Checking api invoker with id: " + access_token_req["client_id"])
                invoker = invokers_col.find_one(
@@ -591,11 +612,15 @@ class SecurityOperations(Resource):
                current_app.logger.warning("Not found security context with id: " + security_id)
                return not_found_error(detail= security_context_not_found_detail, cause=api_invoker_no_context_cause)

            result = self.__check_scope(
            is_success, result = self.__check_scope(
                access_token_req["scope"], service_security)

            if result != None:
            if not is_success:
                return result
            if result is not None:
                current_app.logger.debug("AEF belong to a provider registered in another CCF, requesting token")
                # Request token to Provider's CCF
                return self.request_token_to_other_ccf(security_id, result, access_token_req)

            expire_time = timedelta(minutes=10)
            now = datetime.now()
@@ -613,7 +638,7 @@ class SecurityOperations(Resource):
            return res
        except Exception as e:
            exception = "An exception occurred in return token"
            current_app.logger.error(exception + "::" + str(e))
            current_app.logger.exception(f"{exception}::{str(e)}")
            return internal_server_error(detail=exception, cause=str(e))

    def update_servicesecurity(self, api_invoker_id, service_security):
@@ -654,7 +679,7 @@ class SecurityOperations(Resource):
                        }, 
                        {"_id": 0})
                    
                    current_app.logger.debug("Aef profile: " + str(aef_profile))
                    current_app.logger.debug("Aef profile: " + str(service_api_description))

                    if service_api_description is None:
                        current_app.logger.warning(
@@ -803,7 +828,7 @@ class SecurityOperations(Resource):
            return res
        except Exception as e:
            exception = "An exception occurred in update security info"
            current_app.logger.error(exception + "::" + str(e))
            current_app.logger.exception(f"{exception}::{str(e)}")
            return internal_server_error(detail=exception, cause=str(e))

    def revoke_api_authorization(self, api_invoker_id, security_notification):
@@ -851,7 +876,7 @@ class SecurityOperations(Resource):

        except Exception as e:
            exception = "An exception occurred in revoke security auth"
            current_app.logger.error(exception + "::" + str(e))
            current_app.logger.exception(f"{exception}::{str(e)}")
            return internal_server_error(detail=exception, cause=str(e))


@@ -876,23 +901,82 @@ class SecurityOperations(Resource):
            peer could not be asked.
            """
            url = self.interconnection_security_url(ccf_id, api_invoker_id)
    # TODO-> I'M HERE!!!
            try:
                response = requests.request("PUT", url, headers={'accept': 'application/json'},
                                            cert=INTERCONNECTION_CERT, verify=INTERCONNECTION_CA,
                                            timeout=INTERCONNECTION_TIMEOUT, json=serialize_clean_camel_case(service_security))
            except requests.exceptions.RequestException as exc:
                current_app.logger.error(
                current_app.logger.exception(
                    "Interconnection: Create security context on {} failed: {}".format(url, str(exc)))
                return None, internal_server_error(
                return False, internal_server_error(
                    detail="Could not reach interconnected CCF {}".format(url), cause=str(exc))

            if response.status_code != 200:
            if response.status_code != 201:
                current_app.logger.error(
                    "Interconnection: {} answered the security service with status {}".format(
                        url, response.status_code))
                return None, internal_server_error(
                    "Interconnection: {} answered the security service with status {} and body: {}".format(
                        url, response.status_code, response.text))
                return False, internal_server_error(
                    detail="Interconnected CCF {} did not return its security service correctly".format(url),
                    cause=str(exc))
                    cause="Response status code: {} with body: {}".format(response.status_code, response.text))

            return True, None

    def get_local_api_id(self, ccf_id, remote_api_id):
        """Retrieve the local API ID corresponding to the remote API ID for the given certificate common name."""
        # TODO: Implement the mapping logic between remote API ID and local API ID
        interconnected_col = self.db.get_col_by_name(self.db.interconnected)
        interconnected_ccf = interconnected_col.find_one({"ccf_id": ccf_id})

        if interconnected_ccf is None:
            # Error
            current_app.logger.warning("Interconnected CCF with ID {} not found".format(ccf_id))
            return None
        api_remote_id_map = interconnected_ccf.get("api_remote_id_map")

        if api_remote_id_map is None:
            current_app.logger.warning("API remote ID map not found for interconnected CCF with ID {}".format(ccf_id))
            return None

        for api_id, remote_ids in api_remote_id_map.items():
            if remote_api_id in remote_ids:
                return api_id

        current_app.logger.warning("Local API ID not found for remote API ID {} in interconnected CCF with ID {}".format(remote_api_id, ccf_id))
        return None

    def interconnection_token_url(self, ccf_id, api_invoker_id):
            """Endpoint holding the token service APIs this CCF published on the given domain."""
            interconnected_col = self.db.get_col_by_name(self.db.interconnected)
            interconnected_ccf = interconnected_col.find_one({"ccf_id": ccf_id})

            if interconnected_ccf is None:
                # Error
                current_app.logger.warning("Interconnected CCF with ID {} not found".format(ccf_id))
                return None
            dom = interconnected_ccf.get("dst_prov_dom")
            return 'https://{}//capif-security/v1/securities/{}/token'.format(dom, api_invoker_id)

    def request_token_to_other_ccf(self, api_invoker_id, ccf_id, access_token_req):
            """Request a token from another interconnected CCF.
            """
            url = self.interconnection_token_url(ccf_id, api_invoker_id)
            try:
                response = requests.request("POST", url, headers={'accept': 'application/json'},
                                            cert=INTERCONNECTION_CERT, verify=INTERCONNECTION_CA,
                                            timeout=INTERCONNECTION_TIMEOUT, data=access_token_req)
            except requests.exceptions.RequestException as exc:
                current_app.logger.exception(
                    "Interconnection: Request Token on {} failed: {}".format(url, str(exc)))
                return internal_server_error(
                    detail="Could not reach interconnected CCF {}".format(url), cause=str(exc))

            if response.status_code != 200:
                current_app.logger.error(
                    "Interconnection: {} answered the Token request with status {} and body: {}".format(
                        url, response.status_code, response.text))
                return internal_server_error(
                    detail="Interconnected CCF {} did not return its Token service correctly".format(url),
                    cause="Response status code: {} with body: {}".format(response.status_code, response.text))

            return make_response(object=clean_empty(response.json()), status=200)
            return True
+6 −0
Changes for services/TS29222_CAPIF_Security_API/capif_security/util.py: 6 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -4,7 +4,13 @@ from capif_security import typing_utils


def serialize_clean_camel_case(obj):
    if isinstance(obj, dict):
        res = obj
    elif hasattr(obj, "to_dict"):
        res = obj.to_dict()
    else:
        res = obj

    res = clean_empty(res)
    res = dict_to_camel_case(res)

+1 −0
Changes for services/docker-compose-capif.yml: 1 added line, 0 removed lines.
Original line number Diff line number Diff line
@@ -321,6 +321,7 @@ services:
      - fluent-bit:host-gateway
      - otel-collector:host-gateway
      - ${CAPIF_VAULT}:host-gateway
      - ${CAPIF_INTERCONNECTION_HOSTNAME:-capifcore-b}:host-gateway
    depends_on:
      - redis
      - nginx
+1 −0
Changes for services/envs/dev1.env: 1 added line, 0 removed lines.
Original line number Diff line number Diff line
@@ -45,3 +45,4 @@ export MONGO_CCF_CONTAINER_NAME=mongo-a
export MONGO_EXPRESS_CCF_CONTAINER_NAME=mongo-express-a
export NGINX_CONTAINER_NAME=nginx-a
export MOCK_SERVER_CONTAINER_NAME=mock-server-a
export CAPIF_OPEN_DISCOVER_CONTAINER_NAME=open-api-disc-a
 No newline at end of file
+1 −0
Changes for services/envs/dev2.env: 1 added line, 0 removed lines.
Original line number Diff line number Diff line
@@ -47,3 +47,4 @@ export MONGO_CCF_CONTAINER_NAME=mongo-b
export MONGO_EXPRESS_CCF_CONTAINER_NAME=mongo-express-b
export NGINX_CONTAINER_NAME=nginx-b
export MOCK_SERVER_CONTAINER_NAME=mock-server-b
export CAPIF_OPEN_DISCOVER_CONTAINER_NAME=open-api-disc-b
 No newline at end of file
Loading