Loading services/TS29222_CAPIF_Publish_Service_API/config.yaml +44 −21 Original line number Diff line number Diff line mongo: { 'user': 'root', 'password': 'example', 'db': 'capif', 'col': 'serviceapidescriptions', 'certs_col': "certs", 'capif_provider_col': "providerenrolmentdetails", 'col_interconnected': "interconnected", 'host': 'mongo', 'port': "27017" } mongo: user: root password: example db: capif col: serviceapidescriptions certs_col: certs capif_provider_col: providerenrolmentdetails col_interconnected: interconnected col_capif_configuration: capif_configuration host: mongo port: 27017 monitoring: { "fluent_bit_host": fluent-bit, "fluent_bit_port": 24224, "opentelemetry_url": "otel-collector", "opentelemetry_port": "55680", "opentelemetry_max_queue_size": 8192, "opentelemetry_schedule_delay_millis": 20000, "opentelemetry_max_export_batch_size": 2048, "opentelemetry_export_timeout_millis": 60000 } No newline at end of file monitoring: fluent_bit_host: fluent-bit fluent_bit_port: 24224 opentelemetry_url: otel-collector opentelemetry_port: 55680 opentelemetry_max_queue_size: 8192 opentelemetry_schedule_delay_millis: 20000 opentelemetry_max_export_batch_size: 2048 opentelemetry_export_timeout_millis: 60000 ca_factory: url: !ENV ${VAULT_HOSTNAME} port: 8200 token: dev-only-token verify: False capif_configuration: config_description: Default CAPIF Configuration config_name: default config_version: "1.0" settings: acl_policy_settings: allowed_invocation_time_range_days: 365 allowed_invocations_per_second: 10 allowed_total_invocations: 5 certificates_expiry: ttl_invoker_cert: 4300h ttl_provider_cert: 4300h ttl_superadmin_cert: 4300h security_method_priority: oauth: 1 pki: 2 psk: 3 No newline at end of file services/TS29222_CAPIF_Publish_Service_API/prepare_publish.sh +32 −0 Original line number Diff line number Diff line #!/bin/bash VAULT_ADDR="http://$VAULT_HOSTNAME:$VAULT_PORT" VAULT_TOKEN=$VAULT_ACCESS_TOKEN CERTS_DIR=/usr/src/app/published_apis/certs mkdir -p "$CERTS_DIR" MAX_RETRIES=30; RETRY_DELAY=10; ATTEMPT=0 # 1) get this CCF's id from helper CCF_ID="" while [ -z "$CCF_ID" ] && [ $ATTEMPT -lt $MAX_RETRIES ]; do ATTEMPT=$((ATTEMPT+1)) CCF_ID=$(curl -sS --max-time 10 "http://helper:8080/helper/api/getCcfId" | jq -r '.ccf_id // empty') [ -z "$CCF_ID" ] && sleep $RETRY_DELAY done [ -z "$CCF_ID" ] && echo "no ccf_id" && exit 1 # 2) pull the nginx server identity + CA from Vault (retry until nginx has stored it) ATTEMPT=0 while [ $ATTEMPT -lt $MAX_RETRIES ]; do ATTEMPT=$((ATTEMPT+1)) RESP=$(curl -s -k --header "X-Vault-Token: $VAULT_TOKEN" \ --request GET "$VAULT_ADDR/v1/secret/data/capif/${CCF_ID}/nginx") CRT=$(echo "$RESP" | jq -r '.data.data.server_crt // empty') KEY=$(echo "$RESP" | jq -r '.data.data.server_key // empty') CA=$(echo "$RESP" | jq -r '.data.data.ca // empty') if [ -n "$CRT" ] && [ -n "$KEY" ] && [ -n "$CA" ]; then printf '%s\n' "$CRT" > "$CERTS_DIR/server.crt" printf '%s\n' "$KEY" > "$CERTS_DIR/server.key" printf '%s\n' "$CA" > "$CERTS_DIR/ca.crt" chmod 600 "$CERTS_DIR/server.key" break fi sleep $RETRY_DELAY done gunicorn -k uvicorn.workers.UvicornH11Worker --timeout 120 --bind 0.0.0.0:8080 \ --chdir /usr/src/app/published_apis wsgi:app services/TS29222_CAPIF_Publish_Service_API/published_apis/config.py +69 −14 Original line number Diff line number Diff line import os import yaml import re # pattern for global vars: look for ${word} pattern = re.compile(r'.*?\${(\w+)}.*?') loader = yaml.SafeLoader def constructor_env_variables(loader, node): """ Extracts the environment variable from the node's value :param yaml.Loader loader: the yaml loader :param node: the current node in the yaml :return: the parsed string that contains the value of the environment variable """ value = loader.construct_scalar(node) match = pattern.findall(value) # to find all env variables in line if match: full_value = value for g in match: full_value = full_value.replace( f'${{{g}}}', os.environ.get(g, g) ) return full_value return value def parse_config(path=None, data=None, tag='!ENV'): """ Load a yaml configuration file and resolve any environment variables The environment variables must have !ENV before them and be in this format to be parsed: ${VAR_NAME}. E.g.: database: host: !ENV ${HOST} port: !ENV ${PORT} app: log_path: !ENV '/var/${LOG_PATH}' something_else: !ENV '${AWESOME_ENV_VAR}/var/${A_SECOND_AWESOME_VAR}' :param str path: the path to the yaml file :param str data: the yaml data itself as a stream :param str tag: the tag to look for :return: the dict configuration :rtype: dict[str, T] """ # the tag will be used to mark where to start searching for the pattern # e.g. somekey: !ENV somestring${MYENVVAR}blah blah blah loader.add_implicit_resolver(tag, pattern, None) loader.add_constructor(tag, constructor_env_variables) if path: with open(path) as conf_data: return yaml.load(conf_data, Loader=loader) elif data: return yaml.load(data, Loader=loader) else: raise ValueError('Either a path or data should be defined as input') #Config class to get config class Config: def __init__(self): Loading @@ -13,9 +70,7 @@ class Config: stamp = os.stat(self.file).st_mtime if stamp != self.cached: self.cached = stamp f = open(self.file) self.my_config = yaml.safe_load(f) f.close() self.my_config = parse_config(path=self.file) def get_config(self): return self.my_config Loading services/TS29222_CAPIF_Publish_Service_API/published_apis/core/serviceapidescriptions.py +35 −12 Original line number Diff line number Diff line Loading @@ -15,6 +15,9 @@ from .resources import Resource from .responses import (bad_request_error, forbidden_error, internal_server_error, make_response, not_found_error, unauthorized_error) import requests import json import encoder TOTAL_FEATURES = 10 SUPPORTED_FEATURES_HEX = "120" Loading Loading @@ -182,23 +185,43 @@ class PublishServiceOperations(Resource): return bad_request_error( detail="Set apiStatus with apiStatusMonitoring feature inactive at supportedFeatures if not allowed", cause="apiStatus can't be set if apiStatusMonitoring is inactive", invalid_params=[{"param": "apiStatus", "reason": "defined but apiStatusMoniroting feature not active"}] invalid_params=[{"param": "apiStatus", "reason": "defined but apiStatusMonitoring feature not active"}] ) # Here code to check publish on the interconnected CCF # 1. Check shareableInfo # 2. If true then iterate through capifProvDoms # 3. For each domain, check if there is record in the "interconnected" table # 4. If yes, make a publish request with CCF... certificate # 5. When get an ACK that API was published then proceed if serviceapidescription.shareable_info.is_sharable: if serviceapidescription.shareable_info.is_shareable: interconnected_col = self.db.get_col_by_name(self.db.interconnected) # 2. Iterate through capifProvDoms for dom in serviceapidescription.shareable_info.capif_prov_doms: # 3. For each domain, check if there is record in the "interconnected" table interconnected_ccf = interconnected_col.find_one({"dst_prov_dom": dom}) # 4. If yes, make a publish request with CCF certificate if interconnected_ccf: current_app.logger.debug("CCF interconnected and API can be shared: {}".format(dom)) config_col = self.db.get_col_by_name(self.db.capif_configuration) config = config_col.find_one({}, {"_id": 0}) ccf_id = config['ccf_id'] serviceapidescription_dict['ccf_id'] = ccf_id serviceapidescription_dict['shareable_info'] = {"is_shareable": False} url = 'https://{}/published-apis/v1/{}/service-apis'.format(dom, ccf_id) current_app.logger.debug("{}".format(url)) headers = { 'accept': 'application/json', 'Content-Type': 'application/json' } current_app.logger.debug("Add variables to request") response = requests.request("POST", url, headers=headers, data=json.dumps(clean_n_camel_case(serviceapidescription_dict), cls=encoder.CustomJSONEncoder), cert=('certs/superadmin.crt', 'certs/superadmin.key'), verify='certs/ca_root.crt') if not (response.status_code == 201 or response.status_code == 200): rec['shareable_info']['capif_prov_doms'].remove(dom) mycol.insert_one(rec) self.auth_manager.add_auth_service(api_id, apf_id) Loading services/TS29222_CAPIF_Publish_Service_API/published_apis/db/db.py +9 −0 Original line number Diff line number Diff line Loading @@ -28,6 +28,7 @@ class MongoDatabse(): self.capif_provider_col = self.config['mongo']['capif_provider_col'] self.certs_col = self.config['mongo']['certs_col'] self.interconnected = self.config['mongo']['col_interconnected'] self.capif_configuration = self.config['mongo']['col_capif_configuration'] def get_col_by_name(self, name): return self.db[name].with_options(codec_options=CodecOptions(tz_aware=True)) Loading @@ -51,3 +52,11 @@ class MongoDatabse(): def close_connection(self): if self.db.client: self.db.client.close() _singleton = None def get_mongo(): global _singleton if _singleton is None: _singleton = MongoDatabse() return _singleton No newline at end of file Loading
services/TS29222_CAPIF_Publish_Service_API/config.yaml +44 −21 Original line number Diff line number Diff line mongo: { 'user': 'root', 'password': 'example', 'db': 'capif', 'col': 'serviceapidescriptions', 'certs_col': "certs", 'capif_provider_col': "providerenrolmentdetails", 'col_interconnected': "interconnected", 'host': 'mongo', 'port': "27017" } mongo: user: root password: example db: capif col: serviceapidescriptions certs_col: certs capif_provider_col: providerenrolmentdetails col_interconnected: interconnected col_capif_configuration: capif_configuration host: mongo port: 27017 monitoring: { "fluent_bit_host": fluent-bit, "fluent_bit_port": 24224, "opentelemetry_url": "otel-collector", "opentelemetry_port": "55680", "opentelemetry_max_queue_size": 8192, "opentelemetry_schedule_delay_millis": 20000, "opentelemetry_max_export_batch_size": 2048, "opentelemetry_export_timeout_millis": 60000 } No newline at end of file monitoring: fluent_bit_host: fluent-bit fluent_bit_port: 24224 opentelemetry_url: otel-collector opentelemetry_port: 55680 opentelemetry_max_queue_size: 8192 opentelemetry_schedule_delay_millis: 20000 opentelemetry_max_export_batch_size: 2048 opentelemetry_export_timeout_millis: 60000 ca_factory: url: !ENV ${VAULT_HOSTNAME} port: 8200 token: dev-only-token verify: False capif_configuration: config_description: Default CAPIF Configuration config_name: default config_version: "1.0" settings: acl_policy_settings: allowed_invocation_time_range_days: 365 allowed_invocations_per_second: 10 allowed_total_invocations: 5 certificates_expiry: ttl_invoker_cert: 4300h ttl_provider_cert: 4300h ttl_superadmin_cert: 4300h security_method_priority: oauth: 1 pki: 2 psk: 3 No newline at end of file
services/TS29222_CAPIF_Publish_Service_API/prepare_publish.sh +32 −0 Original line number Diff line number Diff line #!/bin/bash VAULT_ADDR="http://$VAULT_HOSTNAME:$VAULT_PORT" VAULT_TOKEN=$VAULT_ACCESS_TOKEN CERTS_DIR=/usr/src/app/published_apis/certs mkdir -p "$CERTS_DIR" MAX_RETRIES=30; RETRY_DELAY=10; ATTEMPT=0 # 1) get this CCF's id from helper CCF_ID="" while [ -z "$CCF_ID" ] && [ $ATTEMPT -lt $MAX_RETRIES ]; do ATTEMPT=$((ATTEMPT+1)) CCF_ID=$(curl -sS --max-time 10 "http://helper:8080/helper/api/getCcfId" | jq -r '.ccf_id // empty') [ -z "$CCF_ID" ] && sleep $RETRY_DELAY done [ -z "$CCF_ID" ] && echo "no ccf_id" && exit 1 # 2) pull the nginx server identity + CA from Vault (retry until nginx has stored it) ATTEMPT=0 while [ $ATTEMPT -lt $MAX_RETRIES ]; do ATTEMPT=$((ATTEMPT+1)) RESP=$(curl -s -k --header "X-Vault-Token: $VAULT_TOKEN" \ --request GET "$VAULT_ADDR/v1/secret/data/capif/${CCF_ID}/nginx") CRT=$(echo "$RESP" | jq -r '.data.data.server_crt // empty') KEY=$(echo "$RESP" | jq -r '.data.data.server_key // empty') CA=$(echo "$RESP" | jq -r '.data.data.ca // empty') if [ -n "$CRT" ] && [ -n "$KEY" ] && [ -n "$CA" ]; then printf '%s\n' "$CRT" > "$CERTS_DIR/server.crt" printf '%s\n' "$KEY" > "$CERTS_DIR/server.key" printf '%s\n' "$CA" > "$CERTS_DIR/ca.crt" chmod 600 "$CERTS_DIR/server.key" break fi sleep $RETRY_DELAY done gunicorn -k uvicorn.workers.UvicornH11Worker --timeout 120 --bind 0.0.0.0:8080 \ --chdir /usr/src/app/published_apis wsgi:app
services/TS29222_CAPIF_Publish_Service_API/published_apis/config.py +69 −14 Original line number Diff line number Diff line import os import yaml import re # pattern for global vars: look for ${word} pattern = re.compile(r'.*?\${(\w+)}.*?') loader = yaml.SafeLoader def constructor_env_variables(loader, node): """ Extracts the environment variable from the node's value :param yaml.Loader loader: the yaml loader :param node: the current node in the yaml :return: the parsed string that contains the value of the environment variable """ value = loader.construct_scalar(node) match = pattern.findall(value) # to find all env variables in line if match: full_value = value for g in match: full_value = full_value.replace( f'${{{g}}}', os.environ.get(g, g) ) return full_value return value def parse_config(path=None, data=None, tag='!ENV'): """ Load a yaml configuration file and resolve any environment variables The environment variables must have !ENV before them and be in this format to be parsed: ${VAR_NAME}. E.g.: database: host: !ENV ${HOST} port: !ENV ${PORT} app: log_path: !ENV '/var/${LOG_PATH}' something_else: !ENV '${AWESOME_ENV_VAR}/var/${A_SECOND_AWESOME_VAR}' :param str path: the path to the yaml file :param str data: the yaml data itself as a stream :param str tag: the tag to look for :return: the dict configuration :rtype: dict[str, T] """ # the tag will be used to mark where to start searching for the pattern # e.g. somekey: !ENV somestring${MYENVVAR}blah blah blah loader.add_implicit_resolver(tag, pattern, None) loader.add_constructor(tag, constructor_env_variables) if path: with open(path) as conf_data: return yaml.load(conf_data, Loader=loader) elif data: return yaml.load(data, Loader=loader) else: raise ValueError('Either a path or data should be defined as input') #Config class to get config class Config: def __init__(self): Loading @@ -13,9 +70,7 @@ class Config: stamp = os.stat(self.file).st_mtime if stamp != self.cached: self.cached = stamp f = open(self.file) self.my_config = yaml.safe_load(f) f.close() self.my_config = parse_config(path=self.file) def get_config(self): return self.my_config Loading
services/TS29222_CAPIF_Publish_Service_API/published_apis/core/serviceapidescriptions.py +35 −12 Original line number Diff line number Diff line Loading @@ -15,6 +15,9 @@ from .resources import Resource from .responses import (bad_request_error, forbidden_error, internal_server_error, make_response, not_found_error, unauthorized_error) import requests import json import encoder TOTAL_FEATURES = 10 SUPPORTED_FEATURES_HEX = "120" Loading Loading @@ -182,23 +185,43 @@ class PublishServiceOperations(Resource): return bad_request_error( detail="Set apiStatus with apiStatusMonitoring feature inactive at supportedFeatures if not allowed", cause="apiStatus can't be set if apiStatusMonitoring is inactive", invalid_params=[{"param": "apiStatus", "reason": "defined but apiStatusMoniroting feature not active"}] invalid_params=[{"param": "apiStatus", "reason": "defined but apiStatusMonitoring feature not active"}] ) # Here code to check publish on the interconnected CCF # 1. Check shareableInfo # 2. If true then iterate through capifProvDoms # 3. For each domain, check if there is record in the "interconnected" table # 4. If yes, make a publish request with CCF... certificate # 5. When get an ACK that API was published then proceed if serviceapidescription.shareable_info.is_sharable: if serviceapidescription.shareable_info.is_shareable: interconnected_col = self.db.get_col_by_name(self.db.interconnected) # 2. Iterate through capifProvDoms for dom in serviceapidescription.shareable_info.capif_prov_doms: # 3. For each domain, check if there is record in the "interconnected" table interconnected_ccf = interconnected_col.find_one({"dst_prov_dom": dom}) # 4. If yes, make a publish request with CCF certificate if interconnected_ccf: current_app.logger.debug("CCF interconnected and API can be shared: {}".format(dom)) config_col = self.db.get_col_by_name(self.db.capif_configuration) config = config_col.find_one({}, {"_id": 0}) ccf_id = config['ccf_id'] serviceapidescription_dict['ccf_id'] = ccf_id serviceapidescription_dict['shareable_info'] = {"is_shareable": False} url = 'https://{}/published-apis/v1/{}/service-apis'.format(dom, ccf_id) current_app.logger.debug("{}".format(url)) headers = { 'accept': 'application/json', 'Content-Type': 'application/json' } current_app.logger.debug("Add variables to request") response = requests.request("POST", url, headers=headers, data=json.dumps(clean_n_camel_case(serviceapidescription_dict), cls=encoder.CustomJSONEncoder), cert=('certs/superadmin.crt', 'certs/superadmin.key'), verify='certs/ca_root.crt') if not (response.status_code == 201 or response.status_code == 200): rec['shareable_info']['capif_prov_doms'].remove(dom) mycol.insert_one(rec) self.auth_manager.add_auth_service(api_id, apf_id) Loading
services/TS29222_CAPIF_Publish_Service_API/published_apis/db/db.py +9 −0 Original line number Diff line number Diff line Loading @@ -28,6 +28,7 @@ class MongoDatabse(): self.capif_provider_col = self.config['mongo']['capif_provider_col'] self.certs_col = self.config['mongo']['certs_col'] self.interconnected = self.config['mongo']['col_interconnected'] self.capif_configuration = self.config['mongo']['col_capif_configuration'] def get_col_by_name(self, name): return self.db[name].with_options(codec_options=CodecOptions(tz_aware=True)) Loading @@ -51,3 +52,11 @@ class MongoDatabse(): def close_connection(self): if self.db.client: self.db.client.close() _singleton = None def get_mongo(): global _singleton if _singleton is None: _singleton = MongoDatabse() return _singleton No newline at end of file