bugfix/guest-login-multiple-auths (#782)

* This fixes guest login with using multiple auths, removes empty items from ApiError, and raises if redirect_url given to login does not match expected frontend host w/ burnettk

* get body for debug

* try to get the logs from the correct place to upload w/ burnettk

* mock the openid call instead of actually calling it w/ burnettk

---------

Co-authored-by: jasquat <jasquat@users.noreply.github.com>
Co-authored-by: burnettk <burnettk@users.noreply.github.com>
This commit is contained in:
jasquat 2023-11-30 13:51:01 -05:00 committed by GitHub
parent ac0c83d695
commit 1627f5dd07
41 changed files with 226 additions and 86 deletions

View File

@ -74,7 +74,6 @@ jobs:
env: env:
FLASK_SESSION_SECRET_KEY: super_secret_key FLASK_SESSION_SECRET_KEY: super_secret_key
FORCE_COLOR: "1" FORCE_COLOR: "1"
NOXSESSION: ${{ matrix.session }}
PRE_COMMIT_COLOR: "always" PRE_COMMIT_COLOR: "always"
SPIFFWORKFLOW_BACKEND_DATABASE_PASSWORD: password SPIFFWORKFLOW_BACKEND_DATABASE_PASSWORD: password
SPIFFWORKFLOW_BACKEND_DATABASE_TYPE: ${{ matrix.database }} SPIFFWORKFLOW_BACKEND_DATABASE_TYPE: ${{ matrix.database }}
@ -171,7 +170,7 @@ jobs:
uses: "actions/upload-artifact@v3" uses: "actions/upload-artifact@v3"
with: with:
name: logs-${{matrix.python}}-${{matrix.os}}-${{matrix.database}} name: logs-${{matrix.python}}-${{matrix.os}}-${{matrix.database}}
path: "./log/*.log" path: "./spiffworkflow-backend/log/*.log"
# burnettk created an account at https://app.snyk.io/org/kevin-jfx # burnettk created an account at https://app.snyk.io/org/kevin-jfx
# and added his SNYK_TOKEN secret under the spiff-arena repo. # and added his SNYK_TOKEN secret under the spiff-arena repo.

View File

@ -24,8 +24,9 @@ def main(process_instance_id: str) -> None:
if not process_instance: if not process_instance:
raise Exception(f"Could not find a process instance with id: {process_instance_id}") raise Exception(f"Could not find a process instance with id: {process_instance_id}")
bpmn_process_dict = ProcessInstanceProcessor._get_full_bpmn_process_dict(process_instance, {})
with open(file_path, "w", encoding="utf-8") as f: with open(file_path, "w", encoding="utf-8") as f:
f.write(json.dumps(ProcessInstanceProcessor._get_full_bpmn_json(process_instance))) f.write(json.dumps(bpmn_process_dict, indent=2))
print(f"Saved to {file_path}") print(f"Saved to {file_path}")

View File

@ -2285,6 +2285,33 @@ paths:
schema: schema:
$ref: "#/components/schemas/OkTrue" $ref: "#/components/schemas/OkTrue"
/tasks/{process_instance_id}/{task_guid}/allows-guest:
parameters:
- name: task_guid
in: path
required: true
description: The unique id of an existing process group.
schema:
type: string
- name: process_instance_id
in: path
required: true
description: The unique id of an existing process instance.
schema:
type: integer
get:
tags:
- Tasks
operationId: spiffworkflow_backend.routes.tasks_controller.task_allows_guest
summary: Gets checks if the given task allows guest login
responses:
"200":
description: Whether the task can be completed by a guest
content:
application/json:
schema:
$ref: "#/components/schemas/TaskAllowsGuest"
# NOTE: this should probably be /process-instances instead # NOTE: this should probably be /process-instances instead
/tasks/{process_instance_id}/send-user-signal-event: /tasks/{process_instance_id}/send-user-signal-event:
parameters: parameters:
@ -3013,6 +3040,10 @@ components:
documentation: "# Heading 1\n\nMarkdown documentation text goes here" documentation: "# Heading 1\n\nMarkdown documentation text goes here"
type: form type: form
state: ready state: ready
TaskAllowsGuest:
properties:
allows_guest:
type: boolean
Task: Task:
properties: properties:
id: id:

View File

@ -3,7 +3,6 @@ from __future__ import annotations
import json import json
from dataclasses import dataclass from dataclasses import dataclass
from dataclasses import field
from typing import Any from typing import Any
import flask.wrappers import flask.wrappers
@ -38,18 +37,18 @@ class ApiError(Exception):
error_code: str error_code: str
message: str message: str
error_line: str | None = "" error_line: str | None = None
error_type: str | None = "" error_type: str | None = None
file_name: str | None = "" file_name: str | None = None
line_number: int | None = 0 line_number: int | None = None
offset: int | None = 0 offset: int | None = None
sentry_link: str | None = None sentry_link: str | None = None
status_code: int | None = 400 status_code: int | None = 400
tag: str | None = "" tag: str | None = None
task_data: dict | str | None = field(default_factory=dict) task_data: dict | str | None = None
task_id: str | None = "" task_id: str | None = None
task_name: str | None = "" task_name: str | None = None
task_trace: list | None = field(default_factory=list) task_trace: list | None = None
# these are useful if the error response cannot be json but has to be something else # these are useful if the error response cannot be json but has to be something else
# such as returning content type 'text/event-stream' for the interstitial page # such as returning content type 'text/event-stream' for the interstitial page
@ -67,6 +66,14 @@ class ApiError(Exception):
msg += f"In file {self.file_name}. " msg += f"In file {self.file_name}. "
return msg return msg
def serialized(self) -> dict[str, Any]:
initial_dict = self.__dict__
return_dict = {}
for key, value in initial_dict.items():
if value is not None and value != "":
return_dict[key] = value
return return_dict
@classmethod @classmethod
def from_task( def from_task(
cls, cls,
@ -74,17 +81,17 @@ class ApiError(Exception):
message: str, message: str,
task: Task, task: Task,
status_code: int = 400, status_code: int = 400,
line_number: int = 0, line_number: int | None = None,
offset: int = 0, offset: int | None = None,
error_type: str = "", error_type: str | None = None,
error_line: str = "", error_line: str | None = None,
task_trace: list | None = None, task_trace: list | None = None,
) -> ApiError: ) -> ApiError:
"""Constructs an API Error with details pulled from the current task.""" """Constructs an API Error with details pulled from the current task."""
instance = cls(error_code, message, status_code=status_code) instance = cls(error_code, message, status_code=status_code)
instance.task_id = task.task_spec.name or "" instance.task_id = task.task_spec.name
instance.task_name = task.task_spec.description or "" instance.task_name = task.task_spec.description
instance.file_name = task.workflow.spec.file or "" instance.file_name = task.workflow.spec.file
instance.line_number = line_number instance.line_number = line_number
instance.offset = offset instance.offset = offset
instance.error_type = error_type instance.error_type = error_type
@ -110,17 +117,17 @@ class ApiError(Exception):
message: str, message: str,
task_model: TaskModel, task_model: TaskModel,
status_code: int | None = 400, status_code: int | None = 400,
line_number: int | None = 0, line_number: int | None = None,
offset: int | None = 0, offset: int | None = None,
error_type: str | None = "", error_type: str | None = None,
error_line: str | None = "", error_line: str | None = None,
task_trace: list | None = None, task_trace: list | None = None,
) -> ApiError: ) -> ApiError:
"""Constructs an API Error with details pulled from the current task model.""" """Constructs an API Error with details pulled from the current task model."""
instance = cls(error_code, message, status_code=status_code) instance = cls(error_code, message, status_code=status_code)
task_definition = task_model.task_definition task_definition = task_model.task_definition
instance.task_id = task_definition.bpmn_identifier instance.task_id = task_definition.bpmn_identifier
instance.task_name = task_definition.bpmn_name or "" instance.task_name = task_definition.bpmn_name
instance.line_number = line_number instance.line_number = line_number
instance.offset = offset instance.offset = offset
instance.error_type = error_type instance.error_type = error_type

View File

@ -51,3 +51,7 @@ class UserDoesNotHaveAccessToTaskError(Exception):
class InvalidPermissionError(Exception): class InvalidPermissionError(Exception):
pass pass
class InvalidRedirectUrlError(Exception):
pass

View File

@ -15,6 +15,7 @@ from flask import request
from werkzeug.wrappers import Response from werkzeug.wrappers import Response
from spiffworkflow_backend.exceptions.api_error import ApiError from spiffworkflow_backend.exceptions.api_error import ApiError
from spiffworkflow_backend.exceptions.error import InvalidRedirectUrlError
from spiffworkflow_backend.exceptions.error import MissingAccessTokenError from spiffworkflow_backend.exceptions.error import MissingAccessTokenError
from spiffworkflow_backend.exceptions.error import TokenExpiredError from spiffworkflow_backend.exceptions.error import TokenExpiredError
from spiffworkflow_backend.helpers.api_version import V1_API_PATH_PREFIX from spiffworkflow_backend.helpers.api_version import V1_API_PATH_PREFIX
@ -99,6 +100,12 @@ def login(
process_instance_id: int | None = None, process_instance_id: int | None = None,
task_guid: str | None = None, task_guid: str | None = None,
) -> Response: ) -> Response:
frontend_url = str(current_app.config.get("SPIFFWORKFLOW_BACKEND_URL_FOR_FRONTEND"))
if not redirect_url.startswith(frontend_url):
raise InvalidRedirectUrlError(
f"Invalid redirect url was given: '{redirect_url}'. It must match the domain the frontend is running on."
)
if current_app.config.get("SPIFFWORKFLOW_BACKEND_AUTHENTICATION_DISABLED"): if current_app.config.get("SPIFFWORKFLOW_BACKEND_AUTHENTICATION_DISABLED"):
AuthenticationService.create_guest_token( AuthenticationService.create_guest_token(
username=SPIFF_NO_AUTH_USER, username=SPIFF_NO_AUTH_USER,

View File

@ -80,6 +80,16 @@ class ReactJsonSchemaSelectOption(TypedDict):
enum: list[str] enum: list[str]
def task_allows_guest(
process_instance_id: int,
task_guid: str,
) -> flask.wrappers.Response:
allows_guest = False
if process_instance_id and task_guid and TaskModel.task_guid_allows_guest(task_guid, process_instance_id):
allows_guest = True
return make_response(jsonify({"allows_guest": allows_guest}), 200)
# this is currently not used by the Frontend # this is currently not used by the Frontend
def task_list_my_tasks( def task_list_my_tasks(
process_instance_id: int | None = None, page: int = 1, per_page: int = 100 process_instance_id: int | None = None, page: int = 1, per_page: int = 100

View File

@ -1,11 +1,11 @@
import time import time
from flask import current_app from flask import current_app
from spiffworkflow_backend.models.db import db from spiffworkflow_backend.models.db import db
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus
from spiffworkflow_backend.services.process_instance_service import ProcessInstanceService from spiffworkflow_backend.services.process_instance_service import ProcessInstanceService
from tests.spiffworkflow_backend.helpers.base_test import BaseTest from tests.spiffworkflow_backend.helpers.base_test import BaseTest

View File

@ -13,6 +13,8 @@ from flask import current_app
from flask import g from flask import g
from flask import redirect from flask import redirect
from flask import request from flask import request
from werkzeug.wrappers import Response
from spiffworkflow_backend.config import HTTP_REQUEST_TIMEOUT_SECONDS from spiffworkflow_backend.config import HTTP_REQUEST_TIMEOUT_SECONDS
from spiffworkflow_backend.exceptions.error import OpenIdConnectionError from spiffworkflow_backend.exceptions.error import OpenIdConnectionError
from spiffworkflow_backend.exceptions.error import RefreshTokenStorageError from spiffworkflow_backend.exceptions.error import RefreshTokenStorageError
@ -23,7 +25,6 @@ from spiffworkflow_backend.models.db import db
from spiffworkflow_backend.models.refresh_token import RefreshTokenModel from spiffworkflow_backend.models.refresh_token import RefreshTokenModel
from spiffworkflow_backend.services.authorization_service import AuthorizationService from spiffworkflow_backend.services.authorization_service import AuthorizationService
from spiffworkflow_backend.services.user_service import UserService from spiffworkflow_backend.services.user_service import UserService
from werkzeug.wrappers import Response
class AuthenticationProviderTypes(enum.Enum): class AuthenticationProviderTypes(enum.Enum):
@ -118,7 +119,9 @@ class AuthenticationService:
if redirect_url is None: if redirect_url is None:
redirect_url = f"{self.get_backend_url()}/v1.0/logout_return" redirect_url = f"{self.get_backend_url()}/v1.0/logout_return"
request_url = ( request_url = (
self.open_id_endpoint_for_name("end_session_endpoint", authentication_identifier=authentication_identifier) self.__class__.open_id_endpoint_for_name(
"end_session_endpoint", authentication_identifier=authentication_identifier
)
+ f"?post_logout_redirect_uri={redirect_url}&" + f"?post_logout_redirect_uri={redirect_url}&"
+ f"id_token_hint={id_token}" + f"id_token_hint={id_token}"
) )
@ -137,7 +140,7 @@ class AuthenticationService:
) -> str: ) -> str:
return_redirect_url = f"{self.get_backend_url()}{redirect_url}" return_redirect_url = f"{self.get_backend_url()}{redirect_url}"
login_redirect_url = ( login_redirect_url = (
self.open_id_endpoint_for_name( self.__class__.open_id_endpoint_for_name(
"authorization_endpoint", authentication_identifier=authentication_identifier "authorization_endpoint", authentication_identifier=authentication_identifier
) )
+ f"?state={state}&" + f"?state={state}&"
@ -146,7 +149,6 @@ class AuthenticationService:
+ "scope=openid profile email&" + "scope=openid profile email&"
+ f"redirect_uri={return_redirect_url}" + f"redirect_uri={return_redirect_url}"
) )
print(f"login_redirect_url: {login_redirect_url}")
return login_redirect_url return login_redirect_url
def get_auth_token_object( def get_auth_token_object(
@ -173,7 +175,6 @@ class AuthenticationService:
response = requests.post(request_url, data=data, headers=headers, timeout=HTTP_REQUEST_TIMEOUT_SECONDS) response = requests.post(request_url, data=data, headers=headers, timeout=HTTP_REQUEST_TIMEOUT_SECONDS)
auth_token_object: dict = json.loads(response.text) auth_token_object: dict = json.loads(response.text)
print(f"auth_token_object: {auth_token_object}")
return auth_token_object return auth_token_object
@classmethod @classmethod

View File

@ -7,6 +7,11 @@ from flask import current_app
from flask import g from flask import g
from flask import request from flask import request
from flask import scaffold from flask import scaffold
from sqlalchemy import and_
from sqlalchemy import func
from sqlalchemy import literal
from sqlalchemy import or_
from spiffworkflow_backend.exceptions.error import HumanTaskAlreadyCompletedError from spiffworkflow_backend.exceptions.error import HumanTaskAlreadyCompletedError
from spiffworkflow_backend.exceptions.error import HumanTaskNotFoundError from spiffworkflow_backend.exceptions.error import HumanTaskNotFoundError
from spiffworkflow_backend.exceptions.error import InvalidPermissionError from spiffworkflow_backend.exceptions.error import InvalidPermissionError
@ -34,10 +39,6 @@ from spiffworkflow_backend.models.user_group_assignment import UserGroupAssignme
from spiffworkflow_backend.models.user_group_assignment_waiting import UserGroupAssignmentWaitingModel from spiffworkflow_backend.models.user_group_assignment_waiting import UserGroupAssignmentWaitingModel
from spiffworkflow_backend.routes.openid_blueprint import openid_blueprint from spiffworkflow_backend.routes.openid_blueprint import openid_blueprint
from spiffworkflow_backend.services.user_service import UserService from spiffworkflow_backend.services.user_service import UserService
from sqlalchemy import and_
from sqlalchemy import func
from sqlalchemy import literal
from sqlalchemy import or_
@dataclass @dataclass
@ -228,11 +229,15 @@ class AuthorizationService:
def should_disable_auth_for_request(cls) -> bool: def should_disable_auth_for_request(cls) -> bool:
swagger_functions = ["get_json_spec"] swagger_functions = ["get_json_spec"]
authentication_exclusion_list = [ authentication_exclusion_list = [
"status",
"test_raise_error",
"authentication_begin", "authentication_begin",
"authentication_callback", "authentication_callback",
"authentication_options",
"github_webhook_receive", "github_webhook_receive",
"prometheus_metrics",
"status",
"task_allows_guest",
"test_raise_error",
"url_info",
] ]
if request.method == "OPTIONS": if request.method == "OPTIONS":
return True return True
@ -250,10 +255,6 @@ class AuthorizationService:
api_view_function api_view_function
and api_view_function.__name__.startswith("login") and api_view_function.__name__.startswith("login")
or api_view_function.__name__.startswith("logout") or api_view_function.__name__.startswith("logout")
or api_view_function.__name__.startswith("authentication_options")
or api_view_function.__name__.startswith("prom")
or api_view_function.__name__ == "url_info"
or api_view_function.__name__.startswith("metric")
or api_view_function.__name__.startswith("console_ui_") or api_view_function.__name__.startswith("console_ui_")
or api_view_function.__name__ in authentication_exclusion_list or api_view_function.__name__ in authentication_exclusion_list
or api_view_function.__name__ in swagger_functions or api_view_function.__name__ in swagger_functions

View File

@ -1,4 +1,5 @@
import flask import flask
from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus
from spiffworkflow_backend.services.message_service import MessageService from spiffworkflow_backend.services.message_service import MessageService
from spiffworkflow_backend.services.process_instance_lock_service import ProcessInstanceLockService from spiffworkflow_backend.services.process_instance_lock_service import ProcessInstanceLockService

View File

@ -4,6 +4,7 @@ from SpiffWorkflow.bpmn.parser.BpmnParser import full_tag # type: ignore
from SpiffWorkflow.dmn.parser.BpmnDmnParser import BpmnDmnParser # type: ignore from SpiffWorkflow.dmn.parser.BpmnDmnParser import BpmnDmnParser # type: ignore
from SpiffWorkflow.spiff.parser.process import SpiffBpmnParser # type: ignore from SpiffWorkflow.spiff.parser.process import SpiffBpmnParser # type: ignore
from SpiffWorkflow.spiff.parser.task_spec import ServiceTaskParser # type: ignore from SpiffWorkflow.spiff.parser.task_spec import ServiceTaskParser # type: ignore
from spiffworkflow_backend.data_stores import register_data_store_classes from spiffworkflow_backend.data_stores import register_data_store_classes
from spiffworkflow_backend.services.service_task_service import CustomServiceTask from spiffworkflow_backend.services.service_task_service import CustomServiceTask
from spiffworkflow_backend.specs.start_event import StartEvent from spiffworkflow_backend.specs.start_event import StartEvent

View File

@ -1,6 +1,7 @@
import os import os
from flask import current_app from flask import current_app
from spiffworkflow_backend.models.db import db from spiffworkflow_backend.models.db import db
from spiffworkflow_backend.models.reference_cache import ReferenceCacheModel from spiffworkflow_backend.models.reference_cache import ReferenceCacheModel
from spiffworkflow_backend.services.file_system_service import FileSystemService from spiffworkflow_backend.services.file_system_service import FileSystemService

View File

@ -1,5 +1,6 @@
from flask import current_app from flask import current_app
from flask import g from flask import g
from spiffworkflow_backend.models.db import db from spiffworkflow_backend.models.db import db
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus

View File

@ -7,6 +7,7 @@ from typing import Any
import pytz import pytz
from flask import current_app from flask import current_app
from spiffworkflow_backend.exceptions.api_error import ApiError from spiffworkflow_backend.exceptions.api_error import ApiError
from spiffworkflow_backend.models.file import CONTENT_TYPES from spiffworkflow_backend.models.file import CONTENT_TYPES
from spiffworkflow_backend.models.file import File from spiffworkflow_backend.models.file import File

View File

@ -6,6 +6,7 @@ import uuid
from flask import current_app from flask import current_app
from flask import g from flask import g
from spiffworkflow_backend.config import ConfigurationError from spiffworkflow_backend.config import ConfigurationError
from spiffworkflow_backend.models.process_model import ProcessModelInfo from spiffworkflow_backend.models.process_model import ProcessModelInfo
from spiffworkflow_backend.services.data_setup_service import DataSetupService from spiffworkflow_backend.services.data_setup_service import DataSetupService

View File

@ -5,6 +5,7 @@ import jinja2
from jinja2 import TemplateSyntaxError from jinja2 import TemplateSyntaxError
from SpiffWorkflow.bpmn.exceptions import WorkflowTaskException # type: ignore from SpiffWorkflow.bpmn.exceptions import WorkflowTaskException # type: ignore
from SpiffWorkflow.task import Task as SpiffTask # type: ignore from SpiffWorkflow.task import Task as SpiffTask # type: ignore
from spiffworkflow_backend.exceptions.api_error import ApiError from spiffworkflow_backend.exceptions.api_error import ApiError
from spiffworkflow_backend.models.task import TaskModel # noqa: F401 from spiffworkflow_backend.models.task import TaskModel # noqa: F401
from spiffworkflow_backend.services.task_service import TaskModelError from spiffworkflow_backend.services.task_service import TaskModelError

View File

@ -1,6 +1,7 @@
from SpiffWorkflow.bpmn.event import BpmnEvent # type: ignore from SpiffWorkflow.bpmn.event import BpmnEvent # type: ignore
from SpiffWorkflow.bpmn.specs.event_definitions.message import CorrelationProperty # type: ignore from SpiffWorkflow.bpmn.specs.event_definitions.message import CorrelationProperty # type: ignore
from SpiffWorkflow.spiff.specs.event_definitions import MessageEventDefinition # type: ignore from SpiffWorkflow.spiff.specs.event_definitions import MessageEventDefinition # type: ignore
from spiffworkflow_backend.models.db import db from spiffworkflow_backend.models.db import db
from spiffworkflow_backend.models.message_instance import MessageInstanceModel from spiffworkflow_backend.models.message_instance import MessageInstanceModel
from spiffworkflow_backend.models.message_instance import MessageStatuses from spiffworkflow_backend.models.message_instance import MessageStatuses

View File

@ -5,6 +5,7 @@ from typing import Any
from flask import Flask from flask import Flask
from flask import session from flask import session
from flask_oauthlib.client import OAuth # type: ignore from flask_oauthlib.client import OAuth # type: ignore
from spiffworkflow_backend.exceptions.api_error import ApiError from spiffworkflow_backend.exceptions.api_error import ApiError
from spiffworkflow_backend.services.configuration_service import ConfigurationService from spiffworkflow_backend.services.configuration_service import ConfigurationService
from spiffworkflow_backend.services.secret_service import SecretService from spiffworkflow_backend.services.secret_service import SecretService

View File

@ -1,6 +1,7 @@
from sqlalchemy import or_
from spiffworkflow_backend.models.db import db from spiffworkflow_backend.models.db import db
from spiffworkflow_backend.models.process_caller import ProcessCallerCacheModel from spiffworkflow_backend.models.process_caller import ProcessCallerCacheModel
from sqlalchemy import or_
class ProcessCallerService: class ProcessCallerService:

View File

@ -3,11 +3,12 @@ import time
from typing import Any from typing import Any
from flask import current_app from flask import current_app
from spiffworkflow_backend.models.db import db
from spiffworkflow_backend.models.process_instance_queue import ProcessInstanceQueueModel
from sqlalchemy import and_ from sqlalchemy import and_
from sqlalchemy import or_ from sqlalchemy import or_
from spiffworkflow_backend.models.db import db
from spiffworkflow_backend.models.process_instance_queue import ProcessInstanceQueueModel
class ExpectedLockNotFoundError(Exception): class ExpectedLockNotFoundError(Exception):
pass pass

View File

@ -50,6 +50,8 @@ from SpiffWorkflow.task import Task as SpiffTask # type: ignore
from SpiffWorkflow.util.deep_merge import DeepMerge # type: ignore from SpiffWorkflow.util.deep_merge import DeepMerge # type: ignore
from SpiffWorkflow.util.task import TaskIterator # type: ignore from SpiffWorkflow.util.task import TaskIterator # type: ignore
from SpiffWorkflow.util.task import TaskState from SpiffWorkflow.util.task import TaskState
from sqlalchemy import and_
from spiffworkflow_backend.data_stores.json import JSONDataStore from spiffworkflow_backend.data_stores.json import JSONDataStore
from spiffworkflow_backend.data_stores.json import JSONDataStoreConverter from spiffworkflow_backend.data_stores.json import JSONDataStoreConverter
from spiffworkflow_backend.data_stores.json import JSONFileDataStore from spiffworkflow_backend.data_stores.json import JSONFileDataStore
@ -102,7 +104,6 @@ from spiffworkflow_backend.services.workflow_execution_service import TaskModelS
from spiffworkflow_backend.services.workflow_execution_service import WorkflowExecutionService from spiffworkflow_backend.services.workflow_execution_service import WorkflowExecutionService
from spiffworkflow_backend.services.workflow_execution_service import execution_strategy_named from spiffworkflow_backend.services.workflow_execution_service import execution_strategy_named
from spiffworkflow_backend.specs.start_event import StartEvent from spiffworkflow_backend.specs.start_event import StartEvent
from sqlalchemy import and_
SPIFF_CONFIG[StandardLoopTask] = StandardLoopTaskConverter SPIFF_CONFIG[StandardLoopTask] = StandardLoopTaskConverter

View File

@ -6,6 +6,13 @@ from typing import Any
import sqlalchemy import sqlalchemy
from flask import current_app from flask import current_app
from flask_sqlalchemy.query import Query from flask_sqlalchemy.query import Query
from sqlalchemy import and_
from sqlalchemy import func
from sqlalchemy import or_
from sqlalchemy.orm import aliased
from sqlalchemy.orm import selectinload
from sqlalchemy.orm.util import AliasedClass
from spiffworkflow_backend.exceptions.api_error import ApiError from spiffworkflow_backend.exceptions.api_error import ApiError
from spiffworkflow_backend.models.db import SpiffworkflowBaseDBModel from spiffworkflow_backend.models.db import SpiffworkflowBaseDBModel
from spiffworkflow_backend.models.db import db from spiffworkflow_backend.models.db import db
@ -22,12 +29,6 @@ from spiffworkflow_backend.models.task import TaskModel # noqa: F401
from spiffworkflow_backend.models.user import UserModel from spiffworkflow_backend.models.user import UserModel
from spiffworkflow_backend.models.user_group_assignment import UserGroupAssignmentModel from spiffworkflow_backend.models.user_group_assignment import UserGroupAssignmentModel
from spiffworkflow_backend.services.process_model_service import ProcessModelService from spiffworkflow_backend.services.process_model_service import ProcessModelService
from sqlalchemy import and_
from sqlalchemy import func
from sqlalchemy import or_
from sqlalchemy.orm import aliased
from sqlalchemy.orm import selectinload
from sqlalchemy.orm.util import AliasedClass
class ProcessInstanceReportNotFoundError(Exception): class ProcessInstanceReportNotFoundError(Exception):

View File

@ -16,6 +16,7 @@ from SpiffWorkflow.bpmn.specs.defaults import BoundaryEvent # type: ignore
from SpiffWorkflow.bpmn.specs.event_definitions.timer import TimerEventDefinition # type: ignore from SpiffWorkflow.bpmn.specs.event_definitions.timer import TimerEventDefinition # type: ignore
from SpiffWorkflow.task import Task as SpiffTask # type: ignore from SpiffWorkflow.task import Task as SpiffTask # type: ignore
from SpiffWorkflow.util.task import TaskState # type: ignore from SpiffWorkflow.util.task import TaskState # type: ignore
from spiffworkflow_backend import db from spiffworkflow_backend import db
from spiffworkflow_backend.data_migrations.process_instance_migrator import ProcessInstanceMigrator from spiffworkflow_backend.data_migrations.process_instance_migrator import ProcessInstanceMigrator
from spiffworkflow_backend.exceptions.api_error import ApiError from spiffworkflow_backend.exceptions.api_error import ApiError

View File

@ -3,6 +3,7 @@ import traceback
from flask import g from flask import g
from SpiffWorkflow.bpmn.exceptions import WorkflowTaskException # type: ignore from SpiffWorkflow.bpmn.exceptions import WorkflowTaskException # type: ignore
from spiffworkflow_backend.models.db import db from spiffworkflow_backend.models.db import db
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
from spiffworkflow_backend.models.process_instance_error_detail import ProcessInstanceErrorDetailModel from spiffworkflow_backend.models.process_instance_error_detail import ProcessInstanceErrorDetailModel

View File

@ -6,6 +6,7 @@ from json import JSONDecodeError
from typing import TypeVar from typing import TypeVar
from flask import current_app from flask import current_app
from spiffworkflow_backend.exceptions.api_error import ApiError from spiffworkflow_backend.exceptions.api_error import ApiError
from spiffworkflow_backend.exceptions.process_entity_not_found_error import ProcessEntityNotFoundError from spiffworkflow_backend.exceptions.process_entity_not_found_error import ProcessEntityNotFoundError
from spiffworkflow_backend.interfaces import ProcessGroupLite from spiffworkflow_backend.interfaces import ProcessGroupLite

View File

@ -11,6 +11,7 @@ from SpiffWorkflow.bpmn.exceptions import WorkflowTaskException # type: ignore
from SpiffWorkflow.bpmn.workflow import BpmnWorkflow # type: ignore from SpiffWorkflow.bpmn.workflow import BpmnWorkflow # type: ignore
from SpiffWorkflow.task import Task as SpiffTask # type: ignore from SpiffWorkflow.task import Task as SpiffTask # type: ignore
from SpiffWorkflow.util.task import TaskState # type: ignore from SpiffWorkflow.util.task import TaskState # type: ignore
from spiffworkflow_backend.services.custom_parser import MyCustomParser from spiffworkflow_backend.services.custom_parser import MyCustomParser

View File

@ -1,9 +1,10 @@
import os import os
from sqlalchemy import insert
from spiffworkflow_backend.models.cache_generation import CacheGenerationModel from spiffworkflow_backend.models.cache_generation import CacheGenerationModel
from spiffworkflow_backend.models.db import db from spiffworkflow_backend.models.db import db
from spiffworkflow_backend.models.reference_cache import ReferenceCacheModel from spiffworkflow_backend.models.reference_cache import ReferenceCacheModel
from sqlalchemy import insert
class ReferenceCacheService: class ReferenceCacheService:

View File

@ -6,6 +6,7 @@ from typing import Any
from SpiffWorkflow.bpmn.exceptions import WorkflowTaskException # type: ignore from SpiffWorkflow.bpmn.exceptions import WorkflowTaskException # type: ignore
from SpiffWorkflow.task import Task as SpiffTask # type: ignore from SpiffWorkflow.task import Task as SpiffTask # type: ignore
from spiffworkflow_backend.services.process_instance_processor import CustomBpmnScriptEngine from spiffworkflow_backend.services.process_instance_processor import CustomBpmnScriptEngine
PythonScriptContext = dict[str, Any] PythonScriptContext = dict[str, Any]

View File

@ -2,6 +2,7 @@ import re
import sentry_sdk import sentry_sdk
from flask import current_app from flask import current_app
from spiffworkflow_backend.exceptions.api_error import ApiError from spiffworkflow_backend.exceptions.api_error import ApiError
from spiffworkflow_backend.models.db import db from spiffworkflow_backend.models.db import db
from spiffworkflow_backend.models.secret_model import SecretModel from spiffworkflow_backend.models.secret_model import SecretModel

View File

@ -14,12 +14,13 @@ from SpiffWorkflow.spiff.specs.event_definitions import ErrorEventDefinition #
from SpiffWorkflow.spiff.specs.event_definitions import EscalationEventDefinition from SpiffWorkflow.spiff.specs.event_definitions import EscalationEventDefinition
from SpiffWorkflow.task import Task as SpiffTask # type: ignore from SpiffWorkflow.task import Task as SpiffTask # type: ignore
from SpiffWorkflow.util.task import TaskState # type: ignore from SpiffWorkflow.util.task import TaskState # type: ignore
from spiffworkflow_connector_command.command_interface import CommandErrorDict
from spiffworkflow_backend.config import CONNECTOR_PROXY_COMMAND_TIMEOUT from spiffworkflow_backend.config import CONNECTOR_PROXY_COMMAND_TIMEOUT
from spiffworkflow_backend.config import HTTP_REQUEST_TIMEOUT_SECONDS from spiffworkflow_backend.config import HTTP_REQUEST_TIMEOUT_SECONDS
from spiffworkflow_backend.services.file_system_service import FileSystemService from spiffworkflow_backend.services.file_system_service import FileSystemService
from spiffworkflow_backend.services.secret_service import SecretService from spiffworkflow_backend.services.secret_service import SecretService
from spiffworkflow_backend.services.user_service import UserService from spiffworkflow_backend.services.user_service import UserService
from spiffworkflow_connector_command.command_interface import CommandErrorDict
class ConnectorProxyError(Exception): class ConnectorProxyError(Exception):

View File

@ -4,6 +4,7 @@ from datetime import datetime
from lxml import etree # type: ignore from lxml import etree # type: ignore
from SpiffWorkflow.bpmn.parser.BpmnParser import BpmnValidator # type: ignore from SpiffWorkflow.bpmn.parser.BpmnParser import BpmnValidator # type: ignore
from spiffworkflow_backend.exceptions.error import NotAuthorizedError from spiffworkflow_backend.exceptions.error import NotAuthorizedError
from spiffworkflow_backend.models.correlation_property_cache import CorrelationPropertyCache from spiffworkflow_backend.models.correlation_property_cache import CorrelationPropertyCache
from spiffworkflow_backend.models.db import db from spiffworkflow_backend.models.db import db

View File

@ -10,6 +10,7 @@ from SpiffWorkflow.bpmn.workflow import BpmnWorkflow # type: ignore
from SpiffWorkflow.exceptions import WorkflowException # type: ignore from SpiffWorkflow.exceptions import WorkflowException # type: ignore
from SpiffWorkflow.task import Task as SpiffTask # type: ignore from SpiffWorkflow.task import Task as SpiffTask # type: ignore
from SpiffWorkflow.util.task import TaskState # type: ignore from SpiffWorkflow.util.task import TaskState # type: ignore
from spiffworkflow_backend.models.bpmn_process import BpmnProcessModel from spiffworkflow_backend.models.bpmn_process import BpmnProcessModel
from spiffworkflow_backend.models.bpmn_process import BpmnProcessNotFoundError from spiffworkflow_backend.models.bpmn_process import BpmnProcessNotFoundError
from spiffworkflow_backend.models.bpmn_process_definition import BpmnProcessDefinitionModel from spiffworkflow_backend.models.bpmn_process_definition import BpmnProcessDefinitionModel

View File

@ -3,6 +3,8 @@ from typing import Any
from flask import current_app from flask import current_app
from flask import g from flask import g
from sqlalchemy import and_
from spiffworkflow_backend.exceptions.api_error import ApiError from spiffworkflow_backend.exceptions.api_error import ApiError
from spiffworkflow_backend.interfaces import UserToGroupDict from spiffworkflow_backend.interfaces import UserToGroupDict
from spiffworkflow_backend.models.db import db from spiffworkflow_backend.models.db import db
@ -17,7 +19,6 @@ from spiffworkflow_backend.models.user import UserModel
from spiffworkflow_backend.models.user_group_assignment import UserGroupAssignmentModel from spiffworkflow_backend.models.user_group_assignment import UserGroupAssignmentModel
from spiffworkflow_backend.models.user_group_assignment import UserGroupAssignmentNotFoundError from spiffworkflow_backend.models.user_group_assignment import UserGroupAssignmentNotFoundError
from spiffworkflow_backend.models.user_group_assignment_waiting import UserGroupAssignmentWaitingModel from spiffworkflow_backend.models.user_group_assignment_waiting import UserGroupAssignmentWaitingModel
from sqlalchemy import and_
class UserService: class UserService:

View File

@ -17,6 +17,7 @@ from SpiffWorkflow.bpmn.workflow import BpmnWorkflow # type: ignore
from SpiffWorkflow.exceptions import SpiffWorkflowException # type: ignore from SpiffWorkflow.exceptions import SpiffWorkflowException # type: ignore
from SpiffWorkflow.task import Task as SpiffTask # type: ignore from SpiffWorkflow.task import Task as SpiffTask # type: ignore
from SpiffWorkflow.util.task import TaskState # type: ignore from SpiffWorkflow.util.task import TaskState # type: ignore
from spiffworkflow_backend.exceptions.api_error import ApiError from spiffworkflow_backend.exceptions.api_error import ApiError
from spiffworkflow_backend.models.db import db from spiffworkflow_backend.models.db import db
from spiffworkflow_backend.models.message_instance import MessageInstanceModel from spiffworkflow_backend.models.message_instance import MessageInstanceModel

View File

@ -3,6 +3,7 @@ from datetime import datetime
from SpiffWorkflow.bpmn.workflow import BpmnWorkflow # type: ignore from SpiffWorkflow.bpmn.workflow import BpmnWorkflow # type: ignore
from SpiffWorkflow.task import Task as SpiffTask # type: ignore from SpiffWorkflow.task import Task as SpiffTask # type: ignore
from SpiffWorkflow.util.task import TaskState # type: ignore from SpiffWorkflow.util.task import TaskState # type: ignore
from spiffworkflow_backend.specs.start_event import StartConfiguration from spiffworkflow_backend.specs.start_event import StartConfiguration
from spiffworkflow_backend.specs.start_event import StartEvent from spiffworkflow_backend.specs.start_event import StartEvent

View File

@ -1,6 +1,8 @@
import ast import ast
import base64 import base64
import re
import time import time
from typing import Any
from flask.app import Flask from flask.app import Flask
from flask.testing import FlaskClient from flask.testing import FlaskClient
@ -116,3 +118,42 @@ class TestAuthentication(BaseTest):
UserService.get_permission_targets_for_user(service_account.user, check_groups=False) UserService.get_permission_targets_for_user(service_account.user, check_groups=False)
) )
assert service_account_permissions_before == service_account_permissions_after assert service_account_permissions_before == service_account_permissions_after
def test_can_login_with_valid_user(
self,
app: Flask,
mocker: Any,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,
) -> None:
redirect_uri = f"{app.config['SPIFFWORKFLOW_BACKEND_URL_FOR_FRONTEND']}/test-redirect-dne"
auth_uri = app.config["SPIFFWORKFLOW_BACKEND_AUTH_CONFIGS"][0]["uri"]
login_return_uri = f"{app.config['SPIFFWORKFLOW_BACKEND_URL']}/v1.0/login_return"
class_method_mock = mocker.patch(
"spiffworkflow_backend.services.authentication_service.AuthenticationService.open_id_endpoint_for_name",
return_value=auth_uri,
)
response = client.get(
f"/v1.0/login?redirect_url={redirect_uri}&authentication_identifier=default",
)
assert class_method_mock.call_count == 1
assert response.status_code == 302
assert response.location.startswith(auth_uri)
assert re.search(r"\bredirect_uri=" + re.escape(login_return_uri), response.location) is not None
def test_raises_error_if_invalid_redirect_url(
self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,
) -> None:
redirect_url = "http://www.bad_url.com/test-redirect-dne"
response = client.get(
f"/v1.0/login?redirect_url={redirect_url}&authentication_identifier=DOES_NOT_MATTER",
)
assert response.status_code == 500
assert response.json is not None
assert response.json["message"].startswith("InvalidRedirectUrlError:")

View File

@ -486,7 +486,7 @@ class TestTasksController(BaseTest):
task_guid = task_model.guid task_guid = task_model.guid
# log in a guest user to complete the tasks # log in a guest user to complete the tasks
redirect_url = "/test-redirect-dne" redirect_url = f"{app.config['SPIFFWORKFLOW_BACKEND_URL_FOR_FRONTEND']}/test-redirect-dne"
response = client.get( response = client.get(
f"/v1.0/login?process_instance_id={process_instance_id}&task_guid={task_guid}&redirect_url={redirect_url}&authentication_identifier=DOES_NOT_MATTER", f"/v1.0/login?process_instance_id={process_instance_id}&task_guid={task_guid}&redirect_url={redirect_url}&authentication_identifier=DOES_NOT_MATTER",
) )

View File

@ -272,3 +272,13 @@ export const getLastMilestoneFromProcessInstance = (
} }
return [valueToUse, truncatedValue]; return [valueToUse, truncatedValue];
}; };
export const parseTaskShowUrl = (url: string) => {
const path = pathFromFullUrl(url);
// expected url pattern:
// /tasks/[process_instance_id]/[task_guid]
return path.match(
/^\/tasks\/(\d+)\/([0-9a-z]{8}-([0-9a-z]{4}-){3}[0-9a-z]{12})$/
);
};

View File

@ -1,10 +1,11 @@
import { ArrowRight } from '@carbon/icons-react'; import { ArrowRight } from '@carbon/icons-react';
import { useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { Loading, Button, Grid, Column } from '@carbon/react'; import { Loading, Button, Grid, Column } from '@carbon/react';
import { useNavigate, useSearchParams } from 'react-router-dom'; import { useNavigate, useSearchParams } from 'react-router-dom';
import { AuthenticationOption } from '../interfaces'; import { AuthenticationOption } from '../interfaces';
import HttpService from '../services/HttpService'; import HttpService from '../services/HttpService';
import UserService from '../services/UserService'; import UserService from '../services/UserService';
import { parseTaskShowUrl } from '../helpers';
export default function Login() { export default function Login() {
const navigate = useNavigate(); const navigate = useNavigate();
@ -12,20 +13,34 @@ export default function Login() {
const [authenticationOptions, setAuthenticationOptions] = useState< const [authenticationOptions, setAuthenticationOptions] = useState<
AuthenticationOption[] | null AuthenticationOption[] | null
>(null); >(null);
const [allowsGuestLogin, setAllowsGuestLogin] = useState<boolean | null>(
null
);
const originalUrl = searchParams.get('original_url');
const getOriginalUrl = useCallback(() => {
if (originalUrl === '/login') {
return '/';
}
return originalUrl;
}, [originalUrl]);
useEffect(() => { useEffect(() => {
HttpService.makeCallToBackend({ HttpService.makeCallToBackend({
path: '/authentication-options', path: '/authentication-options',
successCallback: setAuthenticationOptions, successCallback: setAuthenticationOptions,
}); });
}, []); const pathSegments = parseTaskShowUrl(getOriginalUrl() || '');
if (pathSegments) {
const getOriginalUrl = () => { HttpService.makeCallToBackend({
const originalUrl = searchParams.get('original_url'); path: `/tasks/${pathSegments[1]}/${pathSegments[2]}/allows-guest`,
if (originalUrl === '/login') { successCallback: (result: any) =>
return '/'; setAllowsGuestLogin(result.allows_guest),
});
} else {
setAllowsGuestLogin(false);
} }
return originalUrl; }, [getOriginalUrl]);
};
const authenticationOptionButtons = () => { const authenticationOptionButtons = () => {
if (!authenticationOptions) { if (!authenticationOptions) {
@ -48,15 +63,6 @@ export default function Login() {
return buttons; return buttons;
}; };
const doLoginIfAppropriate = () => {
if (!authenticationOptions) {
return;
}
if (authenticationOptions.length === 1) {
UserService.doLogin(authenticationOptions[0], getOriginalUrl());
}
};
const getLoadingIcon = () => { const getLoadingIcon = () => {
const style = { margin: '50px 0 50px 50px' }; const style = { margin: '50px 0 50px 50px' };
return ( return (
@ -98,8 +104,7 @@ export default function Login() {
return null; return null;
} }
if (authenticationOptions === null || authenticationOptions.length < 2) { if (authenticationOptions === null) {
doLoginIfAppropriate();
return ( return (
<div className="fixed-width-container login-page-spacer"> <div className="fixed-width-container login-page-spacer">
{getLoadingIcon()} {getLoadingIcon()}
@ -107,8 +112,11 @@ export default function Login() {
); );
} }
if (authenticationOptions !== null) { if (authenticationOptions !== null && allowsGuestLogin !== null) {
doLoginIfAppropriate(); if (allowsGuestLogin || authenticationOptions.length === 1) {
UserService.doLogin(authenticationOptions[0], getOriginalUrl());
return null;
}
return loginComponments(); return loginComponments();
} }

View File

@ -2,7 +2,7 @@ import jwt from 'jwt-decode';
import cookie from 'cookie'; import cookie from 'cookie';
import { BACKEND_BASE_URL } from '../config'; import { BACKEND_BASE_URL } from '../config';
import { AuthenticationOption } from '../interfaces'; import { AuthenticationOption } from '../interfaces';
import { pathFromFullUrl } from '../helpers'; import { parseTaskShowUrl } from '../helpers';
// NOTE: this currently stores the jwt token in local storage // NOTE: this currently stores the jwt token in local storage
// which is considered insecure. Server set cookies seem to be considered // which is considered insecure. Server set cookies seem to be considered
@ -36,13 +36,7 @@ const getCurrentLocation = (queryParams: string = window.location.search) => {
const checkPathForTaskShowParams = ( const checkPathForTaskShowParams = (
redirectUrl: string = window.location.pathname redirectUrl: string = window.location.pathname
) => { ) => {
const path = pathFromFullUrl(redirectUrl); const pathSegments = parseTaskShowUrl(redirectUrl);
// expected url pattern:
// /tasks/[process_instance_id]/[task_guid]
const pathSegments = path.match(
/^\/tasks\/(\d+)\/([0-9a-z]{8}-([0-9a-z]{4}-){3}[0-9a-z]{12})$/
);
if (pathSegments) { if (pathSegments) {
return { process_instance_id: pathSegments[1], task_guid: pathSegments[2] }; return { process_instance_id: pathSegments[1], task_guid: pathSegments[2] };
} }