From 252216b8237ddf88405ac35af18ea440137be248 Mon Sep 17 00:00:00 2001 From: jbirddog <100367399+jbirddog@users.noreply.github.com> Date: Thu, 25 May 2023 10:30:01 -0400 Subject: [PATCH 1/5] Custom start event (#274) --- .../services/custom_parser.py | 4 + .../services/process_instance_processor.py | 4 + .../services/process_instance_service.py | 15 +- .../services/workflow_service.py | 39 +++++ .../spiffworkflow_backend/specs/__init__.py | 1 + .../specs/start_event.py | 62 ++++++++ .../unit/test_workflow_service.py | 135 ++++++++++++++++++ 7 files changed, 259 insertions(+), 1 deletion(-) create mode 100644 spiffworkflow-backend/src/spiffworkflow_backend/services/workflow_service.py create mode 100644 spiffworkflow-backend/src/spiffworkflow_backend/specs/__init__.py create mode 100644 spiffworkflow-backend/src/spiffworkflow_backend/specs/start_event.py create mode 100644 spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_workflow_service.py diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/custom_parser.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/custom_parser.py index c54c195f..20afce1f 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/custom_parser.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/custom_parser.py @@ -2,9 +2,13 @@ from SpiffWorkflow.dmn.parser.BpmnDmnParser import BpmnDmnParser # type: ignore from SpiffWorkflow.spiff.parser.process import SpiffBpmnParser # type: ignore +from spiffworkflow_backend.specs.start_event import StartEvent + class MyCustomParser(BpmnDmnParser): # type: ignore """A BPMN and DMN parser that can also parse spiffworkflow-specific extensions.""" OVERRIDE_PARSER_CLASSES = BpmnDmnParser.OVERRIDE_PARSER_CLASSES OVERRIDE_PARSER_CLASSES.update(SpiffBpmnParser.OVERRIDE_PARSER_CLASSES) + + StartEvent.register_parser_class(OVERRIDE_PARSER_CLASSES) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py index 29cbab04..d713af7d 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py @@ -101,7 +101,11 @@ from spiffworkflow_backend.services.workflow_execution_service import ( from spiffworkflow_backend.services.workflow_execution_service import ( WorkflowExecutionService, ) +from spiffworkflow_backend.specs.start_event import ( + StartEvent, +) +StartEvent.register_converter(SPIFF_SPEC_CONFIG) # Sorry about all this crap. I wanted to move this thing to another file, but # importing a bunch of types causes circular imports. diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_service.py index ac9b10a3..a2364a92 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_service.py @@ -46,6 +46,7 @@ from spiffworkflow_backend.services.process_instance_queue_service import ( ProcessInstanceQueueService, ) from spiffworkflow_backend.services.process_model_service import ProcessModelService +from spiffworkflow_backend.services.workflow_service import WorkflowService class ProcessInstanceService: @@ -54,6 +55,17 @@ class ProcessInstanceService: FILE_DATA_DIGEST_PREFIX = "spifffiledatadigest+" TASK_STATE_LOCKED = "locked" + @staticmethod + def calculate_start_delay_in_seconds(process_instance_model: ProcessInstanceModel) -> int: + try: + processor = ProcessInstanceProcessor(process_instance_model) + delay_in_seconds = WorkflowService.calculate_run_at_delay_in_seconds( + processor.bpmn_process_instance, datetime.now(timezone.utc) + ) + except Exception: + delay_in_seconds = 0 + return delay_in_seconds + @classmethod def create_process_instance( cls, @@ -77,7 +89,8 @@ class ProcessInstanceService: ) db.session.add(process_instance_model) db.session.commit() - run_at_in_seconds = round(time.time()) + delay_in_seconds = cls.calculate_start_delay_in_seconds(process_instance_model) + run_at_in_seconds = round(time.time()) + delay_in_seconds ProcessInstanceQueueService.enqueue_new_process_instance(process_instance_model, run_at_in_seconds) return process_instance_model diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/workflow_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/workflow_service.py new file mode 100644 index 00000000..9965484f --- /dev/null +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/workflow_service.py @@ -0,0 +1,39 @@ +"""workflow_service.""" +from datetime import datetime + +from SpiffWorkflow.bpmn.workflow import BpmnWorkflow # type: ignore +from SpiffWorkflow.task import Task as SpiffTask # type: ignore +from SpiffWorkflow.task import TaskState + +from spiffworkflow_backend.specs.start_event import StartEvent + + +class WorkflowService: + """WorkflowService.""" + + @classmethod + def future_start_events(cls, workflow: BpmnWorkflow) -> list[SpiffTask]: + return [t for t in workflow.get_tasks(TaskState.FUTURE) if isinstance(t.task_spec, StartEvent)] + + @classmethod + def next_start_event_delay_in_seconds(cls, workflow: BpmnWorkflow, now_in_utc: datetime) -> int: + start_events = cls.future_start_events(workflow) + start_delays: list[int] = [] + for start_event in start_events: + start_delay = start_event.task_spec.start_delay_in_seconds(start_event, now_in_utc) + start_delays.append(start_delay) + start_delays.sort() + return start_delays[0] if len(start_delays) > 0 else 0 + + @classmethod + def calculate_run_at_delay_in_seconds(cls, workflow: BpmnWorkflow, now_in_utc: datetime) -> int: + # TODO: for now we are using the first start time because I am not sure how multiple + # start events should work. I think the right answer is to take the earliest start + # time and have later start events stay FUTURE/WAITING?, then we need to be able + # to respect the other start events when enqueue'ing. + # + # TODO: this method should also expand to include other FUTURE/WAITING timers when + # enqueue'ing so that we don't have to check timers every 10 or whatever seconds + # right now we assume that this is being called to create a process + + return cls.next_start_event_delay_in_seconds(workflow, now_in_utc) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/specs/__init__.py b/spiffworkflow-backend/src/spiffworkflow_backend/specs/__init__.py new file mode 100644 index 00000000..734641eb --- /dev/null +++ b/spiffworkflow-backend/src/spiffworkflow_backend/specs/__init__.py @@ -0,0 +1 @@ +"""docstring.""" diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/specs/start_event.py b/spiffworkflow-backend/src/spiffworkflow_backend/specs/start_event.py new file mode 100644 index 00000000..bcd5a14b --- /dev/null +++ b/spiffworkflow-backend/src/spiffworkflow_backend/specs/start_event.py @@ -0,0 +1,62 @@ +from datetime import datetime +from typing import Any +from typing import Dict + +from SpiffWorkflow.bpmn.parser.util import full_tag # type: ignore +from SpiffWorkflow.bpmn.serializer.task_spec import EventConverter # type: ignore +from SpiffWorkflow.bpmn.serializer.task_spec import StartEventConverter as DefaultStartEventConverter +from SpiffWorkflow.bpmn.specs.defaults import StartEvent as DefaultStartEvent # type: ignore +from SpiffWorkflow.bpmn.specs.event_definitions import CycleTimerEventDefinition # type: ignore +from SpiffWorkflow.bpmn.specs.event_definitions import DurationTimerEventDefinition +from SpiffWorkflow.bpmn.specs.event_definitions import NoneEventDefinition +from SpiffWorkflow.bpmn.specs.event_definitions import TimeDateEventDefinition +from SpiffWorkflow.bpmn.specs.event_definitions import TimerEventDefinition +from SpiffWorkflow.spiff.parser.event_parsers import SpiffStartEventParser # type: ignore +from SpiffWorkflow.task import Task as SpiffTask # type: ignore + + +# TODO: cylce timers and repeat counts? +class StartEvent(DefaultStartEvent): # type: ignore + def __init__(self, wf_spec, bpmn_id, event_definition, **kwargs): # type: ignore + if isinstance(event_definition, TimerEventDefinition): + super().__init__(wf_spec, bpmn_id, NoneEventDefinition(), **kwargs) + self.timer_definition = event_definition + else: + super().__init__(wf_spec, bpmn_id, event_definition, **kwargs) + self.timer_definition = None + + @staticmethod + def register_converter(spec_config: Dict[str, Any]) -> None: + spec_config["task_specs"].remove(DefaultStartEventConverter) + spec_config["task_specs"].append(StartEventConverter) + + @staticmethod + def register_parser_class(parser_config: Dict[str, Any]) -> None: + parser_config[full_tag("startEvent")] = (SpiffStartEventParser, StartEvent) + + def start_delay_in_seconds(self, my_task: SpiffTask, now_in_utc: datetime) -> int: + script_engine = my_task.workflow.script_engine + evaluated_expression = None + parsed_duration = None + + if isinstance(self.timer_definition, TimerEventDefinition) and script_engine is not None: + evaluated_expression = script_engine.evaluate(my_task, self.timer_definition.expression) + + if evaluated_expression is not None: + if isinstance(self.timer_definition, TimeDateEventDefinition): + parsed_duration = TimerEventDefinition.parse_time_or_duration(evaluated_expression) + time_delta = parsed_duration - now_in_utc + return time_delta.seconds # type: ignore + elif isinstance(self.timer_definition, DurationTimerEventDefinition): + parsed_duration = TimerEventDefinition.parse_iso_duration(evaluated_expression) + time_delta = TimerEventDefinition.get_timedelta_from_start(parsed_duration, now_in_utc) + return time_delta.seconds # type: ignore + elif isinstance(self.timer_definition, CycleTimerEventDefinition): + return 0 + + return 0 + + +class StartEventConverter(EventConverter): # type: ignore + def __init__(self, registry): # type: ignore + super().__init__(StartEvent, registry) diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_workflow_service.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_workflow_service.py new file mode 100644 index 00000000..c645c9a3 --- /dev/null +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_workflow_service.py @@ -0,0 +1,135 @@ +"""Test_workflow_service.""" +from datetime import datetime +from datetime import timedelta +from datetime import timezone +from typing import Generator + +import pytest +from SpiffWorkflow.bpmn.workflow import BpmnWorkflow # type: ignore +from SpiffWorkflow.dmn.parser.BpmnDmnParser import BpmnDmnParser # type: ignore +from SpiffWorkflow.spiff.parser.process import SpiffBpmnParser # type: ignore +from tests.spiffworkflow_backend.helpers.base_test import BaseTest + +from spiffworkflow_backend.services.workflow_service import ( + WorkflowService, +) +from spiffworkflow_backend.specs.start_event import StartEvent + +BPMN_WRAPPER = """ + + {} + +""" + + +@pytest.fixture() +def now_in_utc() -> Generator[datetime, None, None]: + yield datetime.now(timezone.utc) + + +@pytest.fixture() +def example_start_datetime_in_utc_str() -> Generator[str, None, None]: + yield "2019-10-01T12:00:00+00:00" + + +@pytest.fixture() +def example_start_datetime_minus_5_mins_in_utc( + example_start_datetime_in_utc_str: str, +) -> Generator[datetime, None, None]: + example_datetime = datetime.fromisoformat(example_start_datetime_in_utc_str) + yield example_datetime - timedelta(minutes=5) + + +class CustomBpmnDmnParser(BpmnDmnParser): # type: ignore + OVERRIDE_PARSER_CLASSES = {} + OVERRIDE_PARSER_CLASSES.update(BpmnDmnParser.OVERRIDE_PARSER_CLASSES) + OVERRIDE_PARSER_CLASSES.update(SpiffBpmnParser.OVERRIDE_PARSER_CLASSES) + + StartEvent.register_parser_class(OVERRIDE_PARSER_CLASSES) + + +def workflow_from_str(bpmn_str: str, process_id: str) -> BpmnWorkflow: + parser = CustomBpmnDmnParser() + parser.add_bpmn_str(bpmn_str) + top_level = parser.get_spec(process_id) + subprocesses = parser.get_subprocess_specs(process_id) + return BpmnWorkflow(top_level, subprocesses) + + +def workflow_from_fragment(bpmn_fragment: str, process_id: str) -> BpmnWorkflow: + return workflow_from_str(BPMN_WRAPPER.format(bpmn_fragment), process_id) + + +class TestWorkflowService(BaseTest): + """TestWorkflowService.""" + + def test_run_at_delay_is_0_for_regular_start_events(self, now_in_utc: datetime) -> None: + workflow = workflow_from_fragment( + """ + + + Flow_184umot + + + Flow_184umot + + + + """, + "no_tasks", + ) + delay = WorkflowService.calculate_run_at_delay_in_seconds(workflow, now_in_utc) + assert delay == 0 + + def test_run_at_delay_is_30_for_30_second_duration_start_timer_event(self, now_in_utc: datetime) -> None: + workflow = workflow_from_fragment( + """ + + + Flow_1x1o335 + + "PT30S" + + + + + Flow_1x1o335 + + + """, + "Process_aldvgey", + ) + delay = WorkflowService.calculate_run_at_delay_in_seconds(workflow, now_in_utc) + assert delay == 30 + + def test_run_at_delay_is_300_if_5_mins_before_date_start_timer_event( + self, example_start_datetime_in_utc_str: str, example_start_datetime_minus_5_mins_in_utc: datetime + ) -> None: + workflow = workflow_from_fragment( + f""" + + + Flow_1x1o335 + + "{example_start_datetime_in_utc_str}" + + + + + Flow_1x1o335 + + + """, + "Process_aldvgey", + ) + delay = WorkflowService.calculate_run_at_delay_in_seconds(workflow, example_start_datetime_minus_5_mins_in_utc) + assert delay == 300 From 7b2c7c5f52f8bab85736bb45953ae7898a371f9a Mon Sep 17 00:00:00 2001 From: jasquat Date: Thu, 25 May 2023 12:00:24 -0400 Subject: [PATCH 2/5] allow disabling authentication from the backend w/ burnettk --- spiffworkflow-backend/bin/get_perms | 12 ++-- .../spiffworkflow_backend/config/default.py | 4 ++ .../src/spiffworkflow_backend/models/group.py | 7 ++- .../spiffworkflow_backend/models/principal.py | 2 + .../src/spiffworkflow_backend/models/user.py | 21 ++++--- .../src/spiffworkflow_backend/routes/user.py | 63 +++++++++++-------- .../services/user_service.py | 1 - .../src/components/NavigationBar.tsx | 22 ++++--- .../src/services/UserService.ts | 14 ++++- 9 files changed, 91 insertions(+), 55 deletions(-) diff --git a/spiffworkflow-backend/bin/get_perms b/spiffworkflow-backend/bin/get_perms index 9199efbc..5f561b01 100755 --- a/spiffworkflow-backend/bin/get_perms +++ b/spiffworkflow-backend/bin/get_perms @@ -14,13 +14,13 @@ fi # shellcheck disable=2016 mysql -uroot "$database" -e ' - select u.username user, g.identifier group + select u.username username, g.identifier group_name FROM `user` u - JOIN `user_group_assignment` uga on uga.user_id = u.id - JOIN `group` g on g.id = uga.group_id; + JOIN `user_group_assignment` uga ON uga.user_id = u.id + JOIN `group` g ON g.id = uga.group_id; select pa.id, g.identifier group_identifier, pt.uri, permission from permission_assignment pa - join principal p on p.id = pa.principal_id - join `group` g on g.id = p.group_id - join permission_target pt on pt.id = pa.permission_target_id; + JOIN principal p ON p.id = pa.principal_id + JOIN `group` g ON g.id = p.group_id + JOIN permission_target pt ON pt.id = pa.permission_target_id; ' diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/config/default.py b/spiffworkflow-backend/src/spiffworkflow_backend/config/default.py index 25cbbab2..63f1ec7b 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/config/default.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/config/default.py @@ -79,6 +79,10 @@ SPIFFWORKFLOW_BACKEND_OPEN_ID_TENANT_SPECIFIC_FIELDS = environ.get( "SPIFFWORKFLOW_BACKEND_OPEN_ID_TENANT_SPECIFIC_FIELDS" ) +SPIFFWORKFLOW_BACKEND_AUTHENTICATION_DISABLED = ( + environ.get("SPIFFWORKFLOW_BACKEND_AUTHENTICATION_DISABLED", default="false") == "true" +) + # loggers to use is a comma separated list of logger prefixes that we will be converted to list of strings SPIFFWORKFLOW_BACKEND_LOGGERS_TO_USE = environ.get("SPIFFWORKFLOW_BACKEND_LOGGERS_TO_USE") diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/group.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/group.py index 18ae2020..f4136f82 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/group.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/group.py @@ -15,13 +15,14 @@ if TYPE_CHECKING: from spiffworkflow_backend.models.user import UserModel # noqa: F401 +SPIFF_NO_AUTH_ANONYMOUS_GROUP = "spiff_anonymous_group" + + class GroupNotFoundError(Exception): - """GroupNotFoundError.""" + pass class GroupModel(SpiffworkflowBaseDBModel): - """GroupModel.""" - __tablename__ = "group" __table_args__ = {"extend_existing": True} diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/principal.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/principal.py index 6e46def5..31d4ccda 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/principal.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/principal.py @@ -32,3 +32,5 @@ class PrincipalModel(SpiffworkflowBaseDBModel): user = relationship("UserModel", viewonly=True) group = relationship("GroupModel", viewonly=True) + + permission_assignments = relationship("PermissionAssignmentModel", cascade="delete") # type: ignore diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/user.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/user.py index 1f667e0a..ad68faac 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/user.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/user.py @@ -15,14 +15,15 @@ from spiffworkflow_backend.models.db import SpiffworkflowBaseDBModel from spiffworkflow_backend.models.group import GroupModel +SPIFF_NO_AUTH_ANONYMOUS_USER = "spiff_anonymous_user" + + class UserNotFoundError(Exception): - """UserNotFoundError.""" + pass @dataclass class UserModel(SpiffworkflowBaseDBModel): - """UserModel.""" - __tablename__ = "user" __table_args__ = (db.UniqueConstraint("service", "service_id", name="service_key"),) @@ -47,9 +48,9 @@ class UserModel(SpiffworkflowBaseDBModel): secondary="user_group_assignment", overlaps="user_group_assignments,users", ) - principal = relationship("PrincipalModel", uselist=False) # type: ignore + principal = relationship("PrincipalModel", uselist=False, cascade="delete") # type: ignore - def encode_auth_token(self) -> str: + def encode_auth_token(self, extra_payload: dict | None = None) -> str: """Generate the Auth Token. :return: string @@ -59,12 +60,16 @@ class UserModel(SpiffworkflowBaseDBModel): raise KeyError("we need current_app.config to have a SECRET_KEY") # hours = float(app.config['TOKEN_AUTH_TTL_HOURS']) - payload = { - # 'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=hours, minutes=0, seconds=0), - # 'iat': datetime.datetime.utcnow(), + base_payload = { + "email": self.email, + "preferred_username": self.username, "sub": f"service:{self.service}::service_id:{self.service_id}", "token_type": "internal", } + + payload = base_payload + if extra_payload is not None: + payload = {**base_payload, **extra_payload} return jwt.encode( payload, secret_key, diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/user.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/user.py index e1fc02eb..134c360f 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/user.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/user.py @@ -6,7 +6,6 @@ import re from typing import Any from typing import Dict from typing import Optional -from typing import Union import flask import jwt @@ -20,6 +19,10 @@ from werkzeug.wrappers import Response from spiffworkflow_backend.exceptions.api_error import ApiError from spiffworkflow_backend.helpers.api_version import V1_API_PATH_PREFIX +from spiffworkflow_backend.models.db import db +from spiffworkflow_backend.models.group import GroupModel +from spiffworkflow_backend.models.group import SPIFF_NO_AUTH_ANONYMOUS_GROUP +from spiffworkflow_backend.models.user import SPIFF_NO_AUTH_ANONYMOUS_USER from spiffworkflow_backend.models.user import UserModel from spiffworkflow_backend.services.authentication_service import AuthenticationService from spiffworkflow_backend.services.authentication_service import ( @@ -27,6 +30,7 @@ from spiffworkflow_backend.services.authentication_service import ( ) from spiffworkflow_backend.services.authentication_service import TokenExpiredError from spiffworkflow_backend.services.authorization_service import AuthorizationService +from spiffworkflow_backend.services.group_service import GroupService from spiffworkflow_backend.services.user_service import UserService """ @@ -36,9 +40,7 @@ from spiffworkflow_backend.services.user_service import UserService # authorization_exclusion_list = ['status'] -def verify_token( - token: Optional[str] = None, force_run: Optional[bool] = False -) -> Optional[Dict[str, Optional[Union[str, int]]]]: +def verify_token(token: Optional[str] = None, force_run: Optional[bool] = False) -> None: """Verify the token for the user (if provided). If in production environment and token is not provided, gets user from the SSO headers and returns their token. @@ -82,6 +84,22 @@ def verify_token( current_app.logger.error( f"Exception in verify_token getting user from decoded internal token. {e}" ) + + # if the user is the anonymous user and we have auth enabled then make sure we clean up the anonymouse user + if ( + user_model + and not current_app.config.get("SPIFFWORKFLOW_BACKEND_AUTHENTICATION_DISABLED") + and user_model.username == SPIFF_NO_AUTH_ANONYMOUS_USER + and user_model.service_id == "spiff_anonymous_service_id" + ): + group_model = GroupModel.query.filter_by(identifier=SPIFF_NO_AUTH_ANONYMOUS_GROUP).first() + db.session.delete(group_model) + db.session.delete(user_model) + db.session.commit() + tld = current_app.config["THREAD_LOCAL_DATA"] + tld.user_has_logged_out = True + return None + elif "iss" in decoded_token.keys(): user_info = None try: @@ -196,29 +214,22 @@ def set_new_access_token_in_cookie( return response -def encode_auth_token(sub: str, token_type: Optional[str] = None) -> str: - """Generates the Auth Token. - - :return: string - """ - payload = {"sub": sub} - if token_type is None: - token_type = "internal" # noqa: S105 - payload["token_type"] = token_type - if "SECRET_KEY" in current_app.config: - secret_key = current_app.config.get("SECRET_KEY") - else: - current_app.logger.error("Missing SECRET_KEY in encode_auth_token") - raise ApiError(error_code="encode_error", message="Missing SECRET_KEY in encode_auth_token") - return jwt.encode( - payload, - str(secret_key), - algorithm="HS256", - ) - - def login(redirect_url: str = "/") -> Response: - """Login.""" + if current_app.config.get("SPIFFWORKFLOW_BACKEND_AUTHENTICATION_DISABLED"): + user = UserModel.query.filter_by(username=SPIFF_NO_AUTH_ANONYMOUS_USER).first() + if user is None: + user = UserService.create_user( + SPIFF_NO_AUTH_ANONYMOUS_USER, "spiff_anonymous_service", "spiff_anonymous_service_id" + ) + GroupService.add_user_to_group_or_add_to_waiting(user.username, SPIFF_NO_AUTH_ANONYMOUS_GROUP) + AuthorizationService.add_permission_from_uri_or_macro(SPIFF_NO_AUTH_ANONYMOUS_GROUP, "all", "/*") + g.user = user + g.token = user.encode_auth_token({"authentication_disabled": True}) + tld = current_app.config["THREAD_LOCAL_DATA"] + tld.new_access_token = g.token + tld.new_id_token = g.token + return redirect(redirect_url) + state = AuthenticationService.generate_state(redirect_url) login_redirect_url = AuthenticationService().get_login_redirect_url(state.decode("UTF-8")) return redirect(login_redirect_url) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/user_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/user_service.py index 2ddc861a..3cca074d 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/user_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/user_service.py @@ -33,7 +33,6 @@ class UserService: tenant_specific_field_2: Optional[str] = None, tenant_specific_field_3: Optional[str] = None, ) -> UserModel: - """Create_user.""" user_model: Optional[UserModel] = ( UserModel.query.filter(UserModel.service == service).filter(UserModel.service_id == service_id).first() ) diff --git a/spiffworkflow-frontend/src/components/NavigationBar.tsx b/spiffworkflow-frontend/src/components/NavigationBar.tsx index b9301d9f..f532d310 100644 --- a/spiffworkflow-frontend/src/components/NavigationBar.tsx +++ b/spiffworkflow-frontend/src/components/NavigationBar.tsx @@ -120,15 +120,19 @@ export default function NavigationBar() { Documentation -
- + {!UserService.authenticationDisabled() ? ( + <> +
+ + + ) : null} diff --git a/spiffworkflow-frontend/src/services/UserService.ts b/spiffworkflow-frontend/src/services/UserService.ts index 23266c3e..8a97a845 100644 --- a/spiffworkflow-frontend/src/services/UserService.ts +++ b/spiffworkflow-frontend/src/services/UserService.ts @@ -62,6 +62,15 @@ const getUserEmail = () => { return null; }; +const authenticationDisabled = () => { + const idToken = getIdToken(); + if (idToken) { + const idObject = jwt(idToken); + return (idObject as any).authentication_disabled; + } + return false; +}; + const getPreferredUsername = () => { const idToken = getIdToken(); if (idToken) { @@ -82,14 +91,15 @@ const hasRole = (_roles: string[]): boolean => { }; const UserService = { + authenticationDisabled, doLogin, doLogout, - isLoggedIn, getAccessToken, - loginIfNeeded, getPreferredUsername, getUserEmail, hasRole, + isLoggedIn, + loginIfNeeded, }; export default UserService; From f3bb6552290405024f93e5618f896a10489c802b Mon Sep 17 00:00:00 2001 From: jbirddog <100367399+jbirddog@users.noreply.github.com> Date: Thu, 25 May 2023 15:15:58 -0400 Subject: [PATCH 3/5] Editor compose (#279) --- .gitignore | 3 +- bin/run_editor | 9 ++++++ bin/stop_editor | 3 ++ bin/update_editor | 3 ++ editor.docker-compose.yml | 66 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 83 insertions(+), 1 deletion(-) create mode 100755 bin/run_editor create mode 100755 bin/stop_editor create mode 100755 bin/update_editor create mode 100644 editor.docker-compose.yml diff --git a/.gitignore b/.gitignore index 570e3759..c04d3d11 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ t .dccache version_info.json .coverage* -UNKNOWN.egg-info/ \ No newline at end of file +UNKNOWN.egg-info/ +process_models/ \ No newline at end of file diff --git a/bin/run_editor b/bin/run_editor new file mode 100755 index 00000000..bd22bfcf --- /dev/null +++ b/bin/run_editor @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +SPIFF_EDITOR_BPMN_SPEC_DIR=$1 \ +docker compose -f editor.docker-compose.yml up -d + +echo "" +echo "Spiff Editor is ready." +echo "" +echo "Please open ${SPIFFWORKFLOW_BACKEND_URL_FOR_FRONTEND:-http://localhost:${SPIFFWORKFLOW_FRONTEND_PORT:-8001}}" \ No newline at end of file diff --git a/bin/stop_editor b/bin/stop_editor new file mode 100755 index 00000000..f995b9cd --- /dev/null +++ b/bin/stop_editor @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +docker compose -f editor.docker-compose.yml down diff --git a/bin/update_editor b/bin/update_editor new file mode 100755 index 00000000..45b145e8 --- /dev/null +++ b/bin/update_editor @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +docker compose -f editor.docker-compose.yml pull diff --git a/editor.docker-compose.yml b/editor.docker-compose.yml new file mode 100644 index 00000000..9270c686 --- /dev/null +++ b/editor.docker-compose.yml @@ -0,0 +1,66 @@ +services: + spiffworkflow-frontend: + container_name: spiffworkflow-frontend + image: ghcr.io/sartography/spiffworkflow-frontend:main-latest + depends_on: + spiffworkflow-backend: + condition: service_healthy + environment: + APPLICATION_ROOT: "/" + PORT0: "${SPIFFWORKFLOW_FRONTEND_PORT:-8001}" + ports: + - "${SPIFFWORKFLOW_FRONTEND_PORT:-8001}:${SPIFFWORKFLOW_FRONTEND_PORT:-8001}/tcp" + + spiffworkflow-backend: + container_name: spiffworkflow-backend + image: ghcr.io/sartography/spiffworkflow-backend:main-latest + environment: + SPIFFWORKFLOW_BACKEND_APPLICATION_ROOT: "/" + SPIFFWORKFLOW_BACKEND_ENV: "local_development" + FLASK_DEBUG: "0" + FLASK_SESSION_SECRET_KEY: "${FLASK_SESSION_SECRET_KEY:-super_secret_key}" + # WARNING: Frontend is a static site which assumes frontend port - 1 on localhost. + SPIFFWORKFLOW_BACKEND_URL: "http://localhost:${SPIFF_BACKEND_PORT:-8000}" + + SPIFFWORKFLOW_BACKEND_BPMN_SPEC_ABSOLUTE_DIR: "/app/process_models" + SPIFFWORKFLOW_BACKEND_CONNECTOR_PROXY_URL: "http://spiffworkflow-connector:8004" + SPIFFWORKFLOW_BACKEND_DATABASE_TYPE: "sqlite" + SPIFFWORKFLOW_BACKEND_LOAD_FIXTURE_DATA: "false" + SPIFFWORKFLOW_BACKEND_OPEN_ID_CLIENT_ID: "spiffworkflow-backend" + SPIFFWORKFLOW_BACKEND_OPEN_ID_CLIENT_SECRET_KEY: "my_open_id_secret_key" + SPIFFWORKFLOW_BACKEND_OPEN_ID_SERVER_URL: "http://localhost:${SPIFF_BACKEND_PORT:-8000}/openid" + SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME: "example.yml" + SPIFFWORKFLOW_BACKEND_PORT: "${SPIFF_BACKEND_PORT:-8000}" + SPIFFWORKFLOW_BACKEND_RUN_BACKGROUND_SCHEDULER: "false" + SPIFFWORKFLOW_BACKEND_UPGRADE_DB: "true" + SPIFFWORKFLOW_BACKEND_URL_FOR_FRONTEND: "http://localhost:${SPIFFWORKFLOW_FRONTEND_PORT:-8001}" + ports: + - "${SPIFF_BACKEND_PORT:-8000}:${SPIFF_BACKEND_PORT:-8000}/tcp" + volumes: + - "${SPIFF_EDITOR_BPMN_SPEC_DIR:-./process_models}:/app/process_models" + - ./log:/app/log + healthcheck: + test: "curl localhost:${SPIFF_BACKEND_PORT:-8000}/v1.0/status --fail" + interval: 10s + timeout: 5s + retries: 20 + + spiffworkflow-connector: + container_name: spiffworkflow-connector + image: ghcr.io/sartography/connector-proxy-demo:latest + environment: + FLASK_ENV: "${FLASK_ENV:-development}" + FLASK_DEBUG: "0" + FLASK_SESSION_SECRET_KEY: "${FLASK_SESSION_SECRET_KEY:-super_secret_key}" + CONNECTOR_PROXY_PORT: "${SPIFF_CONNECTOR_PORT:-8004}" + ports: + - "${SPIFF_CONNECTOR_PORT:-8004}:${SPIFF_CONNECTOR_PORT:-8004}/tcp" + healthcheck: + test: "curl localhost:${SPIFF_CONNECTOR_PORT:-8004}/liveness --fail" + interval: 10s + timeout: 5s + retries: 20 + +volumes: + spiffworkflow_backend: + driver: local From c58ae522438b602ceaf9d4802fc6c28f06e269a2 Mon Sep 17 00:00:00 2001 From: burnettk Date: Thu, 25 May 2023 17:00:02 -0400 Subject: [PATCH 4/5] force a good directory as arg 1 --- bin/run_editor | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/bin/run_editor b/bin/run_editor index bd22bfcf..a95faf1b 100755 --- a/bin/run_editor +++ b/bin/run_editor @@ -1,9 +1,28 @@ #!/usr/bin/env bash +#!/usr/bin/env bash + +function error_handler() { + >&2 echo "Exited with BAD EXIT CODE '${2}' in ${0} script at line: ${1}." + exit "$2" +} +trap 'error_handler ${LINENO} $?' ERR +set -o errtrace -o errexit -o nounset -o pipefail + +if [[ -z "${1:-}" ]]; then + >&2 echo "usage: $(basename "$0") [SPIFF_EDITOR_BPMN_SPEC_DIR]" + exit 1 +fi + +if [[ ! -d "$1" ]]; then + >&2 echo "ERROR: the first argument must be a directory." + exit 1 +fi + SPIFF_EDITOR_BPMN_SPEC_DIR=$1 \ docker compose -f editor.docker-compose.yml up -d echo "" echo "Spiff Editor is ready." echo "" -echo "Please open ${SPIFFWORKFLOW_BACKEND_URL_FOR_FRONTEND:-http://localhost:${SPIFFWORKFLOW_FRONTEND_PORT:-8001}}" \ No newline at end of file +echo "Please open ${SPIFFWORKFLOW_BACKEND_URL_FOR_FRONTEND:-http://localhost:${SPIFFWORKFLOW_FRONTEND_PORT:-8001}}" From 81487328dc79624664f59a908fedc8c253d76ebc Mon Sep 17 00:00:00 2001 From: burnettk Date: Fri, 26 May 2023 07:16:28 -0400 Subject: [PATCH 5/5] allow everything to get overidden --- .../config/terraform_deployed_environment.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/config/terraform_deployed_environment.py b/spiffworkflow-backend/src/spiffworkflow_backend/config/terraform_deployed_environment.py index 985047b7..e1b85751 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/config/terraform_deployed_environment.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/config/terraform_deployed_environment.py @@ -28,12 +28,17 @@ SPIFFWORKFLOW_BACKEND_OPEN_ID_SERVER_URL = environ.get( ), ) -SPIFFWORKFLOW_BACKEND_URL_FOR_FRONTEND = ( - f"https://{environment_identifier_for_this_config_file_only}.spiffworkflow.org" +SPIFFWORKFLOW_BACKEND_URL_FOR_FRONTEND = environ.get( + "SPIFFWORKFLOW_BACKEND_URL_FOR_FRONTEND", + default=f"https://{environment_identifier_for_this_config_file_only}.spiffworkflow.org", ) -SPIFFWORKFLOW_BACKEND_URL = f"https://api.{environment_identifier_for_this_config_file_only}.spiffworkflow.org" -SPIFFWORKFLOW_BACKEND_CONNECTOR_PROXY_URL = ( - f"https://connector-proxy.{environment_identifier_for_this_config_file_only}.spiffworkflow.org" +SPIFFWORKFLOW_BACKEND_URL = environ.get( + "SPIFFWORKFLOW_BACKEND_URL", + default=f"https://api.{environment_identifier_for_this_config_file_only}.spiffworkflow.org", +) +SPIFFWORKFLOW_BACKEND_CONNECTOR_PROXY_URL = environ.get( + "SPIFFWORKFLOW_BACKEND_CONNECTOR_PROXY_URL", + default=f"https://connector-proxy.{environment_identifier_for_this_config_file_only}.spiffworkflow.org", ) SPIFFWORKFLOW_BACKEND_GIT_PUBLISH_CLONE_URL = environ.get( "SPIFFWORKFLOW_BACKEND_GIT_PUBLISH_CLONE_URL",