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:
FLASK_SESSION_SECRET_KEY: super_secret_key
FORCE_COLOR: "1"
NOXSESSION: ${{ matrix.session }}
PRE_COMMIT_COLOR: "always"
SPIFFWORKFLOW_BACKEND_DATABASE_PASSWORD: password
SPIFFWORKFLOW_BACKEND_DATABASE_TYPE: ${{ matrix.database }}
@ -171,7 +170,7 @@ jobs:
uses: "actions/upload-artifact@v3"
with:
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
# 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:
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:
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}")

View File

@ -2285,6 +2285,33 @@ paths:
schema:
$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
/tasks/{process_instance_id}/send-user-signal-event:
parameters:
@ -3013,6 +3040,10 @@ components:
documentation: "# Heading 1\n\nMarkdown documentation text goes here"
type: form
state: ready
TaskAllowsGuest:
properties:
allows_guest:
type: boolean
Task:
properties:
id:

View File

@ -3,7 +3,6 @@ from __future__ import annotations
import json
from dataclasses import dataclass
from dataclasses import field
from typing import Any
import flask.wrappers
@ -38,18 +37,18 @@ class ApiError(Exception):
error_code: str
message: str
error_line: str | None = ""
error_type: str | None = ""
file_name: str | None = ""
line_number: int | None = 0
offset: int | None = 0
error_line: str | None = None
error_type: str | None = None
file_name: str | None = None
line_number: int | None = None
offset: int | None = None
sentry_link: str | None = None
status_code: int | None = 400
tag: str | None = ""
task_data: dict | str | None = field(default_factory=dict)
task_id: str | None = ""
task_name: str | None = ""
task_trace: list | None = field(default_factory=list)
tag: str | None = None
task_data: dict | str | None = None
task_id: str | None = None
task_name: str | None = None
task_trace: list | None = None
# 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
@ -67,6 +66,14 @@ class ApiError(Exception):
msg += f"In file {self.file_name}. "
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
def from_task(
cls,
@ -74,17 +81,17 @@ class ApiError(Exception):
message: str,
task: Task,
status_code: int = 400,
line_number: int = 0,
offset: int = 0,
error_type: str = "",
error_line: str = "",
line_number: int | None = None,
offset: int | None = None,
error_type: str | None = None,
error_line: str | None = None,
task_trace: list | None = None,
) -> ApiError:
"""Constructs an API Error with details pulled from the current task."""
instance = cls(error_code, message, status_code=status_code)
instance.task_id = task.task_spec.name or ""
instance.task_name = task.task_spec.description or ""
instance.file_name = task.workflow.spec.file or ""
instance.task_id = task.task_spec.name
instance.task_name = task.task_spec.description
instance.file_name = task.workflow.spec.file
instance.line_number = line_number
instance.offset = offset
instance.error_type = error_type
@ -110,17 +117,17 @@ class ApiError(Exception):
message: str,
task_model: TaskModel,
status_code: int | None = 400,
line_number: int | None = 0,
offset: int | None = 0,
error_type: str | None = "",
error_line: str | None = "",
line_number: int | None = None,
offset: int | None = None,
error_type: str | None = None,
error_line: str | None = None,
task_trace: list | None = None,
) -> ApiError:
"""Constructs an API Error with details pulled from the current task model."""
instance = cls(error_code, message, status_code=status_code)
task_definition = task_model.task_definition
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.offset = offset
instance.error_type = error_type

View File

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

View File

@ -15,6 +15,7 @@ from flask import request
from werkzeug.wrappers import Response
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 TokenExpiredError
from spiffworkflow_backend.helpers.api_version import V1_API_PATH_PREFIX
@ -99,6 +100,12 @@ def login(
process_instance_id: int | None = None,
task_guid: str | None = None,
) -> 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"):
AuthenticationService.create_guest_token(
username=SPIFF_NO_AUTH_USER,

View File

@ -80,6 +80,16 @@ class ReactJsonSchemaSelectOption(TypedDict):
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
def task_list_my_tasks(
process_instance_id: int | None = None, page: int = 1, per_page: int = 100

View File

@ -1,11 +1,11 @@
import time
from flask import current_app
from spiffworkflow_backend.models.db import db
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus
from spiffworkflow_backend.services.process_instance_service import ProcessInstanceService
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 redirect
from flask import request
from werkzeug.wrappers import Response
from spiffworkflow_backend.config import HTTP_REQUEST_TIMEOUT_SECONDS
from spiffworkflow_backend.exceptions.error import OpenIdConnectionError
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.services.authorization_service import AuthorizationService
from spiffworkflow_backend.services.user_service import UserService
from werkzeug.wrappers import Response
class AuthenticationProviderTypes(enum.Enum):
@ -118,7 +119,9 @@ class AuthenticationService:
if redirect_url is None:
redirect_url = f"{self.get_backend_url()}/v1.0/logout_return"
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"id_token_hint={id_token}"
)
@ -137,7 +140,7 @@ class AuthenticationService:
) -> str:
return_redirect_url = f"{self.get_backend_url()}{redirect_url}"
login_redirect_url = (
self.open_id_endpoint_for_name(
self.__class__.open_id_endpoint_for_name(
"authorization_endpoint", authentication_identifier=authentication_identifier
)
+ f"?state={state}&"
@ -146,7 +149,6 @@ class AuthenticationService:
+ "scope=openid profile email&"
+ f"redirect_uri={return_redirect_url}"
)
print(f"login_redirect_url: {login_redirect_url}")
return login_redirect_url
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)
auth_token_object: dict = json.loads(response.text)
print(f"auth_token_object: {auth_token_object}")
return auth_token_object
@classmethod

View File

@ -7,6 +7,11 @@ from flask import current_app
from flask import g
from flask import request
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 HumanTaskNotFoundError
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.routes.openid_blueprint import openid_blueprint
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
@ -228,11 +229,15 @@ class AuthorizationService:
def should_disable_auth_for_request(cls) -> bool:
swagger_functions = ["get_json_spec"]
authentication_exclusion_list = [
"status",
"test_raise_error",
"authentication_begin",
"authentication_callback",
"authentication_options",
"github_webhook_receive",
"prometheus_metrics",
"status",
"task_allows_guest",
"test_raise_error",
"url_info",
]
if request.method == "OPTIONS":
return True
@ -250,10 +255,6 @@ class AuthorizationService:
api_view_function
and api_view_function.__name__.startswith("login")
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__ in authentication_exclusion_list
or api_view_function.__name__ in swagger_functions

View File

@ -1,4 +1,5 @@
import flask
from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus
from spiffworkflow_backend.services.message_service import MessageService
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.spiff.parser.process import SpiffBpmnParser # 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.services.service_task_service import CustomServiceTask
from spiffworkflow_backend.specs.start_event import StartEvent

View File

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

View File

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

View File

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

View File

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

View File

@ -5,6 +5,7 @@ import jinja2
from jinja2 import TemplateSyntaxError
from SpiffWorkflow.bpmn.exceptions import WorkflowTaskException # type: ignore
from SpiffWorkflow.task import Task as SpiffTask # type: ignore
from spiffworkflow_backend.exceptions.api_error import ApiError
from spiffworkflow_backend.models.task import TaskModel # noqa: F401
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.specs.event_definitions.message import CorrelationProperty # type: ignore
from SpiffWorkflow.spiff.specs.event_definitions import MessageEventDefinition # type: ignore
from spiffworkflow_backend.models.db import db
from spiffworkflow_backend.models.message_instance import MessageInstanceModel
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 session
from flask_oauthlib.client import OAuth # type: ignore
from spiffworkflow_backend.exceptions.api_error import ApiError
from spiffworkflow_backend.services.configuration_service import ConfigurationService
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.process_caller import ProcessCallerCacheModel
from sqlalchemy import or_
class ProcessCallerService:

View File

@ -3,11 +3,12 @@ import time
from typing import Any
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 or_
from spiffworkflow_backend.models.db import db
from spiffworkflow_backend.models.process_instance_queue import ProcessInstanceQueueModel
class ExpectedLockNotFoundError(Exception):
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.task import TaskIterator # type: ignore
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 JSONDataStoreConverter
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 execution_strategy_named
from spiffworkflow_backend.specs.start_event import StartEvent
from sqlalchemy import and_
SPIFF_CONFIG[StandardLoopTask] = StandardLoopTaskConverter

View File

@ -6,6 +6,13 @@ from typing import Any
import sqlalchemy
from flask import current_app
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.models.db import SpiffworkflowBaseDBModel
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_group_assignment import UserGroupAssignmentModel
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):

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.task import Task as SpiffTask # type: ignore
from SpiffWorkflow.util.task import TaskState # type: ignore
from spiffworkflow_backend import db
from spiffworkflow_backend.data_migrations.process_instance_migrator import ProcessInstanceMigrator
from spiffworkflow_backend.exceptions.api_error import ApiError

View File

@ -3,6 +3,7 @@ import traceback
from flask import g
from SpiffWorkflow.bpmn.exceptions import WorkflowTaskException # type: ignore
from spiffworkflow_backend.models.db import db
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
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 flask import current_app
from spiffworkflow_backend.exceptions.api_error import ApiError
from spiffworkflow_backend.exceptions.process_entity_not_found_error import ProcessEntityNotFoundError
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.task import Task as SpiffTask # type: ignore
from SpiffWorkflow.util.task import TaskState # type: ignore
from spiffworkflow_backend.services.custom_parser import MyCustomParser

View File

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

View File

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

View File

@ -2,6 +2,7 @@ import re
import sentry_sdk
from flask import current_app
from spiffworkflow_backend.exceptions.api_error import ApiError
from spiffworkflow_backend.models.db import db
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.task import Task as SpiffTask # 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 HTTP_REQUEST_TIMEOUT_SECONDS
from spiffworkflow_backend.services.file_system_service import FileSystemService
from spiffworkflow_backend.services.secret_service import SecretService
from spiffworkflow_backend.services.user_service import UserService
from spiffworkflow_connector_command.command_interface import CommandErrorDict
class ConnectorProxyError(Exception):

View File

@ -4,6 +4,7 @@ from datetime import datetime
from lxml import etree # type: ignore
from SpiffWorkflow.bpmn.parser.BpmnParser import BpmnValidator # type: ignore
from spiffworkflow_backend.exceptions.error import NotAuthorizedError
from spiffworkflow_backend.models.correlation_property_cache import CorrelationPropertyCache
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.task import Task as SpiffTask # 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 BpmnProcessNotFoundError
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 g
from sqlalchemy import and_
from spiffworkflow_backend.exceptions.api_error import ApiError
from spiffworkflow_backend.interfaces import UserToGroupDict
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 UserGroupAssignmentNotFoundError
from spiffworkflow_backend.models.user_group_assignment_waiting import UserGroupAssignmentWaitingModel
from sqlalchemy import and_
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.task import Task as SpiffTask # type: ignore
from SpiffWorkflow.util.task import TaskState # type: ignore
from spiffworkflow_backend.exceptions.api_error import ApiError
from spiffworkflow_backend.models.db import db
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.task import Task as SpiffTask # 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 StartEvent

View File

@ -1,6 +1,8 @@
import ast
import base64
import re
import time
from typing import Any
from flask.app import Flask
from flask.testing import FlaskClient
@ -116,3 +118,42 @@ class TestAuthentication(BaseTest):
UserService.get_permission_targets_for_user(service_account.user, check_groups=False)
)
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
# 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(
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];
};
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 { useEffect, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { Loading, Button, Grid, Column } from '@carbon/react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { AuthenticationOption } from '../interfaces';
import HttpService from '../services/HttpService';
import UserService from '../services/UserService';
import { parseTaskShowUrl } from '../helpers';
export default function Login() {
const navigate = useNavigate();
@ -12,20 +13,34 @@ export default function Login() {
const [authenticationOptions, setAuthenticationOptions] = useState<
AuthenticationOption[] | 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(() => {
HttpService.makeCallToBackend({
path: '/authentication-options',
successCallback: setAuthenticationOptions,
});
}, []);
const getOriginalUrl = () => {
const originalUrl = searchParams.get('original_url');
if (originalUrl === '/login') {
return '/';
const pathSegments = parseTaskShowUrl(getOriginalUrl() || '');
if (pathSegments) {
HttpService.makeCallToBackend({
path: `/tasks/${pathSegments[1]}/${pathSegments[2]}/allows-guest`,
successCallback: (result: any) =>
setAllowsGuestLogin(result.allows_guest),
});
} else {
setAllowsGuestLogin(false);
}
return originalUrl;
};
}, [getOriginalUrl]);
const authenticationOptionButtons = () => {
if (!authenticationOptions) {
@ -48,15 +63,6 @@ export default function Login() {
return buttons;
};
const doLoginIfAppropriate = () => {
if (!authenticationOptions) {
return;
}
if (authenticationOptions.length === 1) {
UserService.doLogin(authenticationOptions[0], getOriginalUrl());
}
};
const getLoadingIcon = () => {
const style = { margin: '50px 0 50px 50px' };
return (
@ -98,8 +104,7 @@ export default function Login() {
return null;
}
if (authenticationOptions === null || authenticationOptions.length < 2) {
doLoginIfAppropriate();
if (authenticationOptions === null) {
return (
<div className="fixed-width-container login-page-spacer">
{getLoadingIcon()}
@ -107,8 +112,11 @@ export default function Login() {
);
}
if (authenticationOptions !== null) {
doLoginIfAppropriate();
if (authenticationOptions !== null && allowsGuestLogin !== null) {
if (allowsGuestLogin || authenticationOptions.length === 1) {
UserService.doLogin(authenticationOptions[0], getOriginalUrl());
return null;
}
return loginComponments();
}

View File

@ -2,7 +2,7 @@ import jwt from 'jwt-decode';
import cookie from 'cookie';
import { BACKEND_BASE_URL } from '../config';
import { AuthenticationOption } from '../interfaces';
import { pathFromFullUrl } from '../helpers';
import { parseTaskShowUrl } from '../helpers';
// NOTE: this currently stores the jwt token in local storage
// which is considered insecure. Server set cookies seem to be considered
@ -36,13 +36,7 @@ const getCurrentLocation = (queryParams: string = window.location.search) => {
const checkPathForTaskShowParams = (
redirectUrl: string = window.location.pathname
) => {
const path = pathFromFullUrl(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})$/
);
const pathSegments = parseTaskShowUrl(redirectUrl);
if (pathSegments) {
return { process_instance_id: pathSegments[1], task_guid: pathSegments[2] };
}