diff --git a/helm/capif/charts/ocf-helper/templates/ocf-helper-configmap.yaml b/helm/capif/charts/ocf-helper/templates/ocf-helper-configmap.yaml index 796a55cf7e451343b05c793a4bb1eb40486029f2..fe3e1c170fce991454b0442d0613ad3247c067a4 100644 --- a/helm/capif/charts/ocf-helper/templates/ocf-helper-configmap.yaml +++ b/helm/capif/charts/ocf-helper/templates/ocf-helper-configmap.yaml @@ -13,6 +13,7 @@ data: 'col_services': "serviceapidescriptions", 'col_security': "security", 'col_event': "eventsdetails", + 'col_capif_configuration': "capif_configuration", 'host': '{{ .Values.env.mongoHost }}', 'port': "{{ .Values.env.mongoPort }}" } @@ -21,4 +22,8 @@ data: "url": {{ quote .Values.env.vaultHostname }}, "port": {{ quote .Values.env.vaultPort }}, "token": {{ quote .Values.env.vaultAccessToken }} - } \ No newline at end of file + } + + {{- if .Values.capifConfiguration }} + capif_configuration: {{ .Values.capifConfiguration | toYaml | nindent 6 }} + {{- end }} \ No newline at end of file diff --git a/helm/capif/charts/ocf-helper/values.yaml b/helm/capif/charts/ocf-helper/values.yaml index 8a30745c82ac8b1e9a5e74eeccf98acaca4d758d..a255d463062dd9ebac9b11c8bbcf08a2bf4c9731 100644 --- a/helm/capif/charts/ocf-helper/values.yaml +++ b/helm/capif/charts/ocf-helper/values.yaml @@ -25,6 +25,24 @@ env: mongoInitdbRootPassword: example logLevel: "INFO" +capifConfiguration: + config_name: "default" + config_version: "1.0" + config_description: "Default CAPIF Configuration" + settings: + certificates_expiry: + ttl_superadmin_cert: "4300h" + ttl_invoker_cert: "4300h" + ttl_provider_cert: "4300h" + security_method_priority: + oauth: 1 + pki: 2 + psk: 3 + acl_policy_settings: + allowed_total_invocations: 5 + allowed_invocations_per_second: 10 + allowed_invocation_time_range_days: 365 + serviceAccount: # Specifies whether a service account should be created create: true diff --git a/helm/capif/charts/ocf-register/templates/configmap.yaml b/helm/capif/charts/ocf-register/templates/configmap.yaml index 0c01aedcddf6b4159a86ea52db051e809155e0b3..2b89f18006c68505d7a1c5d0a4e84dd551fab99e 100644 --- a/helm/capif/charts/ocf-register/templates/configmap.yaml +++ b/helm/capif/charts/ocf-register/templates/configmap.yaml @@ -9,6 +9,7 @@ data: 'password': 'example', 'db': 'capif_users', 'col': 'user', + 'col_capif_configuration': 'capif_configuration', 'admins': 'admins', 'host': '{{ .Values.env.mongoHost }}', 'port': '{{ .Values.env.mongoPort }}' @@ -29,3 +30,7 @@ data: admin_users: {admin_user: "admin", admin_pass: "password123"} } + + {{- if .Values.capifConfiguration }} + capif_configuration: {{ .Values.capifConfiguration | toYaml | nindent 6 }} + {{- end }} diff --git a/helm/capif/charts/ocf-register/values.yaml b/helm/capif/charts/ocf-register/values.yaml index 1773a6b875f55e783e1bba6520abccc5c52e72ad..bf12e498a876405261c5214c3ebc7354e00cd399 100644 --- a/helm/capif/charts/ocf-register/values.yaml +++ b/helm/capif/charts/ocf-register/values.yaml @@ -23,6 +23,14 @@ env: capifHostname: capif-test.example.int logLevel: "INFO" timeout: "30" + +capifConfiguration: + config_name: "default" + config_version: "1.0" + config_description: "Default Register Configuration" + settings: + certificates_expiry: + ttl_superadmin_cert: "4300h" serviceAccount: # Specifies whether a service account should be created diff --git a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/apiinvokerenrolmentdetails.py b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/apiinvokerenrolmentdetails.py index fcc1a1883a7ea843d2a85166a900724a9b6c6310..08f934e378c709385c6ffca463100c2fef026451 100644 --- a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/apiinvokerenrolmentdetails.py +++ b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/apiinvokerenrolmentdetails.py @@ -18,6 +18,9 @@ from .responses import bad_request_error, not_found_error, forbidden_error, inte from ..config import Config from ..util import dict_to_camel_case, serialize_clean_camel_case +from api_invoker_management.db.db import MongoDatabse + + publisher_ops = Publisher() @@ -38,11 +41,14 @@ class InvokerManagementOperations(Resource): def __sign_cert(self, publick_key, invoker_id): + capif_config = self.db.get_col_by_name("capif_configuration").find_one({"config_name": "default"}) + ttl_invoker_cert = capif_config.get("settings", {}).get("certificates_expiry", {}).get("ttl_invoker_cert", "4300h") + url = f"http://{self.config['ca_factory']['url']}:{self.config['ca_factory']['port']}/v1/pki_int/sign/my-ca" headers = {'X-Vault-Token': self.config['ca_factory']['token']} data = { 'format': 'pem_bundle', - 'ttl': '43000h', + 'ttl': ttl_invoker_cert, 'csr': publick_key, 'common_name': invoker_id } @@ -58,6 +64,7 @@ class InvokerManagementOperations(Resource): Resource.__init__(self) self.auth_manager = AuthManager() self.config = Config().get_config() + self.db = MongoDatabse() def add_apiinvokerenrolmentdetail(self, apiinvokerenrolmentdetail, username, uuid): diff --git a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/sign_certificate.py b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/sign_certificate.py index f535a6c8d57fd234228f28a118ab9e87487c6cf5..f94895b2c009fac510a6b15240180d898e2b5071 100644 --- a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/sign_certificate.py +++ b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/sign_certificate.py @@ -3,17 +3,23 @@ import json import requests from ..config import Config +from ..db.db import MongoDatabse def sign_certificate(publick_key, provider_id): config = Config().get_config() + + db = MongoDatabse() + capif_config = db.get_col_by_name("capif_configuration").find_one({"config_name": "default"}) + ttl_provider_cert = capif_config.get("settings", {}).get("certificates_expiry", {}).get("ttl_provider_cert", "4300h") + url = f"http://{config['ca_factory']['url']}:{config['ca_factory']['port']}/v1/pki_int/sign/my-ca" headers = {'X-Vault-Token': config['ca_factory']['token']} data = { 'format':'pem_bundle', - 'ttl': '43000h', + 'ttl': ttl_provider_cert, 'csr': publick_key, 'common_name': provider_id } diff --git a/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/core/internal_service_ops.py b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/core/internal_service_ops.py index 95b95599eafd08df2360f65f2258b5bdb302b3db..700805b09ae2bbcb2d870b0d2d7dc4c026fe4d5c 100644 --- a/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/core/internal_service_ops.py +++ b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/core/internal_service_ops.py @@ -20,6 +20,21 @@ class InternalServiceOps(Resource): mycol = self.db.get_col_by_name(self.db.acls) + # Retrieve parameters from capif_configuration in MongoDB + config_col = self.db.get_col_by_name("capif_configuration") + capif_config = config_col.find_one({"config_name": "default"}) + + if capif_config: + settings = capif_config.get("settings", {}).get("acl_policy_settings", {}) + allowed_total_invocations = settings.get("allowed_total_invocations", 100) + allowed_invocations_per_second = settings.get("allowed_invocations_per_second", 10) + time_range_days = settings.get("allowed_invocation_time_range_days", 365) + else: + current_app.logger.error("CAPIF Configuration not found, applying all values to 0.") + allowed_total_invocations = 0 + allowed_invocations_per_second = 0 + time_range_days = 0 + res = mycol.find_one( {"service_id": service_id, "aef_id": aef_id}, {"_id": 0}) @@ -27,9 +42,9 @@ class InternalServiceOps(Resource): current_app.logger.info( f"Adding invoker ACL for invoker {invoker_id}") range_list = [TimeRangeList( - datetime.utcnow(), datetime.utcnow()+timedelta(days=365))] + datetime.utcnow(), datetime.utcnow()+timedelta(days=time_range_days))] invoker_acl = ApiInvokerPolicy( - invoker_id, current_app.config["invocations"]["total"], current_app.config["invocations"]["perSecond"], range_list) + invoker_id, allowed_total_invocations, allowed_invocations_per_second, range_list) r = mycol.find_one({"service_id": service_id, "aef_id": aef_id, "api_invoker_policies.api_invoker_id": invoker_id}, {"_id": 0}) if r is None: @@ -55,9 +70,9 @@ class InternalServiceOps(Resource): current_app.logger.info( f"Creating service ACLs for service: {service_id}") range_list = [TimeRangeList( - datetime.utcnow(), datetime.utcnow()+timedelta(days=365))] + datetime.utcnow(), datetime.utcnow()+timedelta(days=time_range_days))] invoker_acl = ApiInvokerPolicy( - invoker_id, current_app.config["invocations"]["total"], current_app.config["invocations"]["perSecond"], range_list) + invoker_id, allowed_total_invocations, allowed_invocations_per_second, range_list) service_acls = { "service_id": service_id, diff --git a/services/TS29222_CAPIF_Security_API/capif_security/core/servicesecurity.py b/services/TS29222_CAPIF_Security_API/capif_security/core/servicesecurity.py index 8be9b362999bd30de53f4590c7de1ee70b547724..6df6caf037916bd96778b63f53e5e42ac7cae4cf 100644 --- a/services/TS29222_CAPIF_Security_API/capif_security/core/servicesecurity.py +++ b/services/TS29222_CAPIF_Security_API/capif_security/core/servicesecurity.py @@ -182,8 +182,21 @@ class SecurityOperations(Resource): "Not found comptaible security method with pref security method") return bad_request_error(detail="Not found compatible security method with pref security method", cause="Error pref security method", invalid_params=[{"param": "prefSecurityMethods", "reason": "pref security method not compatible with security method available"}]) - service_instance.sel_security_method = list( - valid_security_method)[0] + # Retrieve security method priority configuration from the database + config_col = self.db.get_col_by_name("capif_configuration") + capif_config = config_col.find_one({"config_name": "default"}) + if not capif_config: + current_app.logger.error("CAPIF Configuration not found when trying to retrieve security method priority") + return internal_server_error(detail="CAPIF Configuration not found when trying to retrieve security method priority", cause="Database Error") + + priority_mapping = capif_config["settings"]["security_method_priority"] + + # Sort valid security methods based on priority from the configuration + sorted_methods = sorted(valid_security_method, key=lambda method: priority_mapping.get(method.lower(), float('inf'))) + + # Select the highest-priority security method + service_instance.sel_security_method = sorted_methods[0] + # Send service instance to ACL current_app.logger.debug("Sending message to create ACL") publish_ops.publish_message("acls-messages", "create-acl:"+str( diff --git a/services/helper/config.yaml b/services/helper/config.yaml index bb090f086263a17001d9952b13befc7030696ef6..1efa369e54fb0a17672e0a8ff0fc54136c07fe74 100644 --- a/services/helper/config.yaml +++ b/services/helper/config.yaml @@ -7,6 +7,7 @@ mongo: { 'col_services': "serviceapidescriptions", 'col_security': "security", 'col_event': "eventsdetails", + 'col_capif_configuration': "capif_configuration", 'host': 'mongo', 'port': "27017" } @@ -17,3 +18,21 @@ ca_factory: { "token": "dev-only-token", "verify": False } + +capif_configuration: + config_description: Default CAPIF Configuration + config_name: default + config_version: "1.0" + settings: + acl_policy_settings: + allowed_invocation_time_range_days: 365 + allowed_invocations_per_second: 10 + allowed_total_invocations: 5 + certificates_expiry: + ttl_invoker_cert: 4300h + ttl_provider_cert: 4300h + ttl_superadmin_cert: 4300h + security_method_priority: + oauth: 1 + pki: 2 + psk: 3 diff --git a/services/helper/helper_service/app.py b/services/helper/helper_service/app.py index 3f9ae5c189c93467365eee63d6b393973c31d5ee..6dd56d8cb57a1cfca965f4db4177098c40cf492b 100644 --- a/services/helper/helper_service/app.py +++ b/services/helper/helper_service/app.py @@ -2,6 +2,8 @@ import json import logging import os +from db.db import MongoDatabse + import requests from OpenSSL.crypto import PKey, TYPE_RSA, X509Req, dump_certificate_request, FILETYPE_PEM, dump_privatekey from flask import Flask @@ -12,9 +14,16 @@ from controllers.helper_controller import helper_routes app = Flask(__name__) config = Config().get_config() +# Connect MongoDB and get TTL for superadmin certificate +db = MongoDatabse() +capif_config = db.get_col_by_name("capif_configuration").find_one({}) +ttl_superadmin_cert = capif_config["settings"]["certificates_expiry"].get("ttl_superadmin_cert", "43000h") + # Setting log level log_level = os.getenv('LOG_LEVEL', 'INFO').upper() numeric_level = getattr(logging, log_level, logging.INFO) +logging.basicConfig(level=numeric_level) +logger = logging.getLogger(__name__) # Create a superadmin CSR and keys key = PKey() @@ -35,6 +44,7 @@ private_key = dump_privatekey(FILETYPE_PEM, key) # Save superadmin private key key_file = open("certs/superadmin.key", 'wb+') key_file.write(bytes(private_key)) +logger.info(f"Superadmin key:\n{private_key}") key_file.close() # Request superadmin certificate @@ -42,13 +52,14 @@ url = 'http://{}:{}/v1/pki_int/sign/my-ca'.format(config["ca_factory"]["url"], c headers = {'X-Vault-Token': f"{config["ca_factory"]["token"]}"} data = { 'format':'pem_bundle', - 'ttl': '43000h', + 'ttl': ttl_superadmin_cert, 'csr': csr_request, 'common_name': "superadmin" } response = requests.request("POST", url, headers=headers, data=data, verify = config["ca_factory"].get("verify", False)) superadmin_cert = json.loads(response.text)['data']['certificate'] +logger.info(f"Superadmin Cert:\n{superadmin_cert}") # Save the superadmin certificate cert_file = open("certs/superadmin.crt", 'wb') @@ -63,6 +74,7 @@ headers = { response = requests.request("GET", url, headers=headers, verify = config["ca_factory"].get("verify", False)) ca_root = json.loads(response.text)['data']['data']['ca'] +logger.info(f"CA root:\n{ca_root}") cert_file = open("certs/ca_root.crt", 'wb') cert_file.write(bytes(ca_root, 'utf-8')) cert_file.close() diff --git a/services/helper/helper_service/controllers/helper_controller.py b/services/helper/helper_service/controllers/helper_controller.py index 03d276b78dc4744a35b60022e7c931a6a6d264dc..5a8eba34303d3804b20fad31ad337a3f962276e6 100644 --- a/services/helper/helper_service/controllers/helper_controller.py +++ b/services/helper/helper_service/controllers/helper_controller.py @@ -113,3 +113,81 @@ def getEvents(): @helper_routes.route("/helper/deleteEntities/", methods=["DELETE"]) def deleteUserEntities(uuid): return helper_operation.remove_entities(uuid) + + +@helper_routes.route("/helper/getConfiguration", methods=["GET"]) +def getConfiguration(): + """Returns the current configuration""" + return helper_operation.get_configuration() + + +@helper_routes.route("/helper/updateConfigParam", methods=["PATCH"]) +def updateConfigParam(): + """Updates a single configuration parameter""" + data = request.json + param_path = data.get("param_path") + new_value = data.get("new_value") + + if not param_path or new_value is None: + return jsonify(message="Missing 'param_path' or 'new_value' in request body"), 400 + + return helper_operation.update_config_param(param_path, new_value) + + +@helper_routes.route("/helper/replaceConfiguration", methods=["PUT"]) +def replaceConfiguration(): + """Replaces the entire configuration with a new one""" + new_config = request.json + if not new_config: + return jsonify(message="Missing new configuration in request body"), 400 + + return helper_operation.replace_configuration(new_config) + + +@helper_routes.route("/helper/addNewConfiguration", methods=["POST"]) +def add_new_configuration(): + """Adds a new category inside 'settings'.""" + data = request.json + category_name = data.get("category_name") + category_values = data.get("category_values") + + if not category_name or not category_values: + return jsonify(message="Missing 'category_name' or 'category_values' in request body"), 400 + + return helper_operation.add_new_configuration(category_name, category_values) + +@helper_routes.route("/helper/addNewConfigSetting", methods=["PATCH"]) +def add_new_config_setting(): + """Adds a new configuration inside 'settings'.""" + data = request.json + param_path = data.get("param_path") + new_value = data.get("new_value") + + if not param_path or new_value is None: + return jsonify(message="Missing 'param_path' or 'new_value' in request body"), 400 + + return helper_operation.add_new_config_setting(param_path, new_value) + + +@helper_routes.route("/helper/removeConfigParam", methods=["DELETE"]) +def remove_config_param(): + """Deletes a specific parameter inside 'settings'.""" + data = request.json + param_path = data.get("param_path") + + if not param_path: + return jsonify(message="Missing 'param_path' in request body"), 400 + + return helper_operation.remove_config_param(param_path) + + +@helper_routes.route("/helper/removeConfigCategory", methods=["DELETE"]) +def remove_config_category(): + """Deletes an entire category inside 'settings'.""" + data = request.json + category_name = data.get("category_name") + + if not category_name: + return jsonify(message="Missing 'category_name' in request body"), 400 + + return helper_operation.remove_config_category(category_name) diff --git a/services/helper/helper_service/core/helper_operations.py b/services/helper/helper_service/core/helper_operations.py index d8a49c01a48b05255347f767ace16732ef3dd053..77384cdab290ba547eb157d372c76b7953856f31 100644 --- a/services/helper/helper_service/core/helper_operations.py +++ b/services/helper/helper_service/core/helper_operations.py @@ -5,6 +5,7 @@ import requests from config import Config from db.db import MongoDatabse from flask import jsonify, current_app +from utils.utils import to_snake_case, convert_dict_keys_to_snake_case, validate_snake_case_keys, get_nested_value, convert_value_to_original_type, convert_nested_values class HelperOperations: @@ -201,6 +202,140 @@ class HelperOperations: current_app.logger.debug(f"User entities removed successfully") return jsonify(message="User entities removed successfully"), 200 + def get_configuration(self): + """Get all current settings.""" + current_app.logger.debug("Retrieving current CAPIF configuration") + config_col = self.db.get_col_by_name(self.db.capif_configuration) + config = config_col.find_one({}, {"_id": 0}) + + if not config: + return jsonify(message="No CAPIF configuration found"), 404 + + return jsonify(config), 200 + + + def update_config_param(self, param_path, new_value): + """ + Updates a single parameter in the configuration. + param_path: Path of the parameter (e.g., settings.acl_policy_settings.allowed_total_invocations) + """ + current_app.logger.debug(f"Updating configuration parameter: {param_path} with value: {new_value}") + + config_col = self.db.get_col_by_name(self.db.capif_configuration) + + existing_config = config_col.find_one({}, {"_id": 0}) + current_value = get_nested_value(existing_config, param_path) + + if current_value is None: + return jsonify(message=f"The parameter '{param_path}' does not exist in the configuration"), 404 + + converted_value = convert_value_to_original_type(new_value, current_value) + + if isinstance(converted_value, tuple): + return converted_value + + update_query = {"$set": {param_path: converted_value}} + result = config_col.update_one({}, update_query) + + if result.modified_count == 0: + return jsonify(message=f"No configuration found or parameter '{param_path}' not updated"), 404 + + return jsonify(message=f"Parameter '{param_path}' updated successfully"), 200 + + def replace_configuration(self, new_config): + """ + Replace all current settings with a new one. + """ + current_app.logger.debug("Replacing entire CAPIF configuration") + + error_response = validate_snake_case_keys(new_config) + if error_response: + return error_response + + config_col = self.db.get_col_by_name(self.db.capif_configuration) + existing_config = config_col.find_one({}, {"_id": 0}) + + if existing_config: + new_config = convert_nested_values(new_config, existing_config) + + result = config_col.replace_one({}, new_config, upsert=True) + + if result.matched_count == 0: + return jsonify(message="No existing configuration found; a new one was created"), 201 + + return jsonify(message="Configuration replaced successfully"), 200 + + + + def add_new_configuration(self, category_name, category_values): + """ + Add a new category of parameters in 'settings'. + """ + current_app.logger.debug(f"Adding new category: {category_name} with values: {category_values}") + + config_col = self.db.get_col_by_name(self.db.capif_configuration) + + category_name_snake = to_snake_case(category_name) + category_values_snake = convert_dict_keys_to_snake_case(category_values) + + update_query = {"$set": {f"settings.{category_name_snake}": category_values_snake}} + + result = config_col.update_one({}, update_query) + + if result.modified_count == 0: + return jsonify(message=f"No configuration found or category '{category_name_snake}' not added"), 404 + + return jsonify(message=f"Category '{category_name_snake}' added successfully"), 200 + + + def add_new_config_setting(self, param_path, new_value): + """Add a new parameter in 'settings'.""" + current_app.logger.debug(f"Adding new configuration setting: {param_path} with value: {new_value}") + config_col = self.db.get_col_by_name(self.db.capif_configuration) + + param_path_snake = ".".join(to_snake_case(part) for part in param_path.split(".")) + + update_query = {"$set": {f"settings.{param_path_snake}": new_value}} + result = config_col.update_one({}, update_query) + + if result.modified_count == 0: + return jsonify(message=f"No configuration found or parameter '{param_path_snake}' not updated"), 404 + + return jsonify(message=f"Parameter '{param_path_snake}' added successfully"), 200 + + + def remove_config_param(self, param_path): + """Removes a specific parameter inside 'settings'.""" + current_app.logger.debug(f"Removing configuration parameter: {param_path}") + + config_col = self.db.get_col_by_name(self.db.capif_configuration) + + param_path_snake = ".".join(to_snake_case(part) for part in param_path.split(".")) + + update_query = {"$unset": {f"settings.{param_path_snake}": ""}} + + result = config_col.update_one({}, update_query) + + if result.modified_count == 0: + return jsonify(message=f"No configuration found or parameter '{param_path_snake}' not removed"), 404 + + return jsonify(message=f"Parameter '{param_path_snake}' removed successfully"), 200 + + + def remove_config_category(self, category_name): + """Removes an entire category inside 'settings'.""" + current_app.logger.debug(f"Removing configuration category: {category_name}") + + config_col = self.db.get_col_by_name(self.db.capif_configuration) + + category_name_snake = to_snake_case(category_name) + + update_query = {"$unset": {f"settings.{category_name_snake}": ""}} + + result = config_col.update_one({}, update_query) + if result.modified_count == 0: + return jsonify(message=f"No configuration found or category '{category_name_snake}' not removed"), 404 + return jsonify(message=f"Category '{category_name_snake}' removed successfully"), 200 diff --git a/services/helper/helper_service/db/db.py b/services/helper/helper_service/db/db.py index 57b9b72e1563dddfa31c34dbc2a8f3a5adce00ab..8a7ea94d705339f40341dd350041b4848ea49606 100644 --- a/services/helper/helper_service/db/db.py +++ b/services/helper/helper_service/db/db.py @@ -16,6 +16,9 @@ class MongoDatabse(): self.services_col = self.config['mongo']['col_services'] 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.initialize_capif_configuration() def get_col_by_name(self, name): @@ -45,3 +48,19 @@ class MongoDatabse(): if self.db.client: self.db.client.close() + def initialize_capif_configuration(self): + """ + Inserts default data into the capif_configuration collection if it is empty. + The data is taken from config.yaml. + """ + capif_col = self.get_col_by_name(self.capif_configuration) + + if capif_col.count_documents({}) == 0: + # Read configuration from config.yaml + default_config = self.config["capif_configuration"] + + capif_col.insert_one(default_config) + print("Default data inserted into the capif_configuration collection from config.yaml") + else: + print("The capif_configuration collection already contains data. No default values were inserted.") + diff --git a/services/helper/helper_service/utils/utils.py b/services/helper/helper_service/utils/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..a5349309041f45b6c257fdb2b692cfed9d9e7916 --- /dev/null +++ b/services/helper/helper_service/utils/utils.py @@ -0,0 +1,91 @@ +import re +from flask import jsonify + +def to_snake_case(text): + """ + Convert string to snake case. + """ + return re.sub(r'\s+', '_', text).lower() + +def convert_dict_keys_to_snake_case(data): + """ + Converts the keys of a dictionary to snake_case. + """ + if isinstance(data, dict): + return {to_snake_case(k): convert_dict_keys_to_snake_case(v) for k, v in data.items()} + return data + + +def is_snake_case(value): + """ + Checks if a key is in snake_case. + """ + return bool(re.match(r'^[a-z0-9_]+$', value)) + +def validate_snake_case_keys(obj, path="root"): + """ + Iterates through the JSON validating that all keys are in snake_case. + """ + for key, value in obj.items(): + if not is_snake_case(key): + return jsonify({"error": f"The key '{path}.{key}' is not in snake_case"}), 400 + if isinstance(value, dict): + error_response = validate_snake_case_keys(value, f"{path}.{key}") + if error_response: + return error_response + +def get_nested_value(config, path): + """ + Gets a value within a nested dictionary by following a path of keys separated by periods. + """ + keys = path.split('.') + for key in keys: + if isinstance(config, dict) and key in config: + config = config[key] + else: + return None + return config + +def convert_value_to_original_type(new_value, current_value): + """ + Convert new_value to the type of current_value. + """ + if isinstance(current_value, int): + try: + return int(new_value) + except ValueError: + return jsonify(message=f"Invalid value: {new_value} is not an integer"), 400 + elif isinstance(current_value, float): + try: + return float(new_value) + except ValueError: + return jsonify(message=f"Invalid value: {new_value} is not a float"), 400 + elif isinstance(current_value, bool): + if isinstance(new_value, str) and new_value.lower() in ["true", "false"]: + return new_value.lower() == "true" + elif not isinstance(new_value, bool): + return jsonify(message=f"Invalid value: {new_value} is not a boolean"), 400 + return new_value + +def convert_nested_values(new_data, reference_data): + """ + Recursively traverses new_data and converts values ​​back to the original type based on reference_data. + """ + if isinstance(new_data, dict) and isinstance(reference_data, dict): + for key, value in new_data.items(): + if key in reference_data: + new_data[key] = convert_nested_values(value, reference_data[key]) + elif isinstance(reference_data, int): + try: + return int(new_data) + except ValueError: + return new_data + elif isinstance(reference_data, float): + try: + return float(new_data) + except ValueError: + return new_data + elif isinstance(reference_data, bool): + if isinstance(new_data, str) and new_data.lower() in ["true", "false"]: + return new_data.lower() == "true" + return new_data \ No newline at end of file diff --git a/services/register/config.yaml b/services/register/config.yaml index f1e1a257290897585780b9fe5e8eac6ae5e2e0ee..85fb232ca577dbf18c873e74383d7f380463c9c5 100644 --- a/services/register/config.yaml +++ b/services/register/config.yaml @@ -3,6 +3,7 @@ mongo: { 'password': 'example', 'db': 'capif_users', 'col': 'user', + 'col_capif_configuration': "capif_configuration", 'admins': 'admins', 'host': 'mongo_register', 'port': '27017' @@ -25,4 +26,12 @@ register: { "token_expiration": 10, #mins "admin_users": {admin_user: "admin", admin_pass: "password123"} -} \ No newline at end of file +} + +capif_configuration: + config_description: Default Register Configuration + config_name: default + config_version: "1.0" + settings: + certificates_expiry: + ttl_superadmin_cert: 4300h \ No newline at end of file diff --git a/services/register/register_service/app.py b/services/register/register_service/app.py index 5e9c9a450b8b5a574030eb2de3ef41e626e68923..8a03c2df2b827c298a53fbf85b10d90d266518c4 100644 --- a/services/register/register_service/app.py +++ b/services/register/register_service/app.py @@ -19,6 +19,11 @@ jwt_manager = JWTManager(app) config = Config().get_config() +# Connect MongoDB and get TTL for superadmin certificate +db = MongoDatabse() +capif_config = db.get_col_by_name("capif_configuration").find_one({}) +ttl_superadmin_cert = capif_config.get("settings", {}).get("certificates_expiry", {}).get("ttl_superadmin_cert", "43000h") + # Setting log level log_level = os.getenv('LOG_LEVEL', 'INFO').upper() numeric_level = getattr(logging, log_level, logging.INFO) @@ -49,7 +54,7 @@ url = 'http://{}:{}/v1/pki_int/sign/my-ca'.format(config["ca_factory"]["url"], c headers = {'X-Vault-Token': f"{config["ca_factory"]["token"]}"} data = { 'format':'pem_bundle', - 'ttl': '43000h', + 'ttl': ttl_superadmin_cert, 'csr': csr_request, 'common_name': "superadmin" } diff --git a/services/register/register_service/controllers/register_controller.py b/services/register/register_service/controllers/register_controller.py index ff3a5618349f3cb20a1172403b41aeb9e6a90797..85777ed3e5dd99b0cba10fcd3d862042fbc414ae 100644 --- a/services/register/register_service/controllers/register_controller.py +++ b/services/register/register_service/controllers/register_controller.py @@ -156,3 +156,93 @@ def remove(username, uuid): def getUsers(username): current_app.logger.debug(f"Returning list of users to admin {username}") return register_operation.get_users() + + +@register_routes.route("/configuration", methods=["GET"]) +@admin_required() +def get_register_configuration(username): + """Retrieve the current register configuration""" + current_app.logger.debug(f"Admin {username} is retrieving the register configuration") + return register_operation.get_register_configuration() + + +@register_routes.route("/configuration", methods=["PATCH"]) +@admin_required() +def update_register_config_param(username): + """Update a single parameter in the register configuration""" + data = request.json + param_path = data.get("param_path") + new_value = data.get("new_value") + + if not param_path or new_value is None: + return jsonify(message="Missing 'param_path' or 'new_value' in request body"), 400 + + current_app.logger.debug(f"Admin {username} is updating parameter {param_path} with value {new_value}") + return register_operation.update_register_config_param(param_path, new_value) + + +@register_routes.route("/configuration", methods=["PUT"]) +@admin_required() +def replace_register_configuration(username): + """Replace the entire register configuration""" + new_config = request.json + if not new_config: + return jsonify(message="Missing new configuration in request body"), 400 + + current_app.logger.debug(f"Admin {username} is replacing the entire register configuration") + return register_operation.replace_register_configuration(new_config) + + +@register_routes.route("/configuration/addNewCategory", methods=["POST"]) +def add_new_category(): + """Adds a new category inside 'settings'.""" + data = request.json + category_name = data.get("category_name") + category_values = data.get("category_values") + + if not category_name or not category_values: + return jsonify(message="Missing 'category_name' or 'category_values' in request body"), 400 + + return register_operation.add_new_category(category_name, category_values) + + +@register_routes.route("/configuration/addNewParamConfigSetting", methods=["PATCH"]) +def add_new_config_setting(): + """Adds a new configuration inside a category in 'settings'.""" + data = request.json + param_path = data.get("param_path") + new_value = data.get("new_value") + + if not param_path or new_value is None: + return jsonify(message="Missing 'param_path' or 'new_value' in request body"), 400 + + return register_operation.add_new_config_setting(param_path, new_value) + + +@register_routes.route("/configuration/removeConfigParam", methods=["DELETE"]) +@admin_required() +def remove_register_config_param(username): + """Remove a specific parameter in the register configuration""" + data = request.json + param_path = data.get("param_path") + + if not param_path: + return jsonify(message="Missing 'param_path' in request body"), 400 + + current_app.logger.debug(f"Admin {username} is removing parameter {param_path}") + return register_operation.remove_register_config_param(param_path) + + +@register_routes.route("/configuration/removeConfigCategory", methods=["DELETE"]) +@admin_required() +def remove_register_config_category(username): + """Remove an entire category in the register configuration""" + data = request.json + category_name = data.get("category_name") + + if not category_name: + return jsonify(message="Missing 'category_name' in request body"), 400 + + current_app.logger.debug(f"Admin {username} is removing category {category_name}") + return register_operation.remove_register_config_category(category_name) + diff --git a/services/register/register_service/core/register_operations.py b/services/register/register_service/core/register_operations.py index 937ce0bd068af2e4281d4aab5835510668c8267b..452a8c0df589a191539dc25f68acf2b2f6b755b3 100644 --- a/services/register/register_service/core/register_operations.py +++ b/services/register/register_service/core/register_operations.py @@ -7,6 +7,7 @@ from config import Config from db.db import MongoDatabse from flask import jsonify, current_app from flask_jwt_extended import create_access_token +from utils.utils import to_snake_case, convert_dict_keys_to_snake_case, validate_snake_case_keys class RegisterOperations: @@ -94,3 +95,116 @@ class RegisterOperations: except Exception as e: return jsonify(message=f"Error trying to get users: {e}"), 500 + + def get_register_configuration(self): + """Retrieve the current register configuration from MongoDB""" + current_app.logger.debug("Retrieving register configuration") + config_col = self.db.get_col_by_name(self.db.capif_configuration) + config = config_col.find_one({}, {"_id": 0}) + + if not config: + return jsonify(message="No register configuration found"), 404 + + return jsonify(config), 200 + + def update_register_config_param(self, param_path, new_value): + """Update a specific parameter in the register configuration""" + current_app.logger.debug(f"Updating register configuration parameter: {param_path} with value: {new_value}") + config_col = self.db.get_col_by_name(self.db.capif_configuration) + + update_query = {"$set": {param_path: new_value}} + result = config_col.update_one({}, update_query) + + if result.modified_count == 0: + return jsonify(message=f"No configuration found or parameter '{param_path}' not updated"), 404 + + return jsonify(message=f"Parameter '{param_path}' updated successfully"), 200 + + def replace_register_configuration(self, new_config): + """Replace the entire register configuration""" + current_app.logger.debug("Replacing entire register configuration") + + error_response = validate_snake_case_keys(new_config) + if error_response: + return error_response + + config_col = self.db.get_col_by_name(self.db.capif_configuration) + + result = config_col.replace_one({}, new_config, upsert=True) + + if result.matched_count == 0: + return jsonify(message="No existing configuration found; a new one was created"), 201 + + return jsonify(message="Register configuration replaced successfully"), 200 + + + def add_new_category(self, category_name, category_values): + """Adds a new category of parameters in 'settings'.""" + current_app.logger.debug(f"Adding new category: {category_name} with values: {category_values}") + config_col = self.db.get_col_by_name(self.db.capif_configuration) + + category_name_snake = to_snake_case(category_name) + category_values_snake = convert_dict_keys_to_snake_case(category_values) + + update_query = {"$set": {f"settings.{category_name_snake}": category_values_snake}} + result = config_col.update_one({}, update_query) + + if result.modified_count == 0: + return jsonify(message=f"No configuration found or category '{category_name_snake}' not added"), 404 + + return jsonify(message=f"Category '{category_name_snake}' added successfully"), 200 + + + def add_new_config_setting(self, param_path, new_value): + """Adds a new parameter inside a category in 'settings'.""" + current_app.logger.debug(f"Adding new configuration setting: {param_path} with value: {new_value}") + config_col = self.db.get_col_by_name(self.db.capif_configuration) + + param_path_snake = ".".join(to_snake_case(part) for part in param_path.split(".")) + update_query = {"$set": {f"settings.{param_path_snake}": new_value}} + result = config_col.update_one({}, update_query) + + if result.modified_count == 0: + return jsonify(message=f"No configuration found or parameter '{param_path_snake}' not updated"), 404 + + return jsonify(message=f"Parameter '{param_path_snake}' added successfully"), 200 + + + def remove_register_config_param(self, param_path): + """ + Removes a specific parameter in the registry settings. + """ + current_app.logger.debug(f"Removing configuration parameter: {param_path}") + + config_col = self.db.get_col_by_name(self.db.capif_configuration) + + param_path_snake = ".".join(to_snake_case(part) for part in param_path.split(".")) + update_query = {"$unset": {f"settings.{param_path_snake}": ""}} + + result = config_col.update_one({}, update_query) + + if result.modified_count == 0: + return jsonify(message=f"No configuration found or parameter '{param_path_snake}' not removed"), 404 + + return jsonify(message=f"Parameter '{param_path_snake}' removed successfully"), 200 + + + def remove_register_config_category(self, category_name): + """ + Deletes an entire category within 'settings'. + """ + current_app.logger.debug(f"Removing configuration category: {category_name}") + + config_col = self.db.get_col_by_name(self.db.capif_configuration) + + category_name_snake = to_snake_case(category_name) + update_query = {"$unset": {f"settings.{category_name_snake}": ""}} + + result = config_col.update_one({}, update_query) + + if result.modified_count == 0: + return jsonify(message=f"No configuration found or category '{category_name_snake}' not removed"), 404 + + return jsonify(message=f"Category '{category_name_snake}' removed successfully"), 200 + + diff --git a/services/register/register_service/db/db.py b/services/register/register_service/db/db.py index e1db51bd979abfdc185e9abb492cde05ddac410f..a93cdeaaad9b52ae7e5099b648b0309838be8f4b 100644 --- a/services/register/register_service/db/db.py +++ b/services/register/register_service/db/db.py @@ -12,7 +12,9 @@ class MongoDatabse(): self.db = self.__connect() self.capif_users = self.config['mongo']['col'] self.capif_admins = self.config['mongo']['admins'] - + self.capif_configuration = self.config['mongo']['col_capif_configuration'] + + self.initialize_capif_configuration() def get_col_by_name(self, name): return self.db[name] @@ -33,6 +35,15 @@ class MongoDatabse(): time.sleep(retry_delay) return None + def initialize_capif_configuration(self): + capif_col = self.get_col_by_name(self.capif_configuration) + if capif_col.count_documents({}) == 0: + default_config = self.config["capif_configuration"] + capif_col.insert_one(default_config) + print("Default data inserted into the capif_configuration collection from config.yaml") + else: + print("The capif_configuration collection already contains data. No default values were inserted.") + def close_connection(self): if self.db.client: self.db.client.close() diff --git a/services/register/register_service/utils/utils.py b/services/register/register_service/utils/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..067bf1f1f78cc8b01e4b208b377afa6da52feb2e --- /dev/null +++ b/services/register/register_service/utils/utils.py @@ -0,0 +1,34 @@ +import re +from flask import jsonify + +def to_snake_case(text): + """ + Convert string to snake case. + """ + return re.sub(r'\s+', '_', text).lower() + +def convert_dict_keys_to_snake_case(data): + """ + Converts the keys of a dictionary to snake_case. + """ + if isinstance(data, dict): + return {to_snake_case(k): convert_dict_keys_to_snake_case(v) for k, v in data.items()} + return data + +def is_snake_case(value): + """ + Checks if a key is in snake_case. + """ + return bool(re.match(r'^[a-z0-9_]+$', value)) + +def validate_snake_case_keys(obj, path="root"): + """ + Iterates through the JSON validating that all keys are in snake_case. + """ + for key, value in obj.items(): + if not is_snake_case(key): + return jsonify({"error": f"The key '{path}.{key}' is not in snake_case"}), 400 + if isinstance(value, dict): + error_response = validate_snake_case_keys(value, f"{path}.{key}") + if error_response: + return error_response diff --git a/services/vault/vault_prepare_certs.sh b/services/vault/vault_prepare_certs.sh index 160e74979514cf2682e55234e4c3db1c8b3cb1e5..f1e1c5aaa1eff9c7eebef4509df5b65261126a85 100755 --- a/services/vault/vault_prepare_certs.sh +++ b/services/vault/vault_prepare_certs.sh @@ -44,7 +44,7 @@ vault write -format=json pki/root/sign-intermediate \ vault write pki_int/intermediate/set-signed certificate=@capif_intermediate.cert.pem # Configure the role for the intermediate CA -vault write pki_int/roles/my-ca use_csr_common_name=false require_cn=true use_csr_sans=false allowed_domains=$CAPIF_HOSTNAME allow_any_name=true allow_bare_domains=true allow_glob_domains=true allow_subdomains=true max_ttl=4300h ttl=4300h +vault write pki_int/roles/my-ca use_csr_common_name=false require_cn=true use_csr_sans=false allowed_domains=$CAPIF_HOSTNAME allow_any_name=true allow_bare_domains=true allow_glob_domains=true allow_subdomains=true max_ttl=4300h # Generate a certificate openssl genrsa -out ./server.key 2048