diff --git a/forms-flow-api/src/formsflow_api/constants/__init__.py b/forms-flow-api/src/formsflow_api/constants/__init__.py index ab5e78c25..21456bc50 100644 --- a/forms-flow-api/src/formsflow_api/constants/__init__.py +++ b/forms-flow-api/src/formsflow_api/constants/__init__.py @@ -127,6 +127,10 @@ class BusinessErrorCode(ErrorCodeMixin, Enum): "Invalid response received from admin service", HTTPStatus.BAD_REQUEST, ) + INVALID_PATH = ( + "The path must not contain: exists, export, role, current, logout, import, form, access, token, recaptcha or end with submission/action.", # pylint: disable=line-too-long + HTTPStatus.BAD_REQUEST, + ) def __new__(cls, message, status_code): """Constructor.""" diff --git a/forms-flow-api/src/formsflow_api/services/form_process_mapper.py b/forms-flow-api/src/formsflow_api/services/form_process_mapper.py index ac06e6d06..2f4863a02 100644 --- a/forms-flow-api/src/formsflow_api/services/form_process_mapper.py +++ b/forms-flow-api/src/formsflow_api/services/form_process_mapper.py @@ -792,6 +792,42 @@ def validate_form_title(cls, title, exclude_id=None): raise BusinessException(BusinessErrorCode.FORM_EXISTS) return True + @staticmethod + def validate_query_parameters(title, name, path): + """Check if at least one query parameter is provided.""" + if not (title or name or path): + raise BusinessException(BusinessErrorCode.INVALID_FORM_VALIDATION_INPUT) + + @staticmethod + def validate_path(path): + """Validate path with formio resevered keywords.""" + current_app.logger.debug(f"Validate path for reseverd keyword:{path}") + # Keywords that are invalid as standalone input + restricted_keywords = { + "exists", + "export", + "role", + "current", + "logout", + "import", + "form", + "access", + "token", + "recaptcha", + } + + # Forbidden end keywords + forbidden_end_keywords = {"submission", "action"} + + if ( + path in restricted_keywords + or path + and any(path.endswith(keyword) for keyword in forbidden_end_keywords) + ): + raise BusinessException(BusinessErrorCode.INVALID_PATH) + + return True + @staticmethod @user_context def validate_form_name_path_title(request, **kwargs): @@ -806,20 +842,25 @@ def validate_form_name_path_title(request, **kwargs): f"Title:{title}, Name:{name}, Path:{path}, form_id:{form_id}, parent_form_id: {parent_form_id}" ) - # Check if at least one query parameter is provided - if not (title or name or path): - raise BusinessException(BusinessErrorCode.INVALID_FORM_VALIDATION_INPUT) + FormProcessMapperService.validate_query_parameters(title, name, path) if title and len(title) > 200: raise BusinessException(BusinessErrorCode.INVALID_FORM_TITLE_LENGTH) FormProcessMapperService.validate_title_name_path(title, path, name) + # In case of new form creation, title alone passed form UI + # Trim space & validate path + if not parent_form_id and title: + path = title.replace(" ", "") + if current_app.config.get("MULTI_TENANCY_ENABLED"): user: UserContext = kwargs["user"] tenant_key = user.tenant_key name = f"{tenant_key}-{name}" path = f"{tenant_key}-{path}" + # Validate path has reserved keywords + FormProcessMapperService.validate_path(path) # Validate title exists validation on mapper & path, name in formio. if title: FormProcessMapperService.validate_form_title(title, parent_form_id) diff --git a/forms-flow-api/src/formsflow_api/services/import_support.py b/forms-flow-api/src/formsflow_api/services/import_support.py index d4355b0b7..104a0933b 100644 --- a/forms-flow-api/src/formsflow_api/services/import_support.py +++ b/forms-flow-api/src/formsflow_api/services/import_support.py @@ -281,6 +281,8 @@ def validate_form( # In case of new import validate title in mapper table & path,name in formio. FormProcessMapperService.validate_form_title(title, exclude_id=None) query_params = f"path={path}&name={name}&select=title,path,name,_id" + # Validate path has reserved keywords + FormProcessMapperService.validate_path(path) current_app.logger.info(f"Validating form exists...{query_params}") response = self.get_form_by_query(query_params) return response diff --git a/forms-flow-api/tests/unit/api/test_form_process_mapper.py b/forms-flow-api/tests/unit/api/test_form_process_mapper.py index a5910cb4c..9e6331dfa 100644 --- a/forms-flow-api/tests/unit/api/test_form_process_mapper.py +++ b/forms-flow-api/tests/unit/api/test_form_process_mapper.py @@ -590,6 +590,14 @@ def test_form_name_invalid_form_title(app, client, session, jwt, mock_redis_clie response.json["message"] == "Title: Only contain alphanumeric characters, hyphens(not at the start or end), spaces,and must include at least one letter." ) + # Validate for formio reserved keyword on path while new form creation + response = client.get("/form/validate?title=import", headers=headers) + assert response.status_code == 400 + assert response.json is not None + assert ( + response.json["message"] + == "The path must not contain: exists, export, role, current, logout, import, form, access, token, recaptcha or end with submission/action." + ) def test_form_name_invalid_form_name(app, client, session, jwt, mock_redis_client): @@ -650,6 +658,14 @@ def test_form_name_invalid_form_path(app, client, session, jwt, mock_redis_clien response.json["message"] == "Path: Only contain alphanumeric characters, hyphens(not at the start or end), no spaces,and must include at least one letter." ) + # Validate for formio reserved keyword on path + response = client.get("/form/validate?path=import", headers=headers) + assert response.status_code == 400 + assert response.json is not None + assert ( + response.json["message"] + == "The path must not contain: exists, export, role, current, logout, import, form, access, token, recaptcha or end with submission/action." + ) def test_form_name_invalid_form_name_title_path(