diff --git a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/config.py b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/config.py index abfa40820dc99bf2c64c7bf0a31994eedc414e03..f4c87cae4aa1da82b8c83190a445a7b21c041970 100644 --- a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/config.py +++ b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/config.py @@ -1,23 +1,75 @@ import os import yaml +import re + + +# pattern for global vars: look for ${word} +pattern = re.compile('.*?\${(\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): - self.cached = 0 - self.file="../config.yaml" - self.my_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() + def __init__(self): + self.cached = 0 + self.file="../config.yaml" + self.my_config = {} - def get_config(self): - return self.my_config + self.my_config = parse_config(path=self.file) + def get_config(self): + return self.my_config diff --git a/services/TS29222_CAPIF_API_Invoker_Management_API/config.yaml b/services/TS29222_CAPIF_API_Invoker_Management_API/config.yaml index 3107e411fbdaecb7e9bf44f86025d4b27fd81e49..c3e9f07afce53379fcf4d5f46e0c2f6e8f0dbdc9 100644 --- a/services/TS29222_CAPIF_API_Invoker_Management_API/config.yaml +++ b/services/TS29222_CAPIF_API_Invoker_Management_API/config.yaml @@ -9,12 +9,12 @@ mongo: { 'port': "27017" } -ca_factory: { - "url": "vault", - "port": "8200", - "token": "dev-only-token", - "verify": False -} +ca_factory: + url: !ENV ${VAULT_HOSTNAME} + port: "8200" + token: "dev-only-token" + verify: False + monitoring: { "fluent_bit_host": fluent-bit, diff --git a/services/TS29222_CAPIF_API_Invoker_Management_API/prepare_invoker.sh b/services/TS29222_CAPIF_API_Invoker_Management_API/prepare_invoker.sh index 0e2accbb5cae55f617d052f22b0de73a752f66a4..d8bcf6ca9afee19a63d442a3bf8edf4e17dcba45 100644 --- a/services/TS29222_CAPIF_API_Invoker_Management_API/prepare_invoker.sh +++ b/services/TS29222_CAPIF_API_Invoker_Management_API/prepare_invoker.sh @@ -26,7 +26,7 @@ while [ $ATTEMPT -lt $MAX_RETRIES ]; do if [ -n "$RESPONSE" ] && [ "$RESPONSE" != "null" ]; then echo "$RESPONSE" > /usr/src/app/api_invoker_management/pubkey.pem echo "Public key successfully saved." - gunicorn -k uvicorn.workers.UvicornH11Worker --bind 0.0.0.0:8080 \ + gunicorn -k uvicorn.workers.UvicornH11Worker --timeout 120 --bind 0.0.0.0:8080 \ --chdir /usr/src/app/api_invoker_management wsgi:app exit 0 # Exit successfully else diff --git a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/config.py b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/config.py index 404434feca0f72cd914c4cbdb94a3fa589bc94b0..f4c87cae4aa1da82b8c83190a445a7b21c041970 100644 --- a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/config.py +++ b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/config.py @@ -1,20 +1,75 @@ import os import yaml +import re + + +# pattern for global vars: look for ${word} +pattern = re.compile('.*?\${(\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): - self.cached = 0 - self.file="../config.yaml" - self.my_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() - - def get_config(self): - return self.my_config + def __init__(self): + self.cached = 0 + self.file="../config.yaml" + self.my_config = {} + + self.my_config = parse_config(path=self.file) + + def get_config(self): + return self.my_config + diff --git a/services/TS29222_CAPIF_API_Provider_Management_API/config.yaml b/services/TS29222_CAPIF_API_Provider_Management_API/config.yaml index ce684f368f6bec6f91775710f96ed097fe0d6bf3..e2bb0de0db799822253f5ad423c978168b444c68 100644 --- a/services/TS29222_CAPIF_API_Provider_Management_API/config.yaml +++ b/services/TS29222_CAPIF_API_Provider_Management_API/config.yaml @@ -9,12 +9,12 @@ mongo: { } -ca_factory: { - "url": "vault", - "port": "8200", - "token": "dev-only-token", - "verify": False -} +ca_factory: + url: !ENV ${VAULT_HOSTNAME} + port: "8200" + token: "dev-only-token" + verify: False + monitoring: { diff --git a/services/TS29222_CAPIF_API_Provider_Management_API/prepare_provider.sh b/services/TS29222_CAPIF_API_Provider_Management_API/prepare_provider.sh index edefb5829ae4eeeea723113ef63f4c49c78c3c20..297f69758eb182dd4ef33464abadc57350fc80b8 100644 --- a/services/TS29222_CAPIF_API_Provider_Management_API/prepare_provider.sh +++ b/services/TS29222_CAPIF_API_Provider_Management_API/prepare_provider.sh @@ -26,7 +26,7 @@ while [ $ATTEMPT -lt $MAX_RETRIES ]; do if [ -n "$RESPONSE" ] && [ "$RESPONSE" != "null" ]; then echo "$RESPONSE" > /usr/src/app/api_provider_management/pubkey.pem echo "Public key successfully saved." - gunicorn -k uvicorn.workers.UvicornH11Worker --bind 0.0.0.0:8080 \ + gunicorn -k uvicorn.workers.UvicornH11Worker --timeout 120 --bind 0.0.0.0:8080 \ --chdir /usr/src/app/api_provider_management wsgi:app exit 0 # Exit successfully else diff --git a/services/TS29222_CAPIF_Access_Control_Policy_API/prepare_capif_acl.sh b/services/TS29222_CAPIF_Access_Control_Policy_API/prepare_capif_acl.sh index 42f12afe4ac1e7bf0389e43aa7181f7670b03ed6..41d326c5c623777ecce2379630bf31274edc1479 100644 --- a/services/TS29222_CAPIF_Access_Control_Policy_API/prepare_capif_acl.sh +++ b/services/TS29222_CAPIF_Access_Control_Policy_API/prepare_capif_acl.sh @@ -1,4 +1,4 @@ #!/bin/bash -gunicorn -k uvicorn.workers.UvicornH11Worker --bind 0.0.0.0:8080 \ +gunicorn -k uvicorn.workers.UvicornH11Worker --timeout 120 --bind 0.0.0.0:8080 \ --chdir /usr/src/app/capif_acl wsgi:app \ No newline at end of file diff --git a/services/TS29222_CAPIF_Auditing_API/prepare_audit.sh b/services/TS29222_CAPIF_Auditing_API/prepare_audit.sh index 6049f2e972087e168236f5a3cc4c5883559f5395..75fee0b1e1f1521b1397cef7ab17e0865d8d814c 100644 --- a/services/TS29222_CAPIF_Auditing_API/prepare_audit.sh +++ b/services/TS29222_CAPIF_Auditing_API/prepare_audit.sh @@ -1,6 +1,6 @@ #!/bin/bash -gunicorn -k uvicorn.workers.UvicornH11Worker --bind 0.0.0.0:8080 \ +gunicorn -k uvicorn.workers.UvicornH11Worker --timeout 120 --bind 0.0.0.0:8080 \ --chdir /usr/src/app/logs wsgi:app diff --git a/services/TS29222_CAPIF_Discover_Service_API/prepare_discover.sh b/services/TS29222_CAPIF_Discover_Service_API/prepare_discover.sh index dd3f3612ac0e74093c40677f2154c92f2f07f0c7..43bcd30c565270288c34b1b7048ec42efc1557fe 100644 --- a/services/TS29222_CAPIF_Discover_Service_API/prepare_discover.sh +++ b/services/TS29222_CAPIF_Discover_Service_API/prepare_discover.sh @@ -1,6 +1,6 @@ #!/bin/bash -gunicorn -k uvicorn.workers.UvicornH11Worker --bind 0.0.0.0:8080 \ +gunicorn -k uvicorn.workers.UvicornH11Worker --timeout 120 --bind 0.0.0.0:8080 \ --chdir /usr/src/app/service_apis wsgi:app diff --git a/services/TS29222_CAPIF_Events_API/prepare_events.sh b/services/TS29222_CAPIF_Events_API/prepare_events.sh index bf16c7f30a46cc93a9c04356bd799f3bc1a86ba1..e3bf0423d6e49f6d943c3bc8838e2ec8031037a1 100644 --- a/services/TS29222_CAPIF_Events_API/prepare_events.sh +++ b/services/TS29222_CAPIF_Events_API/prepare_events.sh @@ -1,4 +1,4 @@ #!/bin/bash -gunicorn -k uvicorn.workers.UvicornH11Worker --bind 0.0.0.0:8080 \ +gunicorn -k uvicorn.workers.UvicornH11Worker --timeout 120 --bind 0.0.0.0:8080 \ --chdir /usr/src/app/capif_events wsgi:app diff --git a/services/TS29222_CAPIF_Logging_API_Invocation_API/prepare_logging.sh b/services/TS29222_CAPIF_Logging_API_Invocation_API/prepare_logging.sh index 25c0c0ecebebf208c78bf60c6e08cde0613a3fdf..29b210da24c3a0a0a1305dc35014298288223bd0 100644 --- a/services/TS29222_CAPIF_Logging_API_Invocation_API/prepare_logging.sh +++ b/services/TS29222_CAPIF_Logging_API_Invocation_API/prepare_logging.sh @@ -1,4 +1,4 @@ #!/bin/bash -gunicorn -k uvicorn.workers.UvicornH11Worker --bind 0.0.0.0:8080 \ +gunicorn -k uvicorn.workers.UvicornH11Worker --timeout 120 --bind 0.0.0.0:8080 \ --chdir /usr/src/app/api_invocation_logs wsgi:app diff --git a/services/TS29222_CAPIF_Publish_Service_API/prepare_publish.sh b/services/TS29222_CAPIF_Publish_Service_API/prepare_publish.sh index 8526fe8899f3bab0fceb15eb6c7420345536e849..6e019e1c5879f8b0c08d470cae390e368711ceb8 100644 --- a/services/TS29222_CAPIF_Publish_Service_API/prepare_publish.sh +++ b/services/TS29222_CAPIF_Publish_Service_API/prepare_publish.sh @@ -1,4 +1,4 @@ #!/bin/bash -gunicorn -k uvicorn.workers.UvicornH11Worker --bind 0.0.0.0:8080 \ +gunicorn -k uvicorn.workers.UvicornH11Worker --timeout 120 --bind 0.0.0.0:8080 \ --chdir /usr/src/app/published_apis wsgi:app diff --git a/services/TS29222_CAPIF_Routing_Info_API/prepare_routing_info.sh b/services/TS29222_CAPIF_Routing_Info_API/prepare_routing_info.sh index aa37163ff34156c5cfa585de5974390c09905ddf..2c8c8d10d8379ee325864f9048d3887627ee2f66 100644 --- a/services/TS29222_CAPIF_Routing_Info_API/prepare_routing_info.sh +++ b/services/TS29222_CAPIF_Routing_Info_API/prepare_routing_info.sh @@ -1,4 +1,4 @@ #!/bin/bash -gunicorn -k uvicorn.workers.UvicornH11Worker --bind 0.0.0.0:8080 \ +gunicorn -k uvicorn.workers.UvicornH11Worker --timeout 120 --bind 0.0.0.0:8080 \ --chdir /usr/src/app/capif_routing_info wsgi:app diff --git a/services/TS29222_CAPIF_Security_API/prepare_security.sh b/services/TS29222_CAPIF_Security_API/prepare_security.sh index c14609ad221170db6da6490cd3a77e85dd8f3ee3..191d727b5c2934d86e5980a328f8f69cdb3cfb2b 100644 --- a/services/TS29222_CAPIF_Security_API/prepare_security.sh +++ b/services/TS29222_CAPIF_Security_API/prepare_security.sh @@ -76,5 +76,5 @@ if [ "$SUCCES_OPERATION" = false ]; then exit 1 # Exit with failure fi -gunicorn -k uvicorn.workers.UvicornH11Worker --bind 0.0.0.0:8080 \ +gunicorn -k uvicorn.workers.UvicornH11Worker --timeout 120 --bind 0.0.0.0:8080 \ --chdir $CERTS_FOLDER wsgi:app diff --git a/services/docker-compose-capif.yml b/services/docker-compose-capif.yml index 01a34a6c9d53eee9bf3b97b45693a25db4ff58f8..67af02517ca0e18f4c6f300a9ca67b1b74d60ac4 100644 --- a/services/docker-compose-capif.yml +++ b/services/docker-compose-capif.yml @@ -21,6 +21,7 @@ services: volumes: - ${SERVICES_DIR}/helper/config.yaml:/usr/src/app/config.yaml - ${SERVICES_DIR}/helper/helper_service/certs:/usr/src/app/helper_service/certs + - ${SERVICES_DIR}/nginx/certs:/usr/src/app/helper_service/server_certs:ro extra_hosts: - host.docker.internal:host-gateway - fluent-bit:host-gateway @@ -29,7 +30,7 @@ services: environment: - CAPIF_HOSTNAME=${CAPIF_HOSTNAME} - CONTAINER_NAME=helper - - VAULT_HOSTNAME=vault + - VAULT_HOSTNAME=${CAPIF_VAULT} - VAULT_ACCESS_TOKEN=dev-only-token - VAULT_PORT=8200 - LOG_LEVEL=${LOG_LEVEL} @@ -76,7 +77,7 @@ services: - CAPIF_HOSTNAME=${CAPIF_HOSTNAME} - CONTAINER_NAME=api-invoker-management - MONITORING=${MONITORING} - - VAULT_HOSTNAME=vault + - VAULT_HOSTNAME=${CAPIF_VAULT} - VAULT_ACCESS_TOKEN=dev-only-token - VAULT_PORT=8200 - LOG_LEVEL=${LOG_LEVEL} @@ -104,7 +105,7 @@ services: - CAPIF_HOSTNAME=${CAPIF_HOSTNAME} - CONTAINER_NAME=api-provider-management - MONITORING=${MONITORING} - - VAULT_HOSTNAME=vault + - VAULT_HOSTNAME=${CAPIF_VAULT} - VAULT_ACCESS_TOKEN=dev-only-token - VAULT_PORT=8200 - LOG_LEVEL=${LOG_LEVEL} @@ -263,7 +264,7 @@ services: - CAPIF_HOSTNAME=${CAPIF_HOSTNAME} - CONTAINER_NAME=api-security - MONITORING=${MONITORING} - - VAULT_HOSTNAME=vault + - VAULT_HOSTNAME=${CAPIF_VAULT} - VAULT_ACCESS_TOKEN=dev-only-token - VAULT_PORT=8200 - LOG_LEVEL=${LOG_LEVEL} @@ -312,7 +313,7 @@ services: image: labs.etsi.org:5050/ocf/capif/nginx-ocf-patched:1.27.1 environment: - CAPIF_HOSTNAME=${CAPIF_HOSTNAME} - - VAULT_HOSTNAME=vault + - VAULT_HOSTNAME=${CAPIF_VAULT} - VAULT_ACCESS_TOKEN=dev-only-token - VAULT_PORT=8200 - LOG_LEVEL=${LOG_LEVEL} diff --git a/services/docker-compose-register.yml b/services/docker-compose-register.yml index 66607e557686a8e4a650ba3165b234c6cc7dd57f..5f12ed8c5764400b8d344ca951c5c65f2170f37c 100644 --- a/services/docker-compose-register.yml +++ b/services/docker-compose-register.yml @@ -10,7 +10,7 @@ services: - ${SERVICES_DIR}/register/config.yaml:/usr/src/app/config.yaml environment: - CAPIF_PRIV_KEY=${CAPIF_PRIV_KEY} - - VAULT_HOSTNAME=vault + - VAULT_HOSTNAME=${CAPIF_VAULT} - VAULT_ACCESS_TOKEN=dev-only-token - VAULT_PORT=8200 - LOG_LEVEL=${LOG_LEVEL} diff --git a/services/helper/config.yaml b/services/helper/config.yaml index 2cf3193d4cff3c52c5c887db11721c3002e37dc6..f331cb3848d08496427fd4ad9c20c3607b8ab9db 100644 --- a/services/helper/config.yaml +++ b/services/helper/config.yaml @@ -8,12 +8,13 @@ mongo: col_security: security col_event: eventsdetails col_capif_configuration: capif_configuration + col_interconnected: interconnected host: mongo port: 27017 ca_factory: - url: vault + url: !ENV ${VAULT_HOSTNAME} port: 8200 token: dev-only-token verify: False @@ -44,4 +45,7 @@ package_paths: configuration_api: path: /configuration openapi_file: configuration/openapi/openapi.yaml + interconnection_api: + path: /interconnection + openapi_file: interconnection/openapi/openapi.yaml diff --git a/services/helper/helper_service/app.py b/services/helper/helper_service/app.py index 88d918b93926a9cea8078bd8e32d1b11d61a7f3e..e4f9ed4f8552fe039751a1826bd4d33113ad0c2f 100644 --- a/services/helper/helper_service/app.py +++ b/services/helper/helper_service/app.py @@ -46,6 +46,7 @@ req.get_subject().OU = 'helper' req.get_subject().L = 'Madrid' req.get_subject().ST = 'Madrid' req.get_subject().C = 'ES' +# req.get_subject().CN = "superadmin{}".format(os.getenv("CAPIF_HOSTNAME")) req.get_subject().emailAddress = 'helper@tid.es' req.set_pubkey(key) req.sign(key, 'sha256') @@ -66,7 +67,9 @@ data = { 'format':'pem_bundle', 'ttl': ttl_superadmin_cert, 'csr': csr_request, - 'common_name': "superadmin" + # 'common_name': "superadmin{}".format(os.getenv("CAPIF_HOSTNAME")), + 'common_name': "superadmin", + 'alt_names': "{}".format(os.getenv("CAPIF_HOSTNAME")) } response = requests.request("POST", url, headers=headers, data=data, verify = config["ca_factory"].get("verify", False)) diff --git a/services/helper/helper_service/config.py b/services/helper/helper_service/config.py index d9f4ad1c1342cf15c3742577c234da6f40b6db77..f4c87cae4aa1da82b8c83190a445a7b21c041970 100644 --- a/services/helper/helper_service/config.py +++ b/services/helper/helper_service/config.py @@ -1,6 +1,64 @@ import os import yaml +import re + + +# pattern for global vars: look for ${word} +pattern = re.compile('.*?\${(\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 @@ -10,12 +68,7 @@ class Config: self.file="../config.yaml" self.my_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 diff --git a/services/helper/helper_service/db/db.py b/services/helper/helper_service/db/db.py index 757933d39fb555c8a71dcedb4e893379276b35cb..f8c7e014724f84fcc64938f64d4f66533a4e3143 100644 --- a/services/helper/helper_service/db/db.py +++ b/services/helper/helper_service/db/db.py @@ -38,6 +38,7 @@ class MongoDatabse(): self.security_context_col = self.config['mongo']['col_security'] self.events = self.config['mongo']['col_event'] self.capif_configuration = self.config['mongo']['col_capif_configuration'] + self.interconnected = self.config['mongo']['col_interconnected'] self.initialize_capif_configuration() diff --git a/services/helper/helper_service/openapi_helper_interconnection.yaml b/services/helper/helper_service/openapi_helper_interconnection.yaml new file mode 100644 index 0000000000000000000000000000000000000000..d038ec151423ae4b78ea3de880d57b02ddf5e624 --- /dev/null +++ b/services/helper/helper_service/openapi_helper_interconnection.yaml @@ -0,0 +1,212 @@ +openapi: 3.0.3 # The version of the OpenAPI standard +info: + title: CCF Interconnection API + description: APIs for interconnnecting CCFs + version: 1.0.0 +servers: +- url: "{apiRoot}/interconnection" + variables: + apiRoot: + default: http://localhost:8080 + description: Base URL of the Helper service. +# 1. PATHS: Where you define your endpoints +paths: + /interconnect: + post: + summary: Send a new interconnection request + operationId: interconnect_request # <--- Becomes 'def interconnect_request(body):' in Python + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CapifDomainDetails' # Reference to the model below + responses: + "201": + description: Interconnection request succeeded + content: + application/json: + schema: + $ref: '#/components/schemas/CapifDomainDetails' + "400": + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ProblemDetails' + description: Bad request + "401": + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ProblemDetails' + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ProblemDetails' + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ProblemDetails' + description: Not Found + "500": + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ProblemDetails' + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ProblemDetails' + description: Service Unavailable + default: + description: Generic Error + /connect: + post: + summary: Create a new interconnection + operationId: connect_ccfs # <--- Becomes 'def connect_ccfs(body):' in Python + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CcfInstanceDetails' # Reference to the model below + responses: + "201": + description: Interconnection succeeded + content: + application/json: + schema: + $ref: '#/components/schemas/CcfInstanceDetails' + "400": + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ProblemDetails' + description: Bad request + "401": + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ProblemDetails' + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ProblemDetails' + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ProblemDetails' + description: Not Found + "500": + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ProblemDetails' + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ProblemDetails' + description: Service Unavailable + default: + description: Generic Error +# 2. COMPONENTS: Reusable data models (Classes in Python) +components: + schemas: + CapifDomainDetails: + type: object + required: + - dstProvDom + properties: + dstProvDom: + type: string + CcfInstanceDetails: + type: object + required: + - caRoot + - publicKey + - ccfId + - dstProvDom + properties: + caRoot: + type: string + publicKey: + type: string + ccfId: + type: string + dstProvDom: + type: string + ProblemDetails: + description: Represents additional information and details on an error response. + properties: + type: + description: string providing an URI formatted according to IETF RFC 3986. + title: type + type: string + title: + description: "A short, human-readable summary of the problem type. It should\ + \ not change from occurrence to occurrence of the problem. \n" + title: title + type: string + status: + description: The HTTP status code for this occurrence of the problem. + title: status + type: integer + detail: + description: A human-readable explanation specific to this occurrence of + the problem. + title: detail + type: string + instance: + description: string providing an URI formatted according to IETF RFC 3986. + title: type + type: string + cause: + description: | + A machine-readable application error cause specific to this occurrence of the problem. This IE should be present and provide application-related error information, if available. + title: cause + type: string + invalidParams: + description: | + Description of invalid parameters, for a request rejected due to invalid parameters. + items: + $ref: '#/components/schemas/InvalidParam' + minItems: 1 + title: invalidParams + type: array + supportedFeatures: + description: | + A string used to indicate the features supported by an API that is used as defined in clause 6.6 in 3GPP TS 29.500. The string shall contain a bitmask indicating supported features in hexadecimal representation Each character in the string shall take a value of "0" to "9", "a" to "f" or "A" to "F" and shall represent the support of 4 features as described in table 5.2.2-3. The most significant character representing the highest-numbered features shall appear first in the string, and the character representing features 1 to 4 shall appear last in the string. The list of features and their numbering (starting with 1) are defined separately for each API. If the string contains a lower number of characters than there are defined features for an API, all features that would be represented by characters that are not present in the string are not supported. + pattern: "^[A-Fa-f0-9]*$" + title: supportedFeatures + type: string + title: ProblemDetails + type: object + InvalidParam: + description: | + Represents the description of invalid parameters, for a request rejected due to invalid parameters. + properties: + param: + description: "Attribute's name encoded as a JSON Pointer, or header's name." + title: param + type: string + reason: + description: "A human-readable reason, e.g. \"must be a positive integer\"\ + ." + title: reason + type: string + required: + - param + title: InvalidParam + type: object \ No newline at end of file diff --git a/services/helper/helper_service/services/interconnection/__init__.py b/services/helper/helper_service/services/interconnection/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/services/helper/helper_service/services/interconnection/__main__.py b/services/helper/helper_service/services/interconnection/__main__.py new file mode 100644 index 0000000000000000000000000000000000000000..916cc33edd1cd0b9a8de7bdaaf8807419faaed59 --- /dev/null +++ b/services/helper/helper_service/services/interconnection/__main__.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 + +import connexion + +from interconnection import encoder + + +def main(): + app = connexion.App(__name__, specification_dir='./openapi/') + app.app.json_encoder = encoder.JSONEncoder + app.add_api('openapi.yaml', + arguments={'title': 'CCF Interconnection API'}, + pythonic_params=True) + + app.run(port=8080) + + +if __name__ == '__main__': + main() diff --git a/services/helper/helper_service/services/interconnection/controllers/__init__.py b/services/helper/helper_service/services/interconnection/controllers/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/services/helper/helper_service/services/interconnection/controllers/default_controller.py b/services/helper/helper_service/services/interconnection/controllers/default_controller.py new file mode 100644 index 0000000000000000000000000000000000000000..9a2d6d89dade409cdb96223ea0d124b63ae1543b --- /dev/null +++ b/services/helper/helper_service/services/interconnection/controllers/default_controller.py @@ -0,0 +1,52 @@ +import connexion +from typing import Dict +from typing import Tuple +from typing import Union + +from interconnection.models.capif_domain_details import CapifDomainDetails # noqa: E501 +from interconnection.models.ccf_instance_details import CcfInstanceDetails # noqa: E501 +from interconnection.models.problem_details import ProblemDetails # noqa: E501 +from interconnection import util +from ..core.ccfinstancedetails import CcfInstanceOperations +from ..core.capifdomaindetails import CapifDomainOperations + +ccf_operations = CcfInstanceOperations() +capif_domain_operations = CapifDomainOperations() + + +def connect_ccfs(body): # noqa: E501 + """Create a new interconnection + + # noqa: E501 + + :param ccf_instance_details: + :type ccf_instance_details: dict | bytes + + :rtype: Union[CcfInstanceDetails, Tuple[CcfInstanceDetails, int], Tuple[CcfInstanceDetails, int, Dict[str, str]] + """ + ccf_instance_details = body + if connexion.request.is_json: + ccf_instance_details = CcfInstanceDetails.from_dict(connexion.request.get_json()) # noqa: E501 + + res = ccf_operations.add_ccfinstance(ccf_instance_details) + + return res + + +def interconnect_request(body): # noqa: E501 + """Send a new interconnection request + + # noqa: E501 + + :param capif_domain_details: + :type capif_domain_details: dict | bytes + + :rtype: Union[CapifDomainDetails, Tuple[CapifDomainDetails, int], Tuple[CapifDomainDetails, int, Dict[str, str]] + """ + capif_domain_details = body + if connexion.request.is_json: + capif_domain_details = CapifDomainDetails.from_dict(connexion.request.get_json()) # noqa: E501 + + res = capif_domain_operations.add_capifdomain(capif_domain_details) + + return res diff --git a/services/helper/helper_service/services/interconnection/controllers/security_controller.py b/services/helper/helper_service/services/interconnection/controllers/security_controller.py new file mode 100644 index 0000000000000000000000000000000000000000..6d294ffd6df1a26a469dbb4e72533b01503468dd --- /dev/null +++ b/services/helper/helper_service/services/interconnection/controllers/security_controller.py @@ -0,0 +1,2 @@ +from typing import List + diff --git a/services/helper/helper_service/services/interconnection/core/auth_manager.py b/services/helper/helper_service/services/interconnection/core/auth_manager.py new file mode 100644 index 0000000000000000000000000000000000000000..d20daffd0d563609e153faada2fbd1f27dcba62f --- /dev/null +++ b/services/helper/helper_service/services/interconnection/core/auth_manager.py @@ -0,0 +1,46 @@ + +from flask import current_app + +from .resources import Resource + + +class AuthManager(Resource): + + def add_auth_service(self, service_id, apf_id): + cert_col = self.db.get_col_by_name(self.db.certs_col) + + auth_context = cert_col.find_one({"id":apf_id}) + + if "services" in auth_context["resources"]: + if service_id not in auth_context["resources"]["services"]: + auth_context["resources"]["services"].append(service_id) + else: + auth_context["resources"]["services"] = [service_id] + + current_app.logger.info(auth_context) + cert_col.find_one_and_update({"id":apf_id}, {"$set":auth_context}) + + + def remove_auth_service(self, service_id, apf_id): + + cert_col = self.db.get_col_by_name(self.db.certs_col) + + auth_context = cert_col.find_one({"id":apf_id}) + + if "services" in auth_context["resources"]: + if service_id in auth_context["resources"]["services"]: + auth_context["resources"]["services"].remove(service_id) + + cert_col.find_one_and_update({"id":apf_id}, {"$set":auth_context}) + + def remove_auth_all_service(self, apf_id): + + cert_col = self.db.get_col_by_name(self.db.certs_col) + + auth_context = cert_col.find_one({"id":apf_id}) + + if auth_context != None: + if "services" in auth_context["resources"]: + auth_context["resources"]["services"] = [] + + cert_col.find_one_and_update({"id":apf_id}, {"$set":auth_context}) diff --git a/services/helper/helper_service/services/interconnection/core/capifdomaindetails.py b/services/helper/helper_service/services/interconnection/core/capifdomaindetails.py new file mode 100644 index 0000000000000000000000000000000000000000..62813de446e83ebb51d264cae614ef534f947d6c --- /dev/null +++ b/services/helper/helper_service/services/interconnection/core/capifdomaindetails.py @@ -0,0 +1,86 @@ +import os +import secrets +from datetime import datetime +import requests +import json + +from flask import current_app +# from pymongo import ReturnDocument + +from db.db import get_mongo +from ..models.ccf_instance_details import CcfInstanceDetails +from ..util import clean_empty, clean_n_camel_case, dict_to_camel_case, serialize_clean_camel_case +# from ..vendor_specific import add_vend_spec_fields +from .auth_manager import AuthManager +from .publisher import Publisher +from .redis_event import RedisEvent +from .resources import Resource +from .responses import ( + bad_request_error, + forbidden_error, + internal_server_error, + make_response, + not_found_error, + unauthorized_error +) + +publisher_ops = Publisher() + + +service_api_not_found_message = "Service API not found" + + +class CapifDomainOperations(Resource): + + def __init__(self): + Resource.__init__(self) + self.auth_manager = AuthManager() + self.db = get_mongo() + + def add_capifdomain(self, capifdomaindetails): + current_app.logger.debug("Interconnection: Add domain") + current_app.logger.debug(capifdomaindetails) + + interconnected_col = self.db.get_col_by_name(self.db.interconnected) + interconnected_ccf = interconnected_col.find_one({"dst_prov_dom": capifdomaindetails.dst_prov_dom}) + if interconnected_ccf: + current_app.logger.debug("CAPIF domain already interconnected : {}".format(capifdomaindetails.dst_prov_dom)) + return make_response("CAPIF domain already interconnected", 409) + + url = "https://{}/helper/interconnection/connect".format(capifdomaindetails.dst_prov_dom) + + with open('server_certs/ca.crt', 'rb') as ca_cert: + server_ca = ca_cert.read() + ca_cert.close() + + with open('server_certs/server.crt', 'rb') as server_cert: + server_pub = server_cert.read() + server_cert.close() + + config_col = self.db.get_col_by_name(self.db.capif_configuration) + config = config_col.find_one({}, {"_id": 0}) + + payload = json.dumps({ + "caRoot": server_ca.decode("utf-8"), + "ccfId": config['ccf_id'], + "dstProvDom": os.getenv("CAPIF_HOSTNAME"), + "publicKey": server_pub.decode("utf-8") + }) + headers = { + 'accept': 'application/json', + 'Content-Type': 'application/json' + } + + response = requests.request("POST", url, headers=headers, data=payload, + cert=('server_certs/server.crt', 'server_certs/server.key'), + verify='server_certs/ca.crt') + + inter_ccf = response.json() + current_app.logger.debug(inter_ccf) + + if response.status_code == 201: + interconnected_col.insert_one(inter_ccf) + + ccfinstancedetails_new = CcfInstanceDetails().from_dict(dict_to_camel_case(inter_ccf)) + res = make_response(object=ccfinstancedetails_new, status=response.status_code) + return res diff --git a/services/helper/helper_service/services/interconnection/core/ccfinstancedetails.py b/services/helper/helper_service/services/interconnection/core/ccfinstancedetails.py new file mode 100644 index 0000000000000000000000000000000000000000..6c2987dcc2ce546b59d5b699448763823e738111 --- /dev/null +++ b/services/helper/helper_service/services/interconnection/core/ccfinstancedetails.py @@ -0,0 +1,71 @@ +import os +import secrets +from datetime import datetime + +from flask import current_app +# from pymongo import ReturnDocument + +from db.db import get_mongo +from ..models.ccf_instance_details import CcfInstanceDetails +from ..util import clean_empty, clean_n_camel_case, dict_to_camel_case, serialize_clean +# from ..vendor_specific import add_vend_spec_fields +from .auth_manager import AuthManager +from .publisher import Publisher +from .redis_event import RedisEvent +from .resources import Resource +from .responses import ( + bad_request_error, + forbidden_error, + internal_server_error, + make_response, + not_found_error, + unauthorized_error +) + +publisher_ops = Publisher() + + +service_api_not_found_message = "Service API not found" + + +class CcfInstanceOperations(Resource): + + def __init__(self): + Resource.__init__(self) + self.auth_manager = AuthManager() + self.db = get_mongo() + + def add_ccfinstance(self, ccfinstancedetails): + current_app.logger.debug("Interconnection: Add instance") + current_app.logger.debug(ccfinstancedetails) + + interconnected_col = self.db.get_col_by_name(self.db.interconnected) + interconnected_ccf = interconnected_col.find_one({"ccf_id": ccfinstancedetails.ccf_id}) + if interconnected_ccf: + current_app.logger.debug("CCF already interconnected : {}".format(ccfinstancedetails.ccf_id)) + return make_response("CCF already interconnected", 409) + + with open('server_certs/ca.crt', 'rb') as ca_cert: + server_ca = ca_cert.read() + ca_cert.close() + + with open('server_certs/server.crt', 'rb') as server_cert: + server_pub = server_cert.read() + server_cert.close() + + config_col = self.db.get_col_by_name(self.db.capif_configuration) + config = config_col.find_one({}, {"_id": 0}) + + ccfinstancedetails_dict = ccfinstancedetails.to_dict() + interconnected_col.insert_one(ccfinstancedetails_dict) + + ccfinstancedetails_dict['ca_root'] = server_ca.decode("utf-8") + ccfinstancedetails_dict['public_key'] = server_pub.decode("utf-8") + ccfinstancedetails_dict['ccf_id'] = config['ccf_id'] + ccfinstancedetails_dict['dst_prov_dom'] = os.getenv("CAPIF_HOSTNAME") + current_app.logger.debug(ccfinstancedetails_dict) + + ccfinstancedetails_new = CcfInstanceDetails().from_dict(dict_to_camel_case(ccfinstancedetails_dict)) + current_app.logger.debug(ccfinstancedetails_new) + res = make_response(object=serialize_clean(ccfinstancedetails_new), status=201) + return res \ No newline at end of file diff --git a/services/helper/helper_service/services/interconnection/core/consumer_messager.py b/services/helper/helper_service/services/interconnection/core/consumer_messager.py new file mode 100644 index 0000000000000000000000000000000000000000..3814c71baae22c5e3c1d4ecbec43f4ae0cd75704 --- /dev/null +++ b/services/helper/helper_service/services/interconnection/core/consumer_messager.py @@ -0,0 +1,30 @@ +# subscriber.py +import json + +import redis +from flask import current_app + +from .internal_service_ops import InternalServiceOps + + +class Subscriber(): + + def __init__(self): + self.r = redis.Redis(host='redis', port=6379, db=0) + self.security_ops = InternalServiceOps() + self.p = self.r.pubsub() + self.p.subscribe("internal-messages") + + def listen(self): + current_app.logger.info("Listening publish messages") + for raw_message in self.p.listen(): + if raw_message["type"] == "message" and raw_message["channel"].decode('utf-8') == "internal-messages": + current_app.logger.info("New internal event received") + internal_redis_event = json.loads( + raw_message["data"].decode('utf-8')) + if internal_redis_event.get('event') == "PROVIDER-REMOVED": + apf_ids = internal_redis_event.get( + 'information', {"apf_ids": []}).get('apf_ids') + if len(apf_ids) > 0: + self.security_ops.delete_intern_service(apf_ids) + diff --git a/services/helper/helper_service/services/interconnection/core/internal_service_ops.py b/services/helper/helper_service/services/interconnection/core/internal_service_ops.py new file mode 100644 index 0000000000000000000000000000000000000000..fa7bc67ded52be54b4222c53b05e151d4062270a --- /dev/null +++ b/services/helper/helper_service/services/interconnection/core/internal_service_ops.py @@ -0,0 +1,25 @@ + +from flask import current_app + +from .auth_manager import AuthManager +from .resources import Resource + + +class InternalServiceOps(Resource): + + def __init__(self): + Resource.__init__(self) + self.auth_manager = AuthManager() + + def delete_intern_service(self, apf_ids): + + current_app.logger.info("Provider removed, removing services published by APF") + mycol = self.db.get_col_by_name(self.db.service_api_descriptions) + for apf_id in apf_ids: + my_query = {'apf_id': apf_id} + mycol.delete_many(my_query) + + #We dont need remove all auth events, because when provider is removed, remove auth entry + #self.auth_manager.remove_auth_all_service(apf_id) + + current_app.logger.info("Removed service") diff --git a/services/helper/helper_service/services/interconnection/core/publisher.py b/services/helper/helper_service/services/interconnection/core/publisher.py new file mode 100644 index 0000000000000000000000000000000000000000..db3049f8866f51b67cd69f100b4df3c754925a72 --- /dev/null +++ b/services/helper/helper_service/services/interconnection/core/publisher.py @@ -0,0 +1,12 @@ +# import redis + + +class Publisher(): + + def __init__(self): + # self. r = redis.Redis(host='redis', port=6379, db=0) + self. r = None + + def publish_message(self, channel, message): + # self.r.publish(channel, message) + self.r = None diff --git a/services/helper/helper_service/services/interconnection/core/redis_event.py b/services/helper/helper_service/services/interconnection/core/redis_event.py new file mode 100644 index 0000000000000000000000000000000000000000..3037ae76a7bff9e74674d6e3b686cfbb24cf0f58 --- /dev/null +++ b/services/helper/helper_service/services/interconnection/core/redis_event.py @@ -0,0 +1,63 @@ +import json + +from ..encoder import JSONEncoder +from .publisher import Publisher + +publisher_ops = Publisher() + + +class RedisEvent(): + def __init__(self, + event, + service_api_descriptions=None, + api_ids=None, + api_invoker_ids=None, + acc_ctrl_pol_list=None, + invocation_logs=None, + api_topo_hide=None) -> None: + self.EVENTS_ENUM = [ + 'SERVICE_API_AVAILABLE', + 'SERVICE_API_UNAVAILABLE', + 'SERVICE_API_UPDATE', + 'API_INVOKER_ONBOARDED', + 'API_INVOKER_OFFBOARDED', + 'SERVICE_API_INVOCATION_SUCCESS', + 'SERVICE_API_INVOCATION_FAILURE', + 'ACCESS_CONTROL_POLICY_UPDATE', + 'ACCESS_CONTROL_POLICY_UNAVAILABLE', + 'API_INVOKER_AUTHORIZATION_REVOKED', + 'API_INVOKER_UPDATED', + 'API_TOPOLOGY_HIDING_CREATED', + 'API_TOPOLOGY_HIDING_REVOKED'] + if event not in self.EVENTS_ENUM: + raise Exception( + "Event (" + event + ") is not on event enum (" + ','.join(self.EVENTS_ENUM) + ")") + self.redis_event = { + "event": event + } + # Add event filter keys to an auxiliary object + event_detail = { + "serviceAPIDescriptions": service_api_descriptions, + "apiIds": api_ids, + "apiInvokerIds": api_invoker_ids, + "accCtrlPolList": acc_ctrl_pol_list, + "invocationLogs": invocation_logs, + "apiTopoHide": api_topo_hide + } + + # Filter keys with not None values + filtered_event_detail = {k: v for k, + v in event_detail.items() if v is not None} + + # If there are valid values then add to redis event. + if filtered_event_detail: + self.redis_event["event_detail"] = filtered_event_detail + + def to_string(self): + return json.dumps(self.redis_event, cls=JSONEncoder) + + def send_event(self): + publisher_ops.publish_message("events", self.to_string()) + + def __call__(self): + return self.redis_event diff --git a/services/helper/helper_service/services/interconnection/core/resources.py b/services/helper/helper_service/services/interconnection/core/resources.py new file mode 100644 index 0000000000000000000000000000000000000000..a204dc67a5448d7a4a9067caaec236e422172553 --- /dev/null +++ b/services/helper/helper_service/services/interconnection/core/resources.py @@ -0,0 +1,10 @@ +from abc import ABC + +# from db.db import MongoDatabse + + +class Resource(ABC): + + def __init__(self): + # self.db = MongoDatabse() + self.db = None diff --git a/services/helper/helper_service/services/interconnection/core/responses.py b/services/helper/helper_service/services/interconnection/core/responses.py new file mode 100644 index 0000000000000000000000000000000000000000..1f0302f64b0ac1791aa594b5b16772343af286b9 --- /dev/null +++ b/services/helper/helper_service/services/interconnection/core/responses.py @@ -0,0 +1,50 @@ +import json + +from flask import Response + +from ..encoder import CustomJSONEncoder +from ..models.problem_details import ProblemDetails +from ..util import serialize_clean_camel_case + +mimetype = "application/json" + + +def make_response(object, status): + res = Response(json.dumps(object, cls=CustomJSONEncoder), status=status, mimetype=mimetype) + + return res + + +def internal_server_error(detail, cause): + prob = ProblemDetails(title="Internal Server Error", status=500, detail=detail, cause=cause) + prob = serialize_clean_camel_case(prob) + + return Response(json.dumps(prob, cls=CustomJSONEncoder), status=500, mimetype=mimetype) + + +def forbidden_error(detail, cause): + prob = ProblemDetails(title="Forbidden", status=403, detail=detail, cause=cause) + prob = serialize_clean_camel_case(prob) + + return Response(json.dumps(prob, cls=CustomJSONEncoder), status=403, mimetype=mimetype) + + +def bad_request_error(detail, cause, invalid_params): + prob = ProblemDetails(title="Bad Request", status=400, detail=detail, cause=cause, invalid_params=invalid_params) + prob = serialize_clean_camel_case(prob) + + return Response(json.dumps(prob, cls=CustomJSONEncoder), status=400, mimetype=cause) + + +def not_found_error(detail, cause): + prob = ProblemDetails(title="Not Found", status=404, detail=detail, cause=cause) + prob = serialize_clean_camel_case(prob) + + return Response(json.dumps(prob, cls=CustomJSONEncoder), status=404, mimetype=mimetype) + + +def unauthorized_error(detail, cause): + prob = ProblemDetails(title="Unauthorized", status=401, detail=detail, cause=cause) + prob = serialize_clean_camel_case(prob) + + return Response(json.dumps(prob, cls=CustomJSONEncoder), status=401, mimetype=mimetype) \ No newline at end of file diff --git a/services/helper/helper_service/services/interconnection/core/validate_user.py b/services/helper/helper_service/services/interconnection/core/validate_user.py new file mode 100644 index 0000000000000000000000000000000000000000..1782fe0425311da838e31af7d96338c28e236bf6 --- /dev/null +++ b/services/helper/helper_service/services/interconnection/core/validate_user.py @@ -0,0 +1,49 @@ +import json + +from flask import Response, current_app + +from ..encoder import CustomJSONEncoder +from ..models.problem_details import ProblemDetails +from ..util import serialize_clean_camel_case +from .resources import Resource +from .responses import internal_server_error + + +class ControlAccess(Resource): + + def validate_user_cert(self, apf_id, cert_signature, service_id=None): + + cert_col = self.db.get_col_by_name(self.db.certs_col) + + try: + my_query = {'id': apf_id} + cert_entry = cert_col.find_one(my_query) + + if cert_entry is not None: + is_user_owner = True + if cert_entry["cert_signature"] != cert_signature: + is_user_owner = False + elif service_id: + if "services" not in cert_entry["resources"]: + is_user_owner = False + elif cert_entry.get("resources") and cert_entry["resources"].get("services"): + if service_id not in cert_entry["resources"].get("services"): + is_user_owner = False + if is_user_owner == False: + current_app.logger.info("STEP3") + prob = ProblemDetails( + title="Unauthorized", + detail="User not authorized", + cause="You are not the owner of this resource") + current_app.logger.info("STEP4") + prob = serialize_clean_camel_case(prob) + current_app.logger.info("STEP5") + return Response( + json.dumps(prob, cls=CustomJSONEncoder), + status=401, + mimetype="application/json") + + except Exception as e: + exception = "An exception occurred in validate apf" + current_app.logger.error(exception + "::" + str(e)) + return internal_server_error(detail=exception, cause=str(e)) diff --git a/services/helper/helper_service/services/interconnection/encoder.py b/services/helper/helper_service/services/interconnection/encoder.py new file mode 100644 index 0000000000000000000000000000000000000000..bb6342784e6cb077bd8b5a2a82ad6ef5efedee15 --- /dev/null +++ b/services/helper/helper_service/services/interconnection/encoder.py @@ -0,0 +1,35 @@ +from connexion.apps.flask_app import FlaskJSONEncoder + +from interconnection.models.base_model import Model + + +class JSONEncoder(FlaskJSONEncoder): + include_nulls = False + + def default(self, o): + if isinstance(o, Model): + dikt = {} + for attr in o.openapi_types: + value = getattr(o, attr) + if value is None and not self.include_nulls: + continue + attr = o.attribute_map[attr] + dikt[attr] = value + return dikt + return FlaskJSONEncoder.default(self, o) + + +class CustomJSONEncoder(JSONEncoder): + include_nulls = False + + def default(self, o): + if isinstance(o, Model): + dikt = {} + for attr in o.openapi_types: + value = getattr(o, attr) + if value is None and not self.include_nulls: + continue + attr = o.attribute_map[attr] + dikt[attr] = value + return dikt + return JSONEncoder.default(self, o) \ No newline at end of file diff --git a/services/helper/helper_service/services/interconnection/models/__init__.py b/services/helper/helper_service/services/interconnection/models/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..2f654f248f5420d0e8c67acb34163ba2a2506fa7 --- /dev/null +++ b/services/helper/helper_service/services/interconnection/models/__init__.py @@ -0,0 +1,6 @@ +# flake8: noqa +# import models into model package +from interconnection.models.capif_domain_details import CapifDomainDetails +from interconnection.models.ccf_instance_details import CcfInstanceDetails +from interconnection.models.invalid_param import InvalidParam +from interconnection.models.problem_details import ProblemDetails diff --git a/services/helper/helper_service/services/interconnection/models/base_model.py b/services/helper/helper_service/services/interconnection/models/base_model.py new file mode 100644 index 0000000000000000000000000000000000000000..b354bac281c2a8d9a85b1052e29e5b6ea943bec0 --- /dev/null +++ b/services/helper/helper_service/services/interconnection/models/base_model.py @@ -0,0 +1,68 @@ +import pprint + +import typing + +from interconnection import util + +T = typing.TypeVar('T') + + +class Model: + # openapiTypes: The key is attribute name and the + # value is attribute type. + openapi_types: typing.Dict[str, type] = {} + + # attributeMap: The key is attribute name and the + # value is json key in definition. + attribute_map: typing.Dict[str, str] = {} + + @classmethod + def from_dict(cls: typing.Type[T], dikt) -> T: + """Returns the dict as a model""" + return util.deserialize_model(dikt, cls) + + def to_dict(self): + """Returns the model properties as a dict + + :rtype: dict + """ + result = {} + + for attr in self.openapi_types: + value = getattr(self, attr) + if isinstance(value, list): + result[attr] = list(map( + lambda x: x.to_dict() if hasattr(x, "to_dict") else x, + value + )) + elif hasattr(value, "to_dict"): + result[attr] = value.to_dict() + elif isinstance(value, dict): + result[attr] = dict(map( + lambda item: (item[0], item[1].to_dict()) + if hasattr(item[1], "to_dict") else item, + value.items() + )) + else: + result[attr] = value + + return result + + def to_str(self): + """Returns the string representation of the model + + :rtype: str + """ + return pprint.pformat(self.to_dict()) + + def __repr__(self): + """For `print` and `pprint`""" + return self.to_str() + + def __eq__(self, other): + """Returns true if both objects are equal""" + return self.__dict__ == other.__dict__ + + def __ne__(self, other): + """Returns true if both objects are not equal""" + return not self == other diff --git a/services/helper/helper_service/services/interconnection/models/capif_domain_details.py b/services/helper/helper_service/services/interconnection/models/capif_domain_details.py new file mode 100644 index 0000000000000000000000000000000000000000..2a2319cda6731dd3e15339682a52daf365e67590 --- /dev/null +++ b/services/helper/helper_service/services/interconnection/models/capif_domain_details.py @@ -0,0 +1,63 @@ +from datetime import date, datetime # noqa: F401 + +from typing import List, Dict # noqa: F401 + +from interconnection.models.base_model import Model +from interconnection import util + + +class CapifDomainDetails(Model): + """NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + + Do not edit the class manually. + """ + + def __init__(self, dst_prov_dom=None): # noqa: E501 + """CapifDomainDetails - a model defined in OpenAPI + + :param dst_prov_dom: The dst_prov_dom of this CapifDomainDetails. # noqa: E501 + :type dst_prov_dom: str + """ + self.openapi_types = { + 'dst_prov_dom': str + } + + self.attribute_map = { + 'dst_prov_dom': 'dstProvDom' + } + + self._dst_prov_dom = dst_prov_dom + + @classmethod + def from_dict(cls, dikt) -> 'CapifDomainDetails': + """Returns the dict as a model + + :param dikt: A dict. + :type: dict + :return: The CapifDomainDetails of this CapifDomainDetails. # noqa: E501 + :rtype: CapifDomainDetails + """ + return util.deserialize_model(dikt, cls) + + @property + def dst_prov_dom(self) -> str: + """Gets the dst_prov_dom of this CapifDomainDetails. + + + :return: The dst_prov_dom of this CapifDomainDetails. + :rtype: str + """ + return self._dst_prov_dom + + @dst_prov_dom.setter + def dst_prov_dom(self, dst_prov_dom: str): + """Sets the dst_prov_dom of this CapifDomainDetails. + + + :param dst_prov_dom: The dst_prov_dom of this CapifDomainDetails. + :type dst_prov_dom: str + """ + if dst_prov_dom is None: + raise ValueError("Invalid value for `dst_prov_dom`, must not be `None`") # noqa: E501 + + self._dst_prov_dom = dst_prov_dom diff --git a/services/helper/helper_service/services/interconnection/models/ccf_instance_details.py b/services/helper/helper_service/services/interconnection/models/ccf_instance_details.py new file mode 100644 index 0000000000000000000000000000000000000000..fa23a751f33ebef650269b3ad6de3aa1b590815a --- /dev/null +++ b/services/helper/helper_service/services/interconnection/models/ccf_instance_details.py @@ -0,0 +1,147 @@ +from datetime import date, datetime # noqa: F401 + +from typing import List, Dict # noqa: F401 + +from interconnection.models.base_model import Model +from interconnection import util + + +class CcfInstanceDetails(Model): + """NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + + Do not edit the class manually. + """ + + def __init__(self, ca_root=None, public_key=None, ccf_id=None, dst_prov_dom=None): # noqa: E501 + """CcfInstanceDetails - a model defined in OpenAPI + + :param ca_root: The ca_root of this CcfInstanceDetails. # noqa: E501 + :type ca_root: str + :param public_key: The public_key of this CcfInstanceDetails. # noqa: E501 + :type public_key: str + :param ccf_id: The ccf_id of this CcfInstanceDetails. # noqa: E501 + :type ccf_id: str + :param dst_prov_dom: The dst_prov_dom of this CcfInstanceDetails. # noqa: E501 + :type dst_prov_dom: str + """ + self.openapi_types = { + 'ca_root': str, + 'public_key': str, + 'ccf_id': str, + 'dst_prov_dom': str + } + + self.attribute_map = { + 'ca_root': 'caRoot', + 'public_key': 'publicKey', + 'ccf_id': 'ccfId', + 'dst_prov_dom': 'dstProvDom' + } + + self._ca_root = ca_root + self._public_key = public_key + self._ccf_id = ccf_id + self._dst_prov_dom = dst_prov_dom + + @classmethod + def from_dict(cls, dikt) -> 'CcfInstanceDetails': + """Returns the dict as a model + + :param dikt: A dict. + :type: dict + :return: The CcfInstanceDetails of this CcfInstanceDetails. # noqa: E501 + :rtype: CcfInstanceDetails + """ + return util.deserialize_model(dikt, cls) + + @property + def ca_root(self) -> str: + """Gets the ca_root of this CcfInstanceDetails. + + + :return: The ca_root of this CcfInstanceDetails. + :rtype: str + """ + return self._ca_root + + @ca_root.setter + def ca_root(self, ca_root: str): + """Sets the ca_root of this CcfInstanceDetails. + + + :param ca_root: The ca_root of this CcfInstanceDetails. + :type ca_root: str + """ + if ca_root is None: + raise ValueError("Invalid value for `ca_root`, must not be `None`") # noqa: E501 + + self._ca_root = ca_root + + @property + def public_key(self) -> str: + """Gets the public_key of this CcfInstanceDetails. + + + :return: The public_key of this CcfInstanceDetails. + :rtype: str + """ + return self._public_key + + @public_key.setter + def public_key(self, public_key: str): + """Sets the public_key of this CcfInstanceDetails. + + + :param public_key: The public_key of this CcfInstanceDetails. + :type public_key: str + """ + if public_key is None: + raise ValueError("Invalid value for `public_key`, must not be `None`") # noqa: E501 + + self._public_key = public_key + + @property + def ccf_id(self) -> str: + """Gets the ccf_id of this CcfInstanceDetails. + + + :return: The ccf_id of this CcfInstanceDetails. + :rtype: str + """ + return self._ccf_id + + @ccf_id.setter + def ccf_id(self, ccf_id: str): + """Sets the ccf_id of this CcfInstanceDetails. + + + :param ccf_id: The ccf_id of this CcfInstanceDetails. + :type ccf_id: str + """ + if ccf_id is None: + raise ValueError("Invalid value for `ccf_id`, must not be `None`") # noqa: E501 + + self._ccf_id = ccf_id + + @property + def dst_prov_dom(self) -> str: + """Gets the dst_prov_dom of this CcfInstanceDetails. + + + :return: The dst_prov_dom of this CcfInstanceDetails. + :rtype: str + """ + return self._dst_prov_dom + + @dst_prov_dom.setter + def dst_prov_dom(self, dst_prov_dom: str): + """Sets the dst_prov_dom of this CcfInstanceDetails. + + + :param dst_prov_dom: The dst_prov_dom of this CcfInstanceDetails. + :type dst_prov_dom: str + """ + if dst_prov_dom is None: + raise ValueError("Invalid value for `dst_prov_dom`, must not be `None`") # noqa: E501 + + self._dst_prov_dom = dst_prov_dom diff --git a/services/helper/helper_service/services/interconnection/models/invalid_param.py b/services/helper/helper_service/services/interconnection/models/invalid_param.py new file mode 100644 index 0000000000000000000000000000000000000000..3776af09c3bd073be10e662be7360b628dd142ee --- /dev/null +++ b/services/helper/helper_service/services/interconnection/models/invalid_param.py @@ -0,0 +1,93 @@ +from datetime import date, datetime # noqa: F401 + +from typing import List, Dict # noqa: F401 + +from interconnection.models.base_model import Model +from interconnection import util + + +class InvalidParam(Model): + """NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + + Do not edit the class manually. + """ + + def __init__(self, param=None, reason=None): # noqa: E501 + """InvalidParam - a model defined in OpenAPI + + :param param: The param of this InvalidParam. # noqa: E501 + :type param: str + :param reason: The reason of this InvalidParam. # noqa: E501 + :type reason: str + """ + self.openapi_types = { + 'param': str, + 'reason': str + } + + self.attribute_map = { + 'param': 'param', + 'reason': 'reason' + } + + self._param = param + self._reason = reason + + @classmethod + def from_dict(cls, dikt) -> 'InvalidParam': + """Returns the dict as a model + + :param dikt: A dict. + :type: dict + :return: The InvalidParam of this InvalidParam. # noqa: E501 + :rtype: InvalidParam + """ + return util.deserialize_model(dikt, cls) + + @property + def param(self) -> str: + """Gets the param of this InvalidParam. + + Attribute's name encoded as a JSON Pointer, or header's name. # noqa: E501 + + :return: The param of this InvalidParam. + :rtype: str + """ + return self._param + + @param.setter + def param(self, param: str): + """Sets the param of this InvalidParam. + + Attribute's name encoded as a JSON Pointer, or header's name. # noqa: E501 + + :param param: The param of this InvalidParam. + :type param: str + """ + if param is None: + raise ValueError("Invalid value for `param`, must not be `None`") # noqa: E501 + + self._param = param + + @property + def reason(self) -> str: + """Gets the reason of this InvalidParam. + + A human-readable reason, e.g. \"must be a positive integer\". # noqa: E501 + + :return: The reason of this InvalidParam. + :rtype: str + """ + return self._reason + + @reason.setter + def reason(self, reason: str): + """Sets the reason of this InvalidParam. + + A human-readable reason, e.g. \"must be a positive integer\". # noqa: E501 + + :param reason: The reason of this InvalidParam. + :type reason: str + """ + + self._reason = reason diff --git a/services/helper/helper_service/services/interconnection/models/problem_details.py b/services/helper/helper_service/services/interconnection/models/problem_details.py new file mode 100644 index 0000000000000000000000000000000000000000..c19f20227c0d220cdeea713220e4c7bb81cb5c75 --- /dev/null +++ b/services/helper/helper_service/services/interconnection/models/problem_details.py @@ -0,0 +1,267 @@ +from datetime import date, datetime # noqa: F401 + +from typing import List, Dict # noqa: F401 + +from interconnection.models.base_model import Model +from interconnection.models.invalid_param import InvalidParam +import re +from interconnection import util + +from interconnection.models.invalid_param import InvalidParam # noqa: E501 +import re # noqa: E501 + +class ProblemDetails(Model): + """NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + + Do not edit the class manually. + """ + + def __init__(self, type=None, title=None, status=None, detail=None, instance=None, cause=None, invalid_params=None, supported_features=None): # noqa: E501 + """ProblemDetails - a model defined in OpenAPI + + :param type: The type of this ProblemDetails. # noqa: E501 + :type type: str + :param title: The title of this ProblemDetails. # noqa: E501 + :type title: str + :param status: The status of this ProblemDetails. # noqa: E501 + :type status: int + :param detail: The detail of this ProblemDetails. # noqa: E501 + :type detail: str + :param instance: The instance of this ProblemDetails. # noqa: E501 + :type instance: str + :param cause: The cause of this ProblemDetails. # noqa: E501 + :type cause: str + :param invalid_params: The invalid_params of this ProblemDetails. # noqa: E501 + :type invalid_params: List[InvalidParam] + :param supported_features: The supported_features of this ProblemDetails. # noqa: E501 + :type supported_features: str + """ + self.openapi_types = { + 'type': str, + 'title': str, + 'status': int, + 'detail': str, + 'instance': str, + 'cause': str, + 'invalid_params': List[InvalidParam], + 'supported_features': str + } + + self.attribute_map = { + 'type': 'type', + 'title': 'title', + 'status': 'status', + 'detail': 'detail', + 'instance': 'instance', + 'cause': 'cause', + 'invalid_params': 'invalidParams', + 'supported_features': 'supportedFeatures' + } + + self._type = type + self._title = title + self._status = status + self._detail = detail + self._instance = instance + self._cause = cause + self._invalid_params = invalid_params + self._supported_features = supported_features + + @classmethod + def from_dict(cls, dikt) -> 'ProblemDetails': + """Returns the dict as a model + + :param dikt: A dict. + :type: dict + :return: The ProblemDetails of this ProblemDetails. # noqa: E501 + :rtype: ProblemDetails + """ + return util.deserialize_model(dikt, cls) + + @property + def type(self) -> str: + """Gets the type of this ProblemDetails. + + string providing an URI formatted according to IETF RFC 3986. # noqa: E501 + + :return: The type of this ProblemDetails. + :rtype: str + """ + return self._type + + @type.setter + def type(self, type: str): + """Sets the type of this ProblemDetails. + + string providing an URI formatted according to IETF RFC 3986. # noqa: E501 + + :param type: The type of this ProblemDetails. + :type type: str + """ + + self._type = type + + @property + def title(self) -> str: + """Gets the title of this ProblemDetails. + + A short, human-readable summary of the problem type. It should not change from occurrence to occurrence of the problem. # noqa: E501 + + :return: The title of this ProblemDetails. + :rtype: str + """ + return self._title + + @title.setter + def title(self, title: str): + """Sets the title of this ProblemDetails. + + A short, human-readable summary of the problem type. It should not change from occurrence to occurrence of the problem. # noqa: E501 + + :param title: The title of this ProblemDetails. + :type title: str + """ + + self._title = title + + @property + def status(self) -> int: + """Gets the status of this ProblemDetails. + + The HTTP status code for this occurrence of the problem. # noqa: E501 + + :return: The status of this ProblemDetails. + :rtype: int + """ + return self._status + + @status.setter + def status(self, status: int): + """Sets the status of this ProblemDetails. + + The HTTP status code for this occurrence of the problem. # noqa: E501 + + :param status: The status of this ProblemDetails. + :type status: int + """ + + self._status = status + + @property + def detail(self) -> str: + """Gets the detail of this ProblemDetails. + + A human-readable explanation specific to this occurrence of the problem. # noqa: E501 + + :return: The detail of this ProblemDetails. + :rtype: str + """ + return self._detail + + @detail.setter + def detail(self, detail: str): + """Sets the detail of this ProblemDetails. + + A human-readable explanation specific to this occurrence of the problem. # noqa: E501 + + :param detail: The detail of this ProblemDetails. + :type detail: str + """ + + self._detail = detail + + @property + def instance(self) -> str: + """Gets the instance of this ProblemDetails. + + string providing an URI formatted according to IETF RFC 3986. # noqa: E501 + + :return: The instance of this ProblemDetails. + :rtype: str + """ + return self._instance + + @instance.setter + def instance(self, instance: str): + """Sets the instance of this ProblemDetails. + + string providing an URI formatted according to IETF RFC 3986. # noqa: E501 + + :param instance: The instance of this ProblemDetails. + :type instance: str + """ + + self._instance = instance + + @property + def cause(self) -> str: + """Gets the cause of this ProblemDetails. + + A machine-readable application error cause specific to this occurrence of the problem. This IE should be present and provide application-related error information, if available. # noqa: E501 + + :return: The cause of this ProblemDetails. + :rtype: str + """ + return self._cause + + @cause.setter + def cause(self, cause: str): + """Sets the cause of this ProblemDetails. + + A machine-readable application error cause specific to this occurrence of the problem. This IE should be present and provide application-related error information, if available. # noqa: E501 + + :param cause: The cause of this ProblemDetails. + :type cause: str + """ + + self._cause = cause + + @property + def invalid_params(self) -> List[InvalidParam]: + """Gets the invalid_params of this ProblemDetails. + + Description of invalid parameters, for a request rejected due to invalid parameters. # noqa: E501 + + :return: The invalid_params of this ProblemDetails. + :rtype: List[InvalidParam] + """ + return self._invalid_params + + @invalid_params.setter + def invalid_params(self, invalid_params: List[InvalidParam]): + """Sets the invalid_params of this ProblemDetails. + + Description of invalid parameters, for a request rejected due to invalid parameters. # noqa: E501 + + :param invalid_params: The invalid_params of this ProblemDetails. + :type invalid_params: List[InvalidParam] + """ + if invalid_params is not None and len(invalid_params) < 1: + raise ValueError("Invalid value for `invalid_params`, number of items must be greater than or equal to `1`") # noqa: E501 + + self._invalid_params = invalid_params + + @property + def supported_features(self) -> str: + """Gets the supported_features of this ProblemDetails. + + A string used to indicate the features supported by an API that is used as defined in clause 6.6 in 3GPP TS 29.500. The string shall contain a bitmask indicating supported features in hexadecimal representation Each character in the string shall take a value of \"0\" to \"9\", \"a\" to \"f\" or \"A\" to \"F\" and shall represent the support of 4 features as described in table 5.2.2-3. The most significant character representing the highest-numbered features shall appear first in the string, and the character representing features 1 to 4 shall appear last in the string. The list of features and their numbering (starting with 1) are defined separately for each API. If the string contains a lower number of characters than there are defined features for an API, all features that would be represented by characters that are not present in the string are not supported. # noqa: E501 + + :return: The supported_features of this ProblemDetails. + :rtype: str + """ + return self._supported_features + + @supported_features.setter + def supported_features(self, supported_features: str): + """Sets the supported_features of this ProblemDetails. + + A string used to indicate the features supported by an API that is used as defined in clause 6.6 in 3GPP TS 29.500. The string shall contain a bitmask indicating supported features in hexadecimal representation Each character in the string shall take a value of \"0\" to \"9\", \"a\" to \"f\" or \"A\" to \"F\" and shall represent the support of 4 features as described in table 5.2.2-3. The most significant character representing the highest-numbered features shall appear first in the string, and the character representing features 1 to 4 shall appear last in the string. The list of features and their numbering (starting with 1) are defined separately for each API. If the string contains a lower number of characters than there are defined features for an API, all features that would be represented by characters that are not present in the string are not supported. # noqa: E501 + + :param supported_features: The supported_features of this ProblemDetails. + :type supported_features: str + """ + if supported_features is not None and not re.search(r'^[A-Fa-f0-9]*$', supported_features): # noqa: E501 + raise ValueError(r"Invalid value for `supported_features`, must be a follow pattern or equal to `/^[A-Fa-f0-9]*$/`") # noqa: E501 + + self._supported_features = supported_features diff --git a/services/helper/helper_service/services/interconnection/openapi/openapi.yaml b/services/helper/helper_service/services/interconnection/openapi/openapi.yaml new file mode 100644 index 0000000000000000000000000000000000000000..41dbf42c35d07e96dae8e9a1d469b93f1afa72b9 --- /dev/null +++ b/services/helper/helper_service/services/interconnection/openapi/openapi.yaml @@ -0,0 +1,226 @@ +openapi: 3.0.3 +info: + description: APIs for interconnnecting CCFs + title: CCF Interconnection API + version: 1.0.0 +servers: +- url: "{apiRoot}/interconnection" + variables: + apiRoot: + default: http://localhost:8080 + description: Base URL of the Helper service. +paths: + /connect: + post: + operationId: connect_ccfs + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/CcfInstanceDetails" + required: true + responses: + "201": + content: + application/json: + schema: + $ref: "#/components/schemas/CcfInstanceDetails" + description: Interconnection succeeded + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/ProblemDetails" + description: Bad request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/ProblemDetails" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/ProblemDetails" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/ProblemDetails" + description: Not Found + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/ProblemDetails" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/ProblemDetails" + description: Service Unavailable + default: + description: Generic Error + summary: Create a new interconnection + x-openapi-router-controller: interconnection.controllers.default_controller + /interconnect: + post: + operationId: interconnect_request + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/CapifDomainDetails" + required: true + responses: + "201": + content: + application/json: + schema: + $ref: "#/components/schemas/CapifDomainDetails" + description: Interconnection request succeeded + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/ProblemDetails" + description: Bad request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/ProblemDetails" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/ProblemDetails" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/ProblemDetails" + description: Not Found + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/ProblemDetails" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/ProblemDetails" + description: Service Unavailable + default: + description: Generic Error + summary: Send a new interconnection request + x-openapi-router-controller: interconnection.controllers.default_controller +components: + schemas: + CapifDomainDetails: + example: + dstProvDom: dstProvDom + properties: + dstProvDom: + title: dstProvDom + type: string + required: + - dstProvDom + title: CapifDomainDetails + type: object + CcfInstanceDetails: + example: + ccfId: ccfId + dstProvDom: dstProvDom + publicKey: publicKey + caRoot: caRoot + properties: + caRoot: + title: caRoot + type: string + publicKey: + title: publicKey + type: string + ccfId: + title: ccfId + type: string + dstProvDom: + title: dstProvDom + type: string + required: + - caRoot + - ccfId + - dstProvDom + - publicKey + title: CcfInstanceDetails + type: object + ProblemDetails: + description: Represents additional information and details on an error response. + properties: + type: + description: string providing an URI formatted according to IETF RFC 3986. + title: type + type: string + title: + description: "A short, human-readable summary of the problem type. It should\ + \ not change from occurrence to occurrence of the problem. \n" + title: title + type: string + status: + description: The HTTP status code for this occurrence of the problem. + title: status + type: integer + detail: + description: A human-readable explanation specific to this occurrence of + the problem. + title: detail + type: string + instance: + description: string providing an URI formatted according to IETF RFC 3986. + title: type + type: string + cause: + description: | + A machine-readable application error cause specific to this occurrence of the problem. This IE should be present and provide application-related error information, if available. + title: cause + type: string + invalidParams: + description: | + Description of invalid parameters, for a request rejected due to invalid parameters. + items: + $ref: "#/components/schemas/InvalidParam" + minItems: 1 + title: invalidParams + type: array + supportedFeatures: + description: | + A string used to indicate the features supported by an API that is used as defined in clause 6.6 in 3GPP TS 29.500. The string shall contain a bitmask indicating supported features in hexadecimal representation Each character in the string shall take a value of "0" to "9", "a" to "f" or "A" to "F" and shall represent the support of 4 features as described in table 5.2.2-3. The most significant character representing the highest-numbered features shall appear first in the string, and the character representing features 1 to 4 shall appear last in the string. The list of features and their numbering (starting with 1) are defined separately for each API. If the string contains a lower number of characters than there are defined features for an API, all features that would be represented by characters that are not present in the string are not supported. + pattern: "^[A-Fa-f0-9]*$" + title: supportedFeatures + type: string + title: ProblemDetails + type: object + InvalidParam: + description: | + Represents the description of invalid parameters, for a request rejected due to invalid parameters. + properties: + param: + description: "Attribute's name encoded as a JSON Pointer, or header's name." + title: param + type: string + reason: + description: "A human-readable reason, e.g. \"must be a positive integer\"\ + ." + title: reason + type: string + required: + - param + title: InvalidParam + type: object diff --git a/services/helper/helper_service/services/interconnection/typing_utils.py b/services/helper/helper_service/services/interconnection/typing_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..74e3c913a7db6246bc765f147ca872996112c6bb --- /dev/null +++ b/services/helper/helper_service/services/interconnection/typing_utils.py @@ -0,0 +1,30 @@ +import sys + +if sys.version_info < (3, 7): + import typing + + def is_generic(klass): + """ Determine whether klass is a generic class """ + return type(klass) == typing.GenericMeta + + def is_dict(klass): + """ Determine whether klass is a Dict """ + return klass.__extra__ == dict + + def is_list(klass): + """ Determine whether klass is a List """ + return klass.__extra__ == list + +else: + + def is_generic(klass): + """ Determine whether klass is a generic class """ + return hasattr(klass, '__origin__') + + def is_dict(klass): + """ Determine whether klass is a Dict """ + return klass.__origin__ == dict + + def is_list(klass): + """ Determine whether klass is a List """ + return klass.__origin__ == list diff --git a/services/helper/helper_service/services/interconnection/util.py b/services/helper/helper_service/services/interconnection/util.py new file mode 100644 index 0000000000000000000000000000000000000000..005fd9c804999462796fd920dc28a4e7f360381f --- /dev/null +++ b/services/helper/helper_service/services/interconnection/util.py @@ -0,0 +1,212 @@ +import datetime + +import typing +from interconnection import typing_utils + + +def serialize_clean_camel_case(obj): + res = obj.to_dict() + res = clean_empty(res) + res = dict_to_camel_case(res) + + return res + + +def serialize_clean(obj): + res = obj.to_dict() + res = clean_empty(res) + + return res + + +def clean_n_camel_case(res): + res = clean_empty(res) + res = dict_to_camel_case(res) + + return res + + +def clean_empty(d): + if isinstance(d, dict): + return { + k: v + for k, v in ((k, clean_empty(v)) for k, v in d.items()) + if v is not None or (isinstance(v, list) and len(v) == 0) + } + if isinstance(d, list): + return [v for v in map(clean_empty, d) if v is not None] + return d + + +def dict_to_camel_case(my_dict): + + + result = {} + + for attr, value in my_dict.items(): + if len(attr.split('_')) != 1: + my_key = ''.join(word.title() for word in attr.split('_')) + my_key= ''.join([my_key[0].lower(), my_key[1:]]) + else: + my_key = attr + + if my_key == "serviceApiCategory": + my_key = "serviceAPICategory" + + if isinstance(value, list): + result[my_key] = list(map( + lambda x: dict_to_camel_case(x) if isinstance(x, dict) else x, value )) + + elif hasattr(value, "to_dict"): + result[my_key] = dict_to_camel_case(value) + + elif isinstance(value, dict): + value = dict_to_camel_case(value) + result[my_key] = value + else: + result[my_key] = value + + return result + + +def _deserialize(data, klass): + """Deserializes dict, list, str into an object. + + :param data: dict, list or str. + :param klass: class literal, or string of class name. + + :return: object. + """ + if data is None: + return None + + if klass in (int, float, str, bool, bytearray): + return _deserialize_primitive(data, klass) + elif klass == object: + return _deserialize_object(data) + elif klass == datetime.date: + return deserialize_date(data) + elif klass == datetime.datetime: + return deserialize_datetime(data) + elif typing_utils.is_generic(klass): + if typing_utils.is_list(klass): + return _deserialize_list(data, klass.__args__[0]) + if typing_utils.is_dict(klass): + return _deserialize_dict(data, klass.__args__[1]) + else: + return deserialize_model(data, klass) + + +def _deserialize_primitive(data, klass): + """Deserializes to primitive type. + + :param data: data to deserialize. + :param klass: class literal. + + :return: int, long, float, str, bool. + :rtype: int | long | float | str | bool + """ + try: + value = klass(data) + except UnicodeEncodeError: + value = data + except TypeError: + value = data + return value + + +def _deserialize_object(value): + """Return an original value. + + :return: object. + """ + return value + + +def deserialize_date(string): + """Deserializes string to date. + + :param string: str. + :type string: str + :return: date. + :rtype: date + """ + if string is None: + return None + + try: + from dateutil.parser import parse + return parse(string).date() + except ImportError: + return string + + +def deserialize_datetime(string): + """Deserializes string to datetime. + + The string should be in iso8601 datetime format. + + :param string: str. + :type string: str + :return: datetime. + :rtype: datetime + """ + if string is None: + return None + + try: + from dateutil.parser import parse + return parse(string) + except ImportError: + return string + + +def deserialize_model(data, klass): + """Deserializes list or dict to model. + + :param data: dict, list. + :type data: dict | list + :param klass: class literal. + :return: model object. + """ + instance = klass() + + if not instance.openapi_types: + return data + + for attr, attr_type in instance.openapi_types.items(): + if data is not None \ + and instance.attribute_map[attr] in data \ + and isinstance(data, (list, dict)): + value = data[instance.attribute_map[attr]] + setattr(instance, attr, _deserialize(value, attr_type)) + + return instance + + +def _deserialize_list(data, boxed_type): + """Deserializes a list and its elements. + + :param data: list to deserialize. + :type data: list + :param boxed_type: class literal. + + :return: deserialized list. + :rtype: list + """ + return [_deserialize(sub_data, boxed_type) + for sub_data in data] + + +def _deserialize_dict(data, boxed_type): + """Deserializes a dict and its elements. + + :param data: dict to deserialize. + :type data: dict + :param boxed_type: class literal. + + :return: deserialized dict. + :rtype: dict + """ + return {k: _deserialize(v, boxed_type) + for k, v in data.items() } diff --git a/services/nginx/nginx_prepare.sh b/services/nginx/nginx_prepare.sh index 91884863cc069fc05e1ad71e018627143ed5aa88..e38770b901e4938597220e0761e43f3073660e30 100644 --- a/services/nginx/nginx_prepare.sh +++ b/services/nginx/nginx_prepare.sh @@ -44,69 +44,114 @@ if [ "$SUCCES_OPERATION" = false ]; then exit 1 # Exit with failure fi -# Setup inital value to ATTEMPT and SUCCESS_OPERATION -ATTEMPT=0 -SUCCES_OPERATION=false - -while [ $ATTEMPT -lt $MAX_RETRIES ]; do - # Increment ATTEMPT using eval - eval "ATTEMPT=\$((ATTEMPT + 1))" - echo "Attempt $ATTEMPT of $MAX_RETRIES" - - # Make the request to Vault and store the response in a variable - RESPONSE=$(curl -s -k --connect-timeout 5 --max-time 10 \ - --header "X-Vault-Token: $VAULT_TOKEN" \ - --request GET "$VAULT_ADDR/v1/secret/data/server_cert" | jq -r '.data.data.cert') - - echo "$RESPONSE" - # Check if the response is "null" or empty - if [ -n "$RESPONSE" ] && [ "$RESPONSE" != "null" ]; then - echo "$RESPONSE" > $CERTS_FOLDER/server.crt - echo "Server Certificate successfully saved." - ATTEMPT=0 - SUCCES_OPERATION=true - break - else - echo "Invalid response ('null' or empty), retrying in $RETRY_DELAY seconds..." - sleep $RETRY_DELAY - fi -done - -if [ "$SUCCES_OPERATION" = false ]; then - echo "Error: Failed to retrieve a valid response after $MAX_RETRIES attempts." - exit 1 # Exit with failure +if [ "$VAULT_HOSTNAME" != "vault" ] ; then + # Setup variables (Replace these or ensure they are in your environment) + TTL="8760h" + + echo "--- Generating Private Key and CSR ---" + # Generate Private Key + openssl genrsa -out "$CERTS_FOLDER/server.key" 2048 + + # Generate CSR + # Note: We pass the Subject information here. + # OpenSSL's -subj format: /C=/ST=/L=/O=/OU=/CN=/emailAddress= + SUBJ="/C=ES/ST=Madrid/L=Madrid/O=OCF helper/OU=helper/CN=${CAPIF_HOSTNAME}/emailAddress=helper@tid.es" + + openssl req -new \ + -key "$CERTS_FOLDER/server.key" \ + -out "$CERTS_FOLDER/server.csr" \ + -subj "$SUBJ" + + echo "--- Requesting Certificate from Vault ---" + + # Prepare the CSR (Safe for JSON) + ESC_CSR=$(awk '{printf "%s\\n", $0}' "$CERTS_FOLDER/server.csr") + + # Request and Extract Certificate in one pipeline + # This prevents the "parse error" by passing the raw stream to jq + curl -s --request POST \ + --header "X-Vault-Token: $VAULT_TOKEN" \ + --header "Content-Type: application/json" \ + --data "{ + \"format\": \"pem_bundle\", + \"ttl\": \"$TTL\", + \"csr\": \"$ESC_CSR\", + \"common_name\": \"superadmin${CAPIF_HOSTNAME}\", + \"alt_names\": \"${CAPIF_HOSTNAME}\" + }" \ + "$VAULT_ADDR/v1/pki_int/sign/my-ca" | jq -r '.data.certificate' > "$CERTS_FOLDER/server.crt" + + # Validation Check + if [ ! -s "$CERTS_FOLDER/server.crt" ] || [ "$(cat $CERTS_FOLDER/server.crt)" == "null" ]; then + echo "ERROR: Failed to generate server.crt. Check Vault Token and Connectivity." + exit 1 + fi +else + # Setup initial value to ATTEMPT and SUCCESS_OPERATION + ATTEMPT=0 + SUCCES_OPERATION=false + + while [ $ATTEMPT -lt $MAX_RETRIES ]; do + # Increment ATTEMPT using eval + eval "ATTEMPT=\$((ATTEMPT + 1))" + echo "Attempt $ATTEMPT of $MAX_RETRIES" + + # Make the request to Vault and store the response in a variable + RESPONSE=$(curl -s -k --connect-timeout 5 --max-time 10 \ + --header "X-Vault-Token: $VAULT_TOKEN" \ + --request GET "$VAULT_ADDR/v1/secret/data/server_cert" | jq -r '.data.data.cert') + + echo "$RESPONSE" + + # Check if the response is "null" or empty + if [ -n "$RESPONSE" ] && [ "$RESPONSE" != "null" ]; then + echo "$RESPONSE" > $CERTS_FOLDER/server.crt + echo "Server Certificate successfully saved." + ATTEMPT=0 + SUCCES_OPERATION=true + break + else + echo "Invalid response ('null' or empty), retrying in $RETRY_DELAY seconds..." + sleep $RETRY_DELAY + fi + done + + if [ "$SUCCES_OPERATION" = false ]; then + echo "Error: Failed to retrieve a valid response after $MAX_RETRIES attempts." + exit 1 # Exit with failure + fi + + # Setup inital value to ATTEMPT and SUCCESS_OPERATION + ATTEMPT=0 + SUCCES_OPERATION=false + + while [ $ATTEMPT -lt $MAX_RETRIES ]; do + # Increment ATTEMPT using eval + eval "ATTEMPT=\$((ATTEMPT + 1))" + echo "Attempt $ATTEMPT of $MAX_RETRIES" + + # Make the request to Vault and store the response in a variable + RESPONSE=$(curl -s -k --connect-timeout 5 --max-time 10 \ + --header "X-Vault-Token: $VAULT_TOKEN" \ + --request GET "$VAULT_ADDR/v1/secret/data/server_cert/private" | jq -r '.data.data.key') + + echo "$RESPONSE" + + # Check if the response is "null" or empty + if [ -n "$RESPONSE" ] && [ "$RESPONSE" != "null" ]; then + echo "$RESPONSE" > $CERTS_FOLDER/server.key + echo "Server Key successfully saved." + ATTEMPT=0 + SUCCES_OPERATION=true + break + else + echo "Invalid response ('null' or empty), retrying in $RETRY_DELAY seconds..." + sleep $RETRY_DELAY + fi + done fi -# Setup inital value to ATTEMPT and SUCCESS_OPERATION -ATTEMPT=0 -SUCCES_OPERATION=false - -while [ $ATTEMPT -lt $MAX_RETRIES ]; do - # Increment ATTEMPT using eval - eval "ATTEMPT=\$((ATTEMPT + 1))" - echo "Attempt $ATTEMPT of $MAX_RETRIES" - - # Make the request to Vault and store the response in a variable - RESPONSE=$(curl -s -k --connect-timeout 5 --max-time 10 \ - --header "X-Vault-Token: $VAULT_TOKEN" \ - --request GET "$VAULT_ADDR/v1/secret/data/server_cert/private" | jq -r '.data.data.key') - - echo "$RESPONSE" - - # Check if the response is "null" or empty - if [ -n "$RESPONSE" ] && [ "$RESPONSE" != "null" ]; then - echo "$RESPONSE" > $CERTS_FOLDER/server.key - echo "Server Key successfully saved." - ATTEMPT=0 - SUCCES_OPERATION=true - break - else - echo "Invalid response ('null' or empty), retrying in $RETRY_DELAY seconds..." - sleep $RETRY_DELAY - fi -done - if [ "$SUCCES_OPERATION" = false ]; then echo "Error: Failed to retrieve a valid response after $MAX_RETRIES attempts." exit 1 # Exit with failure diff --git a/services/register/config.yaml b/services/register/config.yaml index 85fb232ca577dbf18c873e74383d7f380463c9c5..efe628888855ef05c69d8e8f58994421f79ad1ac 100644 --- a/services/register/config.yaml +++ b/services/register/config.yaml @@ -8,12 +8,13 @@ mongo: { 'host': 'mongo_register', 'port': '27017' } -ca_factory: { - "url": "vault", - "port": "8200", - "token": "dev-only-token", - "verify": False -} + +ca_factory: + url: !ENV ${VAULT_HOSTNAME} + port: "8200" + token: "dev-only-token" + verify: False + ccf: { "url": "capifcore", diff --git a/services/register/register_prepare.sh b/services/register/register_prepare.sh index 0523411490e11a6910015ee8ce59fc66b1bfc873..385011d9c220deb943ab24b98510e8a176d6a2cb 100644 --- a/services/register/register_prepare.sh +++ b/services/register/register_prepare.sh @@ -59,5 +59,6 @@ echo "Starting Register service with signed certificate." gunicorn --certfile=/usr/src/app/register_service/certs/register_cert.crt \ --keyfile=/usr/src/app/register_service/certs/register_key.key \ --ca-certs=/usr/src/app/register_service/certs/ca_root.crt \ + --timeout 120 \ --bind 0.0.0.0:8080 \ --chdir /usr/src/app/register_service wsgi:app \ No newline at end of file diff --git a/services/register/register_service/config.py b/services/register/register_service/config.py index 2ac31772c592bd967935a873ae08d3ba2c0791fc..f4c87cae4aa1da82b8c83190a445a7b21c041970 100644 --- a/services/register/register_service/config.py +++ b/services/register/register_service/config.py @@ -1,22 +1,75 @@ import os import yaml +import re + + +# pattern for global vars: look for ${word} +pattern = re.compile('.*?\${(\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): - self.cached = 0 - self.file="../config.yaml" - self.my_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() - - def get_config(self): - return self.my_config + def __init__(self): + self.cached = 0 + self.file="../config.yaml" + self.my_config = {} + + self.my_config = parse_config(path=self.file) + + def get_config(self): + return self.my_config diff --git a/services/run.sh b/services/run.sh index 5acc9afe69c71235ce234378b6bbe2f1236b3027..6871050991e7d60c8d90b6440e1e6262aa8f54f9 100755 --- a/services/run.sh +++ b/services/run.sh @@ -4,6 +4,7 @@ source $(dirname "$(readlink -f "$0")")/variables.sh help() { echo "Usage: $1 " echo " -c : Setup different hostname for capif" + echo " -a : Setup different hostname for vault" echo " -R : Setup different hostname for register service" echo " -s : Run Mock server. Default true" echo " -m : Run monitoring service" @@ -21,11 +22,11 @@ help() { docker_version=$(docker compose version --short | cut -d',' -f1) IFS='.' read -ra version_components <<< "$docker_version" -if [ "${version_components[0]}" -ge 2 ] && [ "${version_components[1]}" -ge 10 ]; then - echo "Docker compose version it greater than 2.10" +if [ "${version_components[0]}" -gt 2 ] || { [ "${version_components[0]}" -eq 2 ] && [ "${version_components[1]}" -ge 10 ]; }; then + echo "Docker compose version is greater than or equal to 2.10" else - echo "Docker compose version is not valid. Should be greater than 2.10" - exit 1 + echo "Docker compose version is not valid. Should be >= 2.10" + exit 1 fi # Check if yq is installed @@ -36,11 +37,14 @@ then fi # Read params -while getopts ":c:l:ms:hrv:f:g:b:" opt; do +while getopts ":c:a:l:ms:hrv:f:g:b:" opt; do case $opt in c) CAPIF_HOSTNAME="$OPTARG" ;; + a) + CAPIF_VAULT="$OPTARG" + ;; R) CAPIF_REGISTER="$OPTARG" ;; @@ -82,7 +86,7 @@ while getopts ":c:l:ms:hrv:f:g:b:" opt; do esac done -echo Nginx hostname will be $CAPIF_HOSTNAME, Register Hostname $CAPIF_REGISTER, deploy $DEPLOY, monitoring $MONITORING_STATE +echo Nginx hostname will be $CAPIF_HOSTNAME, Vault $CAPIF_VAULT, Register Hostname $CAPIF_REGISTER, deploy $DEPLOY, monitoring $MONITORING_STATE if [ "$BUILD_DOCKER_IMAGES" == "true" ] ; then echo '***Building Docker images set as true***' @@ -109,15 +113,17 @@ fi docker network create capif-network -# Deploy Vault service -REGISTRY_BASE_URL=$REGISTRY_BASE_URL OCF_VERSION=$OCF_VERSION CAPIF_HOSTNAME=$CAPIF_HOSTNAME docker compose -f "$SERVICES_DIR/docker-compose-vault.yml" up --detach $BUILD $CACHED_INFO - -status=$? -if [ $status -eq 0 ]; then - echo "*** Vault Service Runing ***" -else - echo "*** Vault failed to start ***" - exit $status +## Deploy Vault service +if [ "$CAPIF_VAULT" == "vault" ] ; then + REGISTRY_BASE_URL=$REGISTRY_BASE_URL OCF_VERSION=$OCF_VERSION CAPIF_HOSTNAME=$CAPIF_HOSTNAME docker compose -f "$SERVICES_DIR/docker-compose-vault.yml" up --detach $BUILD $CACHED_INFO + + status=$? + if [ $status -eq 0 ]; then + echo "*** Vault Service Runing ***" + else + echo "*** Vault failed to start ***" + exit $status + fi fi # Deploy Capif services diff --git a/services/show_logs.sh b/services/show_logs.sh index 7a4c807b20638893efa3089f1cc3cd3455067ce2..d71ae680e230474a167e4c61f30ad3adab3cf4dd 100755 --- a/services/show_logs.sh +++ b/services/show_logs.sh @@ -34,7 +34,7 @@ FOLLOW="" DUID=$(id -u) DGID=$(id -g) -# Read params +# Read parameters while getopts "cvrahmfs" opt; do case $opt in c)