Skip to content

Commit

Permalink
Merge pull request AOT-Technologies#2397 from auslin-aot/FWF-4001-add…
Browse files Browse the repository at this point in the history
…-form-access-import

[Bug fix]  Added form access on import & export fixes
  • Loading branch information
arun-s-aot authored Dec 4, 2024
2 parents 75f8da0 + 3059627 commit e504a01
Show file tree
Hide file tree
Showing 8 changed files with 190 additions and 23 deletions.
1 change: 1 addition & 0 deletions deployment/docker/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ services:
API_LOG_BACKUP_COUNT: ${API_LOG_BACKUP_COUNT:-7}
CONFIGURE_LOGS: ${CONFIGURE_LOGS:-true}
REDIS_URL: ${REDIS_URL:-redis://redis:6379/0}
FORMSFLOW_ADMIN_URL: ${FORMSFLOW_ADMIN_URL}

stdin_open: true # -i
tty: true # -t
Expand Down
1 change: 1 addition & 0 deletions forms-flow-api/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ services:
API_LOG_BACKUP_COUNT: ${API_LOG_BACKUP_COUNT:-7}
CONFIGURE_LOGS: ${CONFIGURE_LOGS:-true}
REDIS_URL: ${REDIS_URL:-redis://redis:6379/0}
FORMSFLOW_ADMIN_URL: ${FORMSFLOW_ADMIN_URL}

stdin_open: true # -i
tty: true # -t
Expand Down
1 change: 1 addition & 0 deletions forms-flow-api/sample.env
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ FORMIO_ROOT_PASSWORD=changeme
##Multitenancy ENV Variables
#MULTI_TENANCY_ENABLED=false
#KEYCLOAK_ENABLE_CLIENT_AUTH=false
#FORMSFLOW_ADMIN_URL=http://{your-ip-address}:5010/api/v1

## Form embedding
#FORM_EMBED_JWT_SECRET=f6a69a42-7f8a-11ed-a1eb-0242ac120002
Expand Down
3 changes: 3 additions & 0 deletions forms-flow-api/src/formsflow_api/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,9 @@ class _Config: # pylint: disable=too-few-public-methods
# Configure LOG
CONFIGURE_LOGS = str(os.getenv("CONFIGURE_LOGS", default="true")).lower() == "true"

# Admin url
ADMIN_URL = os.getenv("FORMSFLOW_ADMIN_URL")


class DevConfig(_Config): # pylint: disable=too-few-public-methods
"""Development environment configuration."""
Expand Down
8 changes: 8 additions & 0 deletions forms-flow-api/src/formsflow_api/constants/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,14 @@ class BusinessErrorCode(ErrorCodeMixin, Enum):
"Can't delete the form that has submissions associated with it.",
HTTPStatus.BAD_REQUEST,
)
ADMIN_SERVICE_UNAVAILABLE = (
"Admin service is not available",
HTTPStatus.SERVICE_UNAVAILABLE,
)
INVALID_ADMIN_RESPONSE = (
"Invalid response received from admin service",
HTTPStatus.BAD_REQUEST,
)

def __new__(cls, message, status_code):
"""Constructor."""
Expand Down
29 changes: 29 additions & 0 deletions forms-flow-api/src/formsflow_api/services/external/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""This exposes the Admin API."""

import json

import requests
from flask import current_app
from formsflow_api_utils.exceptions import BusinessException
from formsflow_api_utils.utils import HTTP_TIMEOUT

from formsflow_api.constants import BusinessErrorCode


class AdminService: # pylint: disable=too-few-public-methods
"""This class manages external calls to admin api service."""

@classmethod
def get_request(cls, url, token):
"""Get HTTP request to Admin API with auth header."""
headers = {"Authorization": token, "content-type": "application/json"}
try:
response = requests.get(url, headers=headers, timeout=HTTP_TIMEOUT)
current_app.logger.debug(
"GET URL : %s, Response Code : %s", url, response.status_code
)
if response.ok:
return json.loads(response.text)
raise BusinessException(BusinessErrorCode.INVALID_ADMIN_RESPONSE)
except requests.ConnectionError as e:
raise BusinessException(BusinessErrorCode.ADMIN_SERVICE_UNAVAILABLE) from e
61 changes: 40 additions & 21 deletions forms-flow-api/src/formsflow_api/services/form_process_mapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,37 @@ def create_form(data, is_designer, **kwargs): # pylint:disable=too-many-locals
FormProcessMapperService.create_default_process(process_name)
return response

def _remove_tenant_key(self, form_json, tenant_key):
"""Remove tenant key from path & name."""
tenant_prefix = f"{tenant_key}-"
form_path = form_json.get("path", "")
form_name = form_json.get("name", "")
current_app.logger.info(
f"Removing tenant key from path: {form_path} & name: {form_name}"
)
if form_path.startswith(tenant_prefix):
form_json["path"] = form_path[len(tenant_prefix) :]

if form_name.startswith(tenant_prefix):
form_json["name"] = form_name[len(tenant_prefix) :]
return form_json

def _sanitize_form_json(self, form_json, tenant_key):
"""Clean form JSON data for export."""
keys_to_remove = [
"_id",
"machineName",
"access",
"submissionAccess",
"parentFormId",
]
for key in keys_to_remove:
form_json.pop(key, None)
# Remove 'tenantkey-' from 'path' and 'name'
if current_app.config.get("MULTI_TENANCY_ENABLED"):
form_json = self._remove_tenant_key(form_json, tenant_key)
return form_json

def _get_form( # pylint: disable=too-many-arguments, too-many-positional-arguments
self,
title_or_path: str,
Expand All @@ -484,27 +515,15 @@ def _get_form( # pylint: disable=too-many-arguments, too-many-positional-argume
form_json = formio_service.get_form_by_path(
title_or_path, form_io_token
)
if form_json:
form_json.pop("_id", None)
form_json.pop("machineName", None)
# In a (sub form)connected form, the workflow provides the form path,
# and the title is obtained from the form JSON
title_or_path = (
form_json.get("title", "") if scope_type == "sub" else title_or_path
)
# Remove 'tenantkey-' from 'path' and 'name'
if current_app.config.get("MULTI_TENANCY_ENABLED"):
tenant_prefix = f"{tenant_key}-"
form_path = form_json.get("path", "")
form_name = form_json.get("name", "")
current_app.logger.info(
f"Removing tenant key from path: {form_path} & name: {form_name}"
)
if form_path.startswith(tenant_prefix):
form_json["path"] = form_path[len(tenant_prefix) :]
if not form_json:
raise BusinessException(BusinessErrorCode.INVALID_FORM_ID)
# In a (sub form)connected form, the workflow provides the form path,
# and the title is obtained from the form JSON
title_or_path = (
form_json.get("title", "") if scope_type == "sub" else title_or_path
)
form_json = self._sanitize_form_json(form_json, tenant_key)

if form_name.startswith(tenant_prefix):
form_json["name"] = form_name[len(tenant_prefix) :]
return {
"formTitle": title_or_path,
"formDescription": description,
Expand Down Expand Up @@ -676,7 +695,7 @@ def export( # pylint:disable=too-many-locals
mapper.process_key, mapper.process_name, "main"
)
workflows.append(workflow)
authorizations.append(self._get_authorizations(mapper.form_id, user))
authorizations.append(self._get_authorizations(mapper.parent_form_id, user))

# Parse bpm xml to get subforms & workflows
# The following lines are currently commented out but may be needed for future use.
Expand Down
109 changes: 107 additions & 2 deletions forms-flow-api/src/formsflow_api/services/import_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
from flask import current_app
from formsflow_api_utils.exceptions import BusinessException
from formsflow_api_utils.services.external import FormioService
from formsflow_api_utils.utils import Cache
from formsflow_api_utils.utils.enums import FormProcessMapperStatus
from formsflow_api_utils.utils.startup import collect_role_ids
from formsflow_api_utils.utils.user_context import UserContext, user_context
from jsonschema import ValidationError, validate
from lxml import etree
Expand All @@ -19,6 +21,7 @@
form_schema,
form_workflow_schema,
)
from formsflow_api.services.external.admin import AdminService

from .authorization import AuthorizationService
from .form_history_logs import FormHistoryService
Expand All @@ -38,6 +41,97 @@ def __get_formio_access_token(self):
"""Returns formio access token."""
return self.formio.get_formio_access_token()

def append_tenant_key_form_name_path(self, form_json, tenant_key):
"""Append tenant key to form name & path."""
name = form_json.get("name")
path = form_json.get("path")
current_app.logger.debug(
f"Appending tenant key: {tenant_key} to form name: {name} & path: {path}.."
)
form_json["name"] = f"{tenant_key}-{name}"
form_json["path"] = f"{tenant_key}-{path}"
return form_json

@user_context
def set_form_and_submission_access(self, form_data, anonymous, **kwargs):
"""Add form and submission access to form."""
if current_app.config.get("MULTI_TENANCY_ENABLED"):
user: UserContext = kwargs["user"]
url = f"{current_app.config.get('ADMIN_URL')}/tenant"
current_app.logger.debug(f"Admin url: {url}")
response = AdminService.get_request(url, user.bearer_token)
role_ids = response["form"]
else:
role_ids = Cache.get("formio_role_ids")
if not role_ids:
collect_role_ids(current_app)
role_ids = Cache.get("formio_role_ids")

role_dict = {role["type"]: role["roleId"] for role in role_ids}

client_id = role_dict.get("CLIENT")
designer_id = role_dict.get("DESIGNER")
reviewer_id = role_dict.get("REVIEWER")
anonymous_id = role_dict.get("ANONYMOUS")

# Include anonymous_id, if anonymous is True
read_all_roles = [client_id, designer_id, reviewer_id]
create_own_roles = [client_id]
if anonymous:
read_all_roles.append(anonymous_id)
create_own_roles.append(anonymous_id)

form_data["access"] = [
{
"type": "read_all",
"roles": read_all_roles,
},
{
"type": "update_all",
"roles": [designer_id],
},
{
"type": "delete_all",
"roles": [designer_id],
},
]

form_data["submissionAccess"] = [
{
"roles": [designer_id],
"type": "create_all",
},
{
"roles": [reviewer_id],
"type": "read_all",
},
{
"roles": [reviewer_id],
"type": "update_all",
},
{
"roles": [designer_id, reviewer_id],
"type": "delete_all",
},
{
"roles": create_own_roles,
"type": "create_own",
},
{
"roles": [client_id],
"type": "read_own",
},
{
"roles": [client_id],
"type": "update_own",
},
{
"roles": [reviewer_id],
"type": "delete_own",
},
]
return form_data

def create_authorization(self, data):
"""Create authorization."""
for auth_type in AuthType:
Expand Down Expand Up @@ -285,6 +379,8 @@ def import_new_form_workflow(
self, file_data, form_json, workflow_data, process_type
):
"""Import new form+workflow."""
anonymous = file_data.get("forms")[0].get("anonymous") or False
form_json = self.set_form_and_submission_access(form_json, anonymous)
form_response = self.form_create(form_json)
form_id = form_response.get("_id")
FormHistoryService.create_form_log_with_clone(
Expand Down Expand Up @@ -350,7 +446,7 @@ def import_form(
current_form = self.get_form_by_formid(mapper.form_id)
new_path = form_json.get("path")
new_title = form_json.get("title")
anonymous = kwargs.get("anonymous", None)
anonymous = kwargs.get("anonymous", False)
description = kwargs.get("description", None)
title_changed = bool(not form_only and mapper.form_name != new_title)
anonymous_changed = bool(
Expand All @@ -377,6 +473,7 @@ def import_form(
# But incase of form only no validation done, so use current form path & title itself.
form_json["title"] = title if form_only else new_title
form_json["path"] = path if form_only else new_path
form_json = self.set_form_and_submission_access(form_json, anonymous)
form_response = self.form_create(form_json)
form_id = form_response.get("_id")
FormHistoryService.create_form_log_with_clone(
Expand Down Expand Up @@ -477,7 +574,7 @@ def import_form_workflow(
if action not in ["validate", "import"]:
raise BusinessException(BusinessErrorCode.INVALID_INPUT)

if import_type == "new":
if import_type == "new": # pylint: disable=too-many-nested-blocks
current_app.logger.info("Import new processing..")
# Validate input file type whether it is json
if not self.validate_file_type(file.filename, (".json",)):
Expand All @@ -499,6 +596,10 @@ def import_form_workflow(
form_major=1, form_minor=0, workflow_major=1, workflow_minor=0
)
if action == "import":
if current_app.config.get("MULTI_TENANCY_ENABLED"):
form_json = self.append_tenant_key_form_name_path(
form_json, tenant_key
)
form_id = self.import_new_form_workflow(
file_data, form_json, workflow_data, process_type
)
Expand Down Expand Up @@ -575,6 +676,10 @@ def import_form_workflow(

if not skip_form:
# Import form
if current_app.config.get("MULTI_TENANCY_ENABLED"):
form_json = self.append_tenant_key_form_name_path(
form_json, tenant_key
)
form_id = self.import_edit_form(
file_data, selected_form_version, form_json, mapper
)
Expand Down

0 comments on commit e504a01

Please sign in to comment.