mirror of
https://github.com/sartography/spiff-arena.git
synced 2025-01-27 09:45:12 +00:00
Feature/regex support in permissions (#530)
* support wildcards when adding users to groups from waiting table * moved the user route to authentication_controller to avoid having so many user routes and this controller was all about login * added test to ensure regexes work for permissions - still need to remove old ones on refresh * moved token related code out of authorization service and into authentication service w/ burnettk * remove old user group assignment waiting entries when refreshing permissions w/ burnettk --------- Co-authored-by: jasquat <jasquat@users.noreply.github.com>
This commit is contained in:
parent
01ef4e6eaa
commit
8bf92f7a39
@ -24,9 +24,9 @@ from spiffworkflow_backend.exceptions.api_error import api_error_blueprint
|
|||||||
from spiffworkflow_backend.helpers.api_version import V1_API_PATH_PREFIX
|
from spiffworkflow_backend.helpers.api_version import V1_API_PATH_PREFIX
|
||||||
from spiffworkflow_backend.models.db import db
|
from spiffworkflow_backend.models.db import db
|
||||||
from spiffworkflow_backend.models.db import migrate
|
from spiffworkflow_backend.models.db import migrate
|
||||||
|
from spiffworkflow_backend.routes.authentication_controller import _set_new_access_token_in_cookie
|
||||||
|
from spiffworkflow_backend.routes.authentication_controller import verify_token
|
||||||
from spiffworkflow_backend.routes.openid_blueprint.openid_blueprint import openid_blueprint
|
from spiffworkflow_backend.routes.openid_blueprint.openid_blueprint import openid_blueprint
|
||||||
from spiffworkflow_backend.routes.user import _set_new_access_token_in_cookie
|
|
||||||
from spiffworkflow_backend.routes.user import verify_token
|
|
||||||
from spiffworkflow_backend.routes.user_blueprint import user_blueprint
|
from spiffworkflow_backend.routes.user_blueprint import user_blueprint
|
||||||
from spiffworkflow_backend.services.authorization_service import AuthorizationService
|
from spiffworkflow_backend.services.authorization_service import AuthorizationService
|
||||||
from spiffworkflow_backend.services.background_processing_service import BackgroundProcessingService
|
from spiffworkflow_backend.services.background_processing_service import BackgroundProcessingService
|
||||||
|
@ -29,7 +29,7 @@ paths:
|
|||||||
type: string
|
type: string
|
||||||
get:
|
get:
|
||||||
summary: redirect to open id authentication server
|
summary: redirect to open id authentication server
|
||||||
operationId: spiffworkflow_backend.routes.user.login
|
operationId: spiffworkflow_backend.routes.authentication_controller.login
|
||||||
tags:
|
tags:
|
||||||
- Authentication
|
- Authentication
|
||||||
responses:
|
responses:
|
||||||
@ -53,7 +53,7 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
get:
|
get:
|
||||||
operationId: spiffworkflow_backend.routes.user.login_return
|
operationId: spiffworkflow_backend.routes.authentication_controller.login_return
|
||||||
tags:
|
tags:
|
||||||
- Authentication
|
- Authentication
|
||||||
responses:
|
responses:
|
||||||
@ -72,7 +72,7 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
get:
|
get:
|
||||||
operationId: spiffworkflow_backend.routes.user.logout
|
operationId: spiffworkflow_backend.routes.authentication_controller.logout
|
||||||
summary: Logout authenticated user
|
summary: Logout authenticated user
|
||||||
tags:
|
tags:
|
||||||
- Authentication
|
- Authentication
|
||||||
@ -81,7 +81,7 @@ paths:
|
|||||||
description: Logout Authenticated User
|
description: Logout Authenticated User
|
||||||
/logout_return:
|
/logout_return:
|
||||||
get:
|
get:
|
||||||
operationId: spiffworkflow_backend.routes.user.logout_return
|
operationId: spiffworkflow_backend.routes.authentication_controller.logout_return
|
||||||
summary: Logout authenticated user
|
summary: Logout authenticated user
|
||||||
tags:
|
tags:
|
||||||
- Authentication
|
- Authentication
|
||||||
@ -97,7 +97,7 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
post:
|
post:
|
||||||
operationId: spiffworkflow_backend.routes.user.login_with_access_token
|
operationId: spiffworkflow_backend.routes.authentication_controller.login_with_access_token
|
||||||
summary: Authenticate user for API access with an openid token already posessed.
|
summary: Authenticate user for API access with an openid token already posessed.
|
||||||
tags:
|
tags:
|
||||||
- Authentication
|
- Authentication
|
||||||
@ -111,7 +111,7 @@ paths:
|
|||||||
|
|
||||||
/login_api:
|
/login_api:
|
||||||
get:
|
get:
|
||||||
operationId: spiffworkflow_backend.routes.user.login_api
|
operationId: spiffworkflow_backend.routes.authentication_controller.login_api
|
||||||
summary: Authenticate user for API access
|
summary: Authenticate user for API access
|
||||||
tags:
|
tags:
|
||||||
- Authentication
|
- Authentication
|
||||||
@ -136,7 +136,7 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
get:
|
get:
|
||||||
operationId: spiffworkflow_backend.routes.user.login_api_return
|
operationId: spiffworkflow_backend.routes.authentication_controller.login_api_return
|
||||||
tags:
|
tags:
|
||||||
- Authentication
|
- Authentication
|
||||||
responses:
|
responses:
|
||||||
@ -2628,8 +2628,8 @@ components:
|
|||||||
type: http
|
type: http
|
||||||
scheme: bearer
|
scheme: bearer
|
||||||
bearerFormat: JWT
|
bearerFormat: JWT
|
||||||
x-bearerInfoFunc: spiffworkflow_backend.routes.user.verify_token
|
x-bearerInfoFunc: spiffworkflow_backend.routes.authentication_controller.verify_token
|
||||||
x-scopeValidateFunc: spiffworkflow_backend.routes.user.validate_scope
|
x-scopeValidateFunc: spiffworkflow_backend.routes.authentication_controller.validate_scope
|
||||||
|
|
||||||
oAuth2AuthCode:
|
oAuth2AuthCode:
|
||||||
type: oauth2
|
type: oauth2
|
||||||
@ -2640,7 +2640,7 @@ components:
|
|||||||
tokenUrl: /v1.0/login_api_return
|
tokenUrl: /v1.0/login_api_return
|
||||||
scopes:
|
scopes:
|
||||||
read_email: read email
|
read_email: read email
|
||||||
x-tokenInfoFunc: spiffworkflow_backend.routes.user.get_scope
|
x-tokenInfoFunc: spiffworkflow_backend.routes.authentication_controller.get_scope
|
||||||
|
|
||||||
schemas:
|
schemas:
|
||||||
OkTrue:
|
OkTrue:
|
||||||
|
@ -20,11 +20,11 @@ from SpiffWorkflow.exceptions import SpiffWorkflowException # type: ignore
|
|||||||
from SpiffWorkflow.exceptions import WorkflowException
|
from SpiffWorkflow.exceptions import WorkflowException
|
||||||
from SpiffWorkflow.specs.base import TaskSpec # type: ignore
|
from SpiffWorkflow.specs.base import TaskSpec # type: ignore
|
||||||
from SpiffWorkflow.task import Task # type: ignore
|
from SpiffWorkflow.task import Task # type: ignore
|
||||||
|
from spiffworkflow_backend.exceptions.error import NotAuthorizedError
|
||||||
|
from spiffworkflow_backend.exceptions.error import TokenInvalidError
|
||||||
|
from spiffworkflow_backend.exceptions.error import TokenNotProvidedError
|
||||||
|
from spiffworkflow_backend.exceptions.error import UserNotLoggedInError
|
||||||
from spiffworkflow_backend.models.task import TaskModel # noqa: F401
|
from spiffworkflow_backend.models.task import TaskModel # noqa: F401
|
||||||
from spiffworkflow_backend.services.authentication_service import NotAuthorizedError
|
|
||||||
from spiffworkflow_backend.services.authentication_service import TokenInvalidError
|
|
||||||
from spiffworkflow_backend.services.authentication_service import TokenNotProvidedError
|
|
||||||
from spiffworkflow_backend.services.authentication_service import UserNotLoggedInError
|
|
||||||
from spiffworkflow_backend.services.task_service import TaskModelError
|
from spiffworkflow_backend.services.task_service import TaskModelError
|
||||||
from spiffworkflow_backend.services.task_service import TaskService
|
from spiffworkflow_backend.services.task_service import TaskService
|
||||||
from werkzeug.exceptions import MethodNotAllowed
|
from werkzeug.exceptions import MethodNotAllowed
|
||||||
|
@ -0,0 +1,53 @@
|
|||||||
|
class MissingAccessTokenError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class RefreshTokenStorageError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# These could be either 'id' OR 'access' tokens and we can't always know which
|
||||||
|
|
||||||
|
|
||||||
|
class TokenExpiredError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TokenInvalidError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TokenNotProvidedError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class OpenIdConnectionError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class UserNotLoggedInError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class NotAuthorizedError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class PermissionsFileNotSetError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class HumanTaskNotFoundError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class HumanTaskAlreadyCompletedError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class UserDoesNotHaveAccessToTaskError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidPermissionError(Exception):
|
||||||
|
pass
|
@ -12,17 +12,21 @@ class UserGroupAssignmentWaitingModel(SpiffworkflowBaseDBModel):
|
|||||||
We cache it here to be applied in the event the user does log in to the system.
|
We cache it here to be applied in the event the user does log in to the system.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
MATCH_ALL_USERS = "*"
|
|
||||||
__tablename__ = "user_group_assignment_waiting"
|
__tablename__ = "user_group_assignment_waiting"
|
||||||
__table_args__ = (db.UniqueConstraint("username", "group_id", name="user_group_assignment_staged_unique"),)
|
__table_args__ = (db.UniqueConstraint("username", "group_id", name="user_group_assignment_staged_unique"),)
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id: int = db.Column(db.Integer, primary_key=True)
|
||||||
username = db.Column(db.String(255), nullable=False)
|
username: str = db.Column(db.String(255), nullable=False)
|
||||||
group_id = db.Column(ForeignKey(GroupModel.id), nullable=False, index=True)
|
group_id: int = db.Column(ForeignKey(GroupModel.id), nullable=False, index=True)
|
||||||
|
|
||||||
group = relationship("GroupModel", overlaps="groups,user_group_assignments_waiting,users") # type: ignore
|
group = relationship("GroupModel", overlaps="groups,user_group_assignments_waiting,users") # type: ignore
|
||||||
|
|
||||||
def is_match_all(self) -> bool:
|
def is_wildcard(self) -> bool:
|
||||||
if self.username == self.MATCH_ALL_USERS:
|
if self.username.startswith("REGEX:"):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def pattern_from_wildcard_username(self) -> str | None:
|
||||||
|
if self.is_wildcard():
|
||||||
|
return self.username.removeprefix("REGEX:")
|
||||||
|
return None
|
||||||
|
@ -15,6 +15,8 @@ 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 MissingAccessTokenError
|
||||||
|
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
|
||||||
from spiffworkflow_backend.models.group import SPIFF_GUEST_GROUP
|
from spiffworkflow_backend.models.group import SPIFF_GUEST_GROUP
|
||||||
from spiffworkflow_backend.models.group import SPIFF_NO_AUTH_GROUP
|
from spiffworkflow_backend.models.group import SPIFF_NO_AUTH_GROUP
|
||||||
@ -24,8 +26,6 @@ from spiffworkflow_backend.models.user import SPIFF_GUEST_USER
|
|||||||
from spiffworkflow_backend.models.user import SPIFF_NO_AUTH_USER
|
from spiffworkflow_backend.models.user import SPIFF_NO_AUTH_USER
|
||||||
from spiffworkflow_backend.models.user import UserModel
|
from spiffworkflow_backend.models.user import UserModel
|
||||||
from spiffworkflow_backend.services.authentication_service import AuthenticationService
|
from spiffworkflow_backend.services.authentication_service import AuthenticationService
|
||||||
from spiffworkflow_backend.services.authentication_service import MissingAccessTokenError
|
|
||||||
from spiffworkflow_backend.services.authentication_service import TokenExpiredError
|
|
||||||
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
|
||||||
|
|
||||||
@ -90,7 +90,7 @@ def verify_token(token: str | None = None, force_run: bool | None = False) -> No
|
|||||||
|
|
||||||
def login(redirect_url: str = "/", process_instance_id: int | None = None, task_guid: str | None = None) -> Response:
|
def login(redirect_url: str = "/", process_instance_id: int | None = None, task_guid: str | None = None) -> Response:
|
||||||
if current_app.config.get("SPIFFWORKFLOW_BACKEND_AUTHENTICATION_DISABLED"):
|
if current_app.config.get("SPIFFWORKFLOW_BACKEND_AUTHENTICATION_DISABLED"):
|
||||||
AuthorizationService.create_guest_token(
|
AuthenticationService.create_guest_token(
|
||||||
username=SPIFF_NO_AUTH_USER,
|
username=SPIFF_NO_AUTH_USER,
|
||||||
group_identifier=SPIFF_NO_AUTH_GROUP,
|
group_identifier=SPIFF_NO_AUTH_GROUP,
|
||||||
permission_target="/*",
|
permission_target="/*",
|
||||||
@ -99,7 +99,7 @@ def login(redirect_url: str = "/", process_instance_id: int | None = None, task_
|
|||||||
return redirect(redirect_url)
|
return redirect(redirect_url)
|
||||||
|
|
||||||
if process_instance_id and task_guid and TaskModel.task_guid_allows_guest(task_guid, process_instance_id):
|
if process_instance_id and task_guid and TaskModel.task_guid_allows_guest(task_guid, process_instance_id):
|
||||||
AuthorizationService.create_guest_token(
|
AuthenticationService.create_guest_token(
|
||||||
username=SPIFF_GUEST_USER,
|
username=SPIFF_GUEST_USER,
|
||||||
group_identifier=SPIFF_GUEST_GROUP,
|
group_identifier=SPIFF_GUEST_GROUP,
|
||||||
auth_token_properties={"only_guest_task_completion": True},
|
auth_token_properties={"only_guest_task_completion": True},
|
@ -20,7 +20,7 @@ from spiffworkflow_backend.models.process_instance_file_data import ProcessInsta
|
|||||||
from spiffworkflow_backend.models.process_model import ProcessModelInfo
|
from spiffworkflow_backend.models.process_model import ProcessModelInfo
|
||||||
from spiffworkflow_backend.models.reference_cache import ReferenceCacheModel
|
from spiffworkflow_backend.models.reference_cache import ReferenceCacheModel
|
||||||
from spiffworkflow_backend.models.reference_cache import ReferenceSchema
|
from spiffworkflow_backend.models.reference_cache import ReferenceSchema
|
||||||
from spiffworkflow_backend.models.task import TaskModel # noqa: F401
|
from spiffworkflow_backend.services.authentication_service import AuthenticationService # noqa: F401
|
||||||
from spiffworkflow_backend.services.authorization_service import AuthorizationService
|
from spiffworkflow_backend.services.authorization_service import AuthorizationService
|
||||||
from spiffworkflow_backend.services.git_service import GitService
|
from spiffworkflow_backend.services.git_service import GitService
|
||||||
from spiffworkflow_backend.services.process_caller_service import ProcessCallerService
|
from spiffworkflow_backend.services.process_caller_service import ProcessCallerService
|
||||||
@ -178,7 +178,7 @@ def process_data_file_download(
|
|||||||
# where 7000 is the port the app is running on locally
|
# where 7000 is the port the app is running on locally
|
||||||
def github_webhook_receive(body: dict) -> Response:
|
def github_webhook_receive(body: dict) -> Response:
|
||||||
auth_header = request.headers.get("X-Hub-Signature-256")
|
auth_header = request.headers.get("X-Hub-Signature-256")
|
||||||
AuthorizationService.verify_sha256_token(auth_header)
|
AuthenticationService.verify_sha256_token(auth_header)
|
||||||
result = GitService.handle_web_hook(body)
|
result = GitService.handle_web_hook(body)
|
||||||
return Response(json.dumps({"git_pull": result}), status=200, mimetype="application/json")
|
return Response(json.dumps({"git_pull": result}), status=200, mimetype="application/json")
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ from flask import request
|
|||||||
from flask.wrappers import Response
|
from flask.wrappers import Response
|
||||||
|
|
||||||
from spiffworkflow_backend.exceptions.api_error import ApiError
|
from spiffworkflow_backend.exceptions.api_error import ApiError
|
||||||
from spiffworkflow_backend.routes.user import verify_token
|
from spiffworkflow_backend.routes.authentication_controller import verify_token
|
||||||
from spiffworkflow_backend.services.oauth_service import OAuthService
|
from spiffworkflow_backend.services.oauth_service import OAuthService
|
||||||
from spiffworkflow_backend.services.secret_service import SecretService
|
from spiffworkflow_backend.services.secret_service import SecretService
|
||||||
from spiffworkflow_backend.services.service_task_service import ServiceTaskService
|
from spiffworkflow_backend.services.service_task_service import ServiceTaskService
|
||||||
|
@ -27,6 +27,9 @@ from sqlalchemy.orm import aliased
|
|||||||
from sqlalchemy.orm.util import AliasedClass
|
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.exceptions.error import HumanTaskAlreadyCompletedError
|
||||||
|
from spiffworkflow_backend.exceptions.error import HumanTaskNotFoundError
|
||||||
|
from spiffworkflow_backend.exceptions.error import UserDoesNotHaveAccessToTaskError
|
||||||
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
|
||||||
from spiffworkflow_backend.models.group import GroupModel
|
from spiffworkflow_backend.models.group import GroupModel
|
||||||
@ -49,9 +52,6 @@ from spiffworkflow_backend.routes.process_api_blueprint import _find_principal_o
|
|||||||
from spiffworkflow_backend.routes.process_api_blueprint import _find_process_instance_by_id_or_raise
|
from spiffworkflow_backend.routes.process_api_blueprint import _find_process_instance_by_id_or_raise
|
||||||
from spiffworkflow_backend.routes.process_api_blueprint import _get_process_model
|
from spiffworkflow_backend.routes.process_api_blueprint import _get_process_model
|
||||||
from spiffworkflow_backend.services.authorization_service import AuthorizationService
|
from spiffworkflow_backend.services.authorization_service import AuthorizationService
|
||||||
from spiffworkflow_backend.services.authorization_service import HumanTaskAlreadyCompletedError
|
|
||||||
from spiffworkflow_backend.services.authorization_service import HumanTaskNotFoundError
|
|
||||||
from spiffworkflow_backend.services.authorization_service import UserDoesNotHaveAccessToTaskError
|
|
||||||
from spiffworkflow_backend.services.error_handling_service import ErrorHandlingService
|
from spiffworkflow_backend.services.error_handling_service import ErrorHandlingService
|
||||||
from spiffworkflow_backend.services.file_system_service import FileSystemService
|
from spiffworkflow_backend.services.file_system_service import FileSystemService
|
||||||
from spiffworkflow_backend.services.git_service import GitCommandError
|
from spiffworkflow_backend.services.git_service import GitCommandError
|
||||||
|
@ -2,52 +2,29 @@ import base64
|
|||||||
import enum
|
import enum
|
||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
|
from hashlib import sha256
|
||||||
|
from hmac import HMAC
|
||||||
|
from hmac import compare_digest
|
||||||
|
|
||||||
import jwt
|
import jwt
|
||||||
import requests
|
import requests
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
|
from flask import g
|
||||||
from flask import redirect
|
from flask import redirect
|
||||||
|
from flask import request
|
||||||
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 RefreshTokenStorageError
|
||||||
|
from spiffworkflow_backend.exceptions.error import TokenExpiredError
|
||||||
|
from spiffworkflow_backend.exceptions.error import TokenInvalidError
|
||||||
|
from spiffworkflow_backend.exceptions.error import TokenNotProvidedError
|
||||||
from spiffworkflow_backend.models.db import db
|
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.user_service import UserService
|
||||||
from werkzeug.wrappers import Response
|
from werkzeug.wrappers import Response
|
||||||
|
|
||||||
|
|
||||||
class MissingAccessTokenError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class NotAuthorizedError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class RefreshTokenStorageError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class UserNotLoggedInError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
# These could be either 'id' OR 'access' tokens and we can't always know which
|
|
||||||
|
|
||||||
|
|
||||||
class TokenExpiredError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class TokenInvalidError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class TokenNotProvidedError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class OpenIdConnectionError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class AuthenticationProviderTypes(enum.Enum):
|
class AuthenticationProviderTypes(enum.Enum):
|
||||||
open_id = "open_id"
|
open_id = "open_id"
|
||||||
internal = "internal"
|
internal = "internal"
|
||||||
@ -249,3 +226,57 @@ 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)
|
||||||
return auth_token_object
|
return auth_token_object
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def decode_auth_token(auth_token: str) -> dict[str, str | None]:
|
||||||
|
secret_key = current_app.config.get("SECRET_KEY")
|
||||||
|
if secret_key is None:
|
||||||
|
raise KeyError("we need current_app.config to have a SECRET_KEY")
|
||||||
|
|
||||||
|
try:
|
||||||
|
payload = jwt.decode(auth_token, options={"verify_signature": False})
|
||||||
|
return payload
|
||||||
|
except jwt.ExpiredSignatureError as exception:
|
||||||
|
raise TokenExpiredError(
|
||||||
|
"The Authentication token you provided expired and must be renewed.",
|
||||||
|
) from exception
|
||||||
|
except jwt.InvalidTokenError as exception:
|
||||||
|
raise TokenInvalidError(
|
||||||
|
"The Authentication token you provided is invalid. You need a new token. ",
|
||||||
|
) from exception
|
||||||
|
|
||||||
|
# https://stackoverflow.com/a/71320673/6090676
|
||||||
|
@classmethod
|
||||||
|
def verify_sha256_token(cls, auth_header: str | None) -> None:
|
||||||
|
if auth_header is None:
|
||||||
|
raise TokenNotProvidedError(
|
||||||
|
"unauthorized",
|
||||||
|
)
|
||||||
|
|
||||||
|
received_sign = auth_header.split("sha256=")[-1].strip()
|
||||||
|
secret = current_app.config["SPIFFWORKFLOW_BACKEND_GITHUB_WEBHOOK_SECRET"].encode()
|
||||||
|
expected_sign = HMAC(key=secret, msg=request.data, digestmod=sha256).hexdigest()
|
||||||
|
if not compare_digest(received_sign, expected_sign):
|
||||||
|
raise TokenInvalidError(
|
||||||
|
"unauthorized",
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_guest_token(
|
||||||
|
cls,
|
||||||
|
username: str,
|
||||||
|
group_identifier: str,
|
||||||
|
permission_target: str | None = None,
|
||||||
|
permission: str = "all",
|
||||||
|
auth_token_properties: dict | None = None,
|
||||||
|
) -> None:
|
||||||
|
guest_user = UserService.find_or_create_guest_user(username=username, group_identifier=group_identifier)
|
||||||
|
if permission_target is not None:
|
||||||
|
AuthorizationService.add_permission_from_uri_or_macro(
|
||||||
|
group_identifier, permission=permission, target=permission_target
|
||||||
|
)
|
||||||
|
g.user = guest_user
|
||||||
|
g.token = guest_user.encode_auth_token(auth_token_properties)
|
||||||
|
tld = current_app.config["THREAD_LOCAL_DATA"]
|
||||||
|
tld.new_access_token = g.token
|
||||||
|
tld.new_id_token = g.token
|
||||||
|
@ -1,17 +1,20 @@
|
|||||||
import inspect
|
import inspect
|
||||||
import re
|
import re
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from hashlib import sha256
|
|
||||||
from hmac import HMAC
|
|
||||||
from hmac import compare_digest
|
|
||||||
from typing import TypedDict
|
from typing import TypedDict
|
||||||
|
|
||||||
import jwt
|
|
||||||
import yaml
|
import yaml
|
||||||
from flask import current_app
|
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 spiffworkflow_backend.exceptions.error import HumanTaskAlreadyCompletedError
|
||||||
|
from spiffworkflow_backend.exceptions.error import HumanTaskNotFoundError
|
||||||
|
from spiffworkflow_backend.exceptions.error import InvalidPermissionError
|
||||||
|
from spiffworkflow_backend.exceptions.error import NotAuthorizedError
|
||||||
|
from spiffworkflow_backend.exceptions.error import PermissionsFileNotSetError
|
||||||
|
from spiffworkflow_backend.exceptions.error import UserDoesNotHaveAccessToTaskError
|
||||||
|
from spiffworkflow_backend.exceptions.error import UserNotLoggedInError
|
||||||
from spiffworkflow_backend.helpers.api_version import V1_API_PATH_PREFIX
|
from spiffworkflow_backend.helpers.api_version import V1_API_PATH_PREFIX
|
||||||
from spiffworkflow_backend.models.db import db
|
from spiffworkflow_backend.models.db import db
|
||||||
from spiffworkflow_backend.models.group import SPIFF_GUEST_GROUP
|
from spiffworkflow_backend.models.group import SPIFF_GUEST_GROUP
|
||||||
@ -26,38 +29,14 @@ from spiffworkflow_backend.models.task import TaskModel # noqa: F401
|
|||||||
from spiffworkflow_backend.models.user import SPIFF_GUEST_USER
|
from spiffworkflow_backend.models.user import SPIFF_GUEST_USER
|
||||||
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.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.authentication_service import NotAuthorizedError
|
|
||||||
from spiffworkflow_backend.services.authentication_service import TokenExpiredError
|
|
||||||
from spiffworkflow_backend.services.authentication_service import TokenInvalidError
|
|
||||||
from spiffworkflow_backend.services.authentication_service import TokenNotProvidedError
|
|
||||||
from spiffworkflow_backend.services.authentication_service import UserNotLoggedInError
|
|
||||||
from spiffworkflow_backend.services.user_service import UserService
|
from spiffworkflow_backend.services.user_service import UserService
|
||||||
from sqlalchemy import and_
|
from sqlalchemy import and_
|
||||||
from sqlalchemy import or_
|
from sqlalchemy import or_
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
|
|
||||||
|
|
||||||
class PermissionsFileNotSetError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class HumanTaskNotFoundError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class HumanTaskAlreadyCompletedError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class UserDoesNotHaveAccessToTaskError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidPermissionError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class PermissionToAssign:
|
class PermissionToAssign:
|
||||||
permission: str
|
permission: str
|
||||||
@ -104,6 +83,7 @@ class AddedPermissionDict(TypedDict):
|
|||||||
group_identifiers: set[str]
|
group_identifiers: set[str]
|
||||||
permission_assignments: list[PermissionAssignmentModel]
|
permission_assignments: list[PermissionAssignmentModel]
|
||||||
user_to_group_identifiers: list[UserToGroupDict]
|
user_to_group_identifiers: list[UserToGroupDict]
|
||||||
|
waiting_user_group_assignments: list[UserGroupAssignmentWaitingModel]
|
||||||
|
|
||||||
|
|
||||||
class DesiredGroupPermissionDict(TypedDict):
|
class DesiredGroupPermissionDict(TypedDict):
|
||||||
@ -120,40 +100,6 @@ class GroupPermissionsDict(TypedDict):
|
|||||||
class AuthorizationService:
|
class AuthorizationService:
|
||||||
"""Determine whether a user has permission to perform their request."""
|
"""Determine whether a user has permission to perform their request."""
|
||||||
|
|
||||||
# https://stackoverflow.com/a/71320673/6090676
|
|
||||||
@classmethod
|
|
||||||
def verify_sha256_token(cls, auth_header: str | None) -> None:
|
|
||||||
if auth_header is None:
|
|
||||||
raise TokenNotProvidedError(
|
|
||||||
"unauthorized",
|
|
||||||
)
|
|
||||||
|
|
||||||
received_sign = auth_header.split("sha256=")[-1].strip()
|
|
||||||
secret = current_app.config["SPIFFWORKFLOW_BACKEND_GITHUB_WEBHOOK_SECRET"].encode()
|
|
||||||
expected_sign = HMAC(key=secret, msg=request.data, digestmod=sha256).hexdigest()
|
|
||||||
if not compare_digest(received_sign, expected_sign):
|
|
||||||
raise TokenInvalidError(
|
|
||||||
"unauthorized",
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def create_guest_token(
|
|
||||||
cls,
|
|
||||||
username: str,
|
|
||||||
group_identifier: str,
|
|
||||||
permission_target: str | None = None,
|
|
||||||
permission: str = "all",
|
|
||||||
auth_token_properties: dict | None = None,
|
|
||||||
) -> None:
|
|
||||||
guest_user = UserService.find_or_create_guest_user(username=username, group_identifier=group_identifier)
|
|
||||||
if permission_target is not None:
|
|
||||||
cls.add_permission_from_uri_or_macro(group_identifier, permission=permission, target=permission_target)
|
|
||||||
g.user = guest_user
|
|
||||||
g.token = guest_user.encode_auth_token(auth_token_properties)
|
|
||||||
tld = current_app.config["THREAD_LOCAL_DATA"]
|
|
||||||
tld.new_access_token = g.token
|
|
||||||
tld.new_id_token = g.token
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def has_permission(cls, principals: list[PrincipalModel], permission: str, target_uri: str) -> bool:
|
def has_permission(cls, principals: list[PrincipalModel], permission: str, target_uri: str) -> bool:
|
||||||
principal_ids = [p.id for p in principals]
|
principal_ids = [p.id for p in principals]
|
||||||
@ -390,24 +336,6 @@ class AuthorizationService:
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def decode_auth_token(auth_token: str) -> dict[str, str | None]:
|
|
||||||
secret_key = current_app.config.get("SECRET_KEY")
|
|
||||||
if secret_key is None:
|
|
||||||
raise KeyError("we need current_app.config to have a SECRET_KEY")
|
|
||||||
|
|
||||||
try:
|
|
||||||
payload = jwt.decode(auth_token, options={"verify_signature": False})
|
|
||||||
return payload
|
|
||||||
except jwt.ExpiredSignatureError as exception:
|
|
||||||
raise TokenExpiredError(
|
|
||||||
"The Authentication token you provided expired and must be renewed.",
|
|
||||||
) from exception
|
|
||||||
except jwt.InvalidTokenError as exception:
|
|
||||||
raise TokenInvalidError(
|
|
||||||
"The Authentication token you provided is invalid. You need a new token. ",
|
|
||||||
) from exception
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def assert_user_can_complete_task(
|
def assert_user_can_complete_task(
|
||||||
process_instance_id: int,
|
process_instance_id: int,
|
||||||
@ -827,6 +755,7 @@ class AuthorizationService:
|
|||||||
) -> AddedPermissionDict:
|
) -> AddedPermissionDict:
|
||||||
unique_user_group_identifiers: set[str] = set()
|
unique_user_group_identifiers: set[str] = set()
|
||||||
user_to_group_identifiers: list[UserToGroupDict] = []
|
user_to_group_identifiers: list[UserToGroupDict] = []
|
||||||
|
waiting_user_group_assignments: list[UserGroupAssignmentWaitingModel] = []
|
||||||
permission_assignments = []
|
permission_assignments = []
|
||||||
|
|
||||||
default_group = None
|
default_group = None
|
||||||
@ -847,8 +776,11 @@ class AuthorizationService:
|
|||||||
"group_identifier": group_identifier,
|
"group_identifier": group_identifier,
|
||||||
}
|
}
|
||||||
user_to_group_identifiers.append(user_to_group_dict)
|
user_to_group_identifiers.append(user_to_group_dict)
|
||||||
UserService.add_user_to_group_or_add_to_waiting(username, group_identifier)
|
wugam = UserService.add_user_to_group_or_add_to_waiting(username, group_identifier)
|
||||||
|
if wugam is not None:
|
||||||
|
waiting_user_group_assignments.append(wugam)
|
||||||
unique_user_group_identifiers.add(group_identifier)
|
unique_user_group_identifiers.add(group_identifier)
|
||||||
|
|
||||||
for group in group_permissions:
|
for group in group_permissions:
|
||||||
group_identifier = group["name"]
|
group_identifier = group["name"]
|
||||||
if user_model and group_identifier not in unique_user_group_identifiers:
|
if user_model and group_identifier not in unique_user_group_identifiers:
|
||||||
@ -875,6 +807,7 @@ class AuthorizationService:
|
|||||||
"group_identifiers": unique_user_group_identifiers,
|
"group_identifiers": unique_user_group_identifiers,
|
||||||
"permission_assignments": permission_assignments,
|
"permission_assignments": permission_assignments,
|
||||||
"user_to_group_identifiers": user_to_group_identifiers,
|
"user_to_group_identifiers": user_to_group_identifiers,
|
||||||
|
"waiting_user_group_assignments": waiting_user_group_assignments,
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -883,11 +816,13 @@ class AuthorizationService:
|
|||||||
added_permissions: AddedPermissionDict,
|
added_permissions: AddedPermissionDict,
|
||||||
initial_permission_assignments: list[PermissionAssignmentModel],
|
initial_permission_assignments: list[PermissionAssignmentModel],
|
||||||
initial_user_to_group_assignments: list[UserGroupAssignmentModel],
|
initial_user_to_group_assignments: list[UserGroupAssignmentModel],
|
||||||
|
initial_waiting_group_assignments: list[UserGroupAssignmentWaitingModel],
|
||||||
group_permissions_only: bool = False,
|
group_permissions_only: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
added_permission_assignments = added_permissions["permission_assignments"]
|
added_permission_assignments = added_permissions["permission_assignments"]
|
||||||
added_group_identifiers = added_permissions["group_identifiers"]
|
added_group_identifiers = added_permissions["group_identifiers"]
|
||||||
added_user_to_group_identifiers = added_permissions["user_to_group_identifiers"]
|
added_user_to_group_identifiers = added_permissions["user_to_group_identifiers"]
|
||||||
|
added_waiting_group_assignments = added_permissions["waiting_user_group_assignments"]
|
||||||
|
|
||||||
for ipa in initial_permission_assignments:
|
for ipa in initial_permission_assignments:
|
||||||
if ipa not in added_permission_assignments:
|
if ipa not in added_permission_assignments:
|
||||||
@ -913,6 +848,11 @@ class AuthorizationService:
|
|||||||
groups_to_delete = GroupModel.query.filter(GroupModel.identifier.not_in(added_group_identifiers)).all()
|
groups_to_delete = GroupModel.query.filter(GroupModel.identifier.not_in(added_group_identifiers)).all()
|
||||||
for gtd in groups_to_delete:
|
for gtd in groups_to_delete:
|
||||||
db.session.delete(gtd)
|
db.session.delete(gtd)
|
||||||
|
|
||||||
|
for wugam in initial_waiting_group_assignments:
|
||||||
|
if wugam not in added_waiting_group_assignments:
|
||||||
|
db.session.delete(wugam)
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -930,6 +870,7 @@ class AuthorizationService:
|
|||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
initial_user_to_group_assignments = UserGroupAssignmentModel.query.all()
|
initial_user_to_group_assignments = UserGroupAssignmentModel.query.all()
|
||||||
|
initial_waiting_group_assignments = UserGroupAssignmentWaitingModel.query.all()
|
||||||
group_permissions = group_permissions + cls.parse_permissions_yaml_into_group_info()
|
group_permissions = group_permissions + cls.parse_permissions_yaml_into_group_info()
|
||||||
added_permissions = cls.add_permissions_from_group_permissions(
|
added_permissions = cls.add_permissions_from_group_permissions(
|
||||||
group_permissions, group_permissions_only=group_permissions_only
|
group_permissions, group_permissions_only=group_permissions_only
|
||||||
@ -938,5 +879,6 @@ class AuthorizationService:
|
|||||||
added_permissions,
|
added_permissions,
|
||||||
initial_permission_assignments,
|
initial_permission_assignments,
|
||||||
initial_user_to_group_assignments,
|
initial_user_to_group_assignments,
|
||||||
|
initial_waiting_group_assignments,
|
||||||
group_permissions_only=group_permissions_only,
|
group_permissions_only=group_permissions_only,
|
||||||
)
|
)
|
||||||
|
@ -17,6 +17,9 @@ 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.exceptions.api_error import ApiError
|
from spiffworkflow_backend.exceptions.api_error import ApiError
|
||||||
|
from spiffworkflow_backend.exceptions.error import HumanTaskAlreadyCompletedError
|
||||||
|
from spiffworkflow_backend.exceptions.error import HumanTaskNotFoundError
|
||||||
|
from spiffworkflow_backend.exceptions.error import UserDoesNotHaveAccessToTaskError
|
||||||
from spiffworkflow_backend.models.group import GroupModel
|
from spiffworkflow_backend.models.group import GroupModel
|
||||||
from spiffworkflow_backend.models.human_task import HumanTaskModel
|
from spiffworkflow_backend.models.human_task import HumanTaskModel
|
||||||
from spiffworkflow_backend.models.process_instance import ProcessInstanceApi
|
from spiffworkflow_backend.models.process_instance import ProcessInstanceApi
|
||||||
@ -30,9 +33,6 @@ from spiffworkflow_backend.models.process_model_cycle import ProcessModelCycleMo
|
|||||||
from spiffworkflow_backend.models.task import Task
|
from spiffworkflow_backend.models.task import Task
|
||||||
from spiffworkflow_backend.models.user import UserModel
|
from spiffworkflow_backend.models.user import UserModel
|
||||||
from spiffworkflow_backend.services.authorization_service import AuthorizationService
|
from spiffworkflow_backend.services.authorization_service import AuthorizationService
|
||||||
from spiffworkflow_backend.services.authorization_service import HumanTaskAlreadyCompletedError
|
|
||||||
from spiffworkflow_backend.services.authorization_service import HumanTaskNotFoundError
|
|
||||||
from spiffworkflow_backend.services.authorization_service import UserDoesNotHaveAccessToTaskError
|
|
||||||
from spiffworkflow_backend.services.git_service import GitCommandError
|
from spiffworkflow_backend.services.git_service import GitCommandError
|
||||||
from spiffworkflow_backend.services.git_service import GitService
|
from spiffworkflow_backend.services.git_service import GitService
|
||||||
from spiffworkflow_backend.services.process_instance_processor import ProcessInstanceProcessor
|
from spiffworkflow_backend.services.process_instance_processor import ProcessInstanceProcessor
|
||||||
|
@ -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.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
|
||||||
from spiffworkflow_backend.models.file import File
|
from spiffworkflow_backend.models.file import File
|
||||||
@ -13,7 +14,6 @@ from spiffworkflow_backend.models.message_triggerable_process_model import Messa
|
|||||||
from spiffworkflow_backend.models.process_model import ProcessModelInfo
|
from spiffworkflow_backend.models.process_model import ProcessModelInfo
|
||||||
from spiffworkflow_backend.models.reference_cache import ReferenceCacheModel
|
from spiffworkflow_backend.models.reference_cache import ReferenceCacheModel
|
||||||
from spiffworkflow_backend.models.user import UserModel
|
from spiffworkflow_backend.models.user import UserModel
|
||||||
from spiffworkflow_backend.services.authentication_service import NotAuthorizedError
|
|
||||||
from spiffworkflow_backend.services.custom_parser import MyCustomParser
|
from spiffworkflow_backend.services.custom_parser import MyCustomParser
|
||||||
from spiffworkflow_backend.services.file_system_service import FileSystemService
|
from spiffworkflow_backend.services.file_system_service import FileSystemService
|
||||||
from spiffworkflow_backend.services.process_caller_service import ProcessCallerService
|
from spiffworkflow_backend.services.process_caller_service import ProcessCallerService
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import re
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
@ -125,22 +126,28 @@ class UserService:
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def add_waiting_group_assignment(cls, username: str, group: GroupModel) -> None:
|
def add_waiting_group_assignment(cls, username: str, group: GroupModel) -> UserGroupAssignmentWaitingModel:
|
||||||
wugam = (
|
"""Only called from set-permissions."""
|
||||||
|
wugam: UserGroupAssignmentWaitingModel | None = (
|
||||||
UserGroupAssignmentWaitingModel().query.filter_by(username=username).filter_by(group_id=group.id).first()
|
UserGroupAssignmentWaitingModel().query.filter_by(username=username).filter_by(group_id=group.id).first()
|
||||||
)
|
)
|
||||||
if not wugam:
|
if wugam is None:
|
||||||
wugam = UserGroupAssignmentWaitingModel(username=username, group_id=group.id)
|
wugam = UserGroupAssignmentWaitingModel(username=username, group_id=group.id)
|
||||||
db.session.add(wugam)
|
db.session.add(wugam)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
# to handle people who are already signed in
|
# backfill existing users
|
||||||
if wugam.is_match_all():
|
wildcard_pattern = wugam.pattern_from_wildcard_username()
|
||||||
for user in UserModel.query.all():
|
if wildcard_pattern is not None:
|
||||||
|
users = UserModel.query.filter(UserModel.username.regexp_match(wildcard_pattern)) # type: ignore
|
||||||
|
for user in users:
|
||||||
cls.add_user_to_group(user, group)
|
cls.add_user_to_group(user, group)
|
||||||
|
|
||||||
|
return wugam
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def apply_waiting_group_assignments(cls, user: UserModel) -> None:
|
def apply_waiting_group_assignments(cls, user: UserModel) -> None:
|
||||||
|
"""Only called from create_user which is normally called at sign-in time"""
|
||||||
waiting = (
|
waiting = (
|
||||||
UserGroupAssignmentWaitingModel()
|
UserGroupAssignmentWaitingModel()
|
||||||
.query.filter(UserGroupAssignmentWaitingModel.username == user.username)
|
.query.filter(UserGroupAssignmentWaitingModel.username == user.username)
|
||||||
@ -149,13 +156,14 @@ class UserService:
|
|||||||
for assignment in waiting:
|
for assignment in waiting:
|
||||||
cls.add_user_to_group(user, assignment.group)
|
cls.add_user_to_group(user, assignment.group)
|
||||||
db.session.delete(assignment)
|
db.session.delete(assignment)
|
||||||
wildcard = (
|
wildcards = (
|
||||||
UserGroupAssignmentWaitingModel()
|
UserGroupAssignmentWaitingModel()
|
||||||
.query.filter(UserGroupAssignmentWaitingModel.username == UserGroupAssignmentWaitingModel.MATCH_ALL_USERS)
|
.query.filter(UserGroupAssignmentWaitingModel.username.regexp_match("^REGEX:")) # type: ignore
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
for assignment in wildcard:
|
for wildcard in wildcards:
|
||||||
cls.add_user_to_group(user, assignment.group)
|
if re.match(wildcard.pattern_from_wildcard_username(), user.username):
|
||||||
|
cls.add_user_to_group(user, wildcard.group)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -226,13 +234,16 @@ class UserService:
|
|||||||
return group
|
return group
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def add_user_to_group_or_add_to_waiting(cls, username: str | UserModel, group_identifier: str) -> None:
|
def add_user_to_group_or_add_to_waiting(
|
||||||
|
cls, username: str | UserModel, group_identifier: str
|
||||||
|
) -> UserGroupAssignmentWaitingModel | None:
|
||||||
group = cls.find_or_create_group(group_identifier)
|
group = cls.find_or_create_group(group_identifier)
|
||||||
user = UserModel.query.filter_by(username=username).first()
|
user = UserModel.query.filter_by(username=username).first()
|
||||||
if user:
|
if user:
|
||||||
cls.add_user_to_group(user, group)
|
cls.add_user_to_group(user, group)
|
||||||
else:
|
else:
|
||||||
cls.add_waiting_group_assignment(username, group)
|
return cls.add_waiting_group_assignment(username, group)
|
||||||
|
return None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def add_user_to_group_by_group_identifier(cls, user: UserModel, group_identifier: str) -> None:
|
def add_user_to_group_by_group_identifier(cls, user: UserModel, group_identifier: str) -> None:
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
from flask.testing import FlaskClient
|
from flask.testing import FlaskClient
|
||||||
|
from spiffworkflow_backend.exceptions.error import InvalidPermissionError
|
||||||
from spiffworkflow_backend.models.group import GroupModel
|
from spiffworkflow_backend.models.group import GroupModel
|
||||||
|
from spiffworkflow_backend.models.user_group_assignment_waiting import UserGroupAssignmentWaitingModel
|
||||||
from spiffworkflow_backend.services.authorization_service import AuthorizationService
|
from spiffworkflow_backend.services.authorization_service import AuthorizationService
|
||||||
from spiffworkflow_backend.services.authorization_service import GroupPermissionsDict
|
from spiffworkflow_backend.services.authorization_service import GroupPermissionsDict
|
||||||
from spiffworkflow_backend.services.authorization_service import InvalidPermissionError
|
|
||||||
from spiffworkflow_backend.services.process_instance_processor import ProcessInstanceProcessor
|
from spiffworkflow_backend.services.process_instance_processor import ProcessInstanceProcessor
|
||||||
from spiffworkflow_backend.services.process_instance_service import ProcessInstanceService
|
from spiffworkflow_backend.services.process_instance_service import ProcessInstanceService
|
||||||
from spiffworkflow_backend.services.user_service import UserService
|
from spiffworkflow_backend.services.user_service import UserService
|
||||||
@ -527,3 +528,67 @@ class TestAuthorizationService(BaseTest):
|
|||||||
("/service-accounts", "create"),
|
("/service-accounts", "create"),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_can_refresh_permissions_with_regexes(
|
||||||
|
self,
|
||||||
|
app: Flask,
|
||||||
|
client: FlaskClient,
|
||||||
|
with_db_and_bpmn_file_cleanup: None,
|
||||||
|
) -> None:
|
||||||
|
user_regex = "REGEX:^user_.*"
|
||||||
|
user = self.find_or_create_user(username="user_one")
|
||||||
|
user_two = self.find_or_create_user(username="second_user_to_not_match_regex")
|
||||||
|
|
||||||
|
# this group is not mentioned so it will get deleted
|
||||||
|
UserService.find_or_create_group("group_two")
|
||||||
|
assert GroupModel.query.filter_by(identifier="group_two").first() is not None
|
||||||
|
|
||||||
|
UserService.find_or_create_group("group_three")
|
||||||
|
assert GroupModel.query.filter_by(identifier="group_three").first() is not None
|
||||||
|
|
||||||
|
group_info: list[GroupPermissionsDict] = [
|
||||||
|
{
|
||||||
|
"users": [user_regex],
|
||||||
|
"name": "group_one",
|
||||||
|
"permissions": [{"actions": ["create", "read"], "uri": "PG:hey"}],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
AuthorizationService.refresh_permissions(group_info)
|
||||||
|
waiting_assignments = UserGroupAssignmentWaitingModel.query.filter_by(username=user_regex).all()
|
||||||
|
assert len(waiting_assignments) == 1
|
||||||
|
assert waiting_assignments[0].username == user_regex
|
||||||
|
self.assert_user_has_permission(user, "read", "/v1.0/process-groups/hey")
|
||||||
|
self.assert_user_has_permission(user_two, "read", "/v1.0/process-groups/hey", expected_result=False)
|
||||||
|
|
||||||
|
user_three_dict = {
|
||||||
|
"username": "user_three",
|
||||||
|
"email": "user_three@example.com",
|
||||||
|
"iss": "test_service",
|
||||||
|
"sub": "unique_id_three",
|
||||||
|
}
|
||||||
|
# create the user using the same method that login uses by default as a sanity check
|
||||||
|
# and since we are testing the authorization service here anyway
|
||||||
|
user_three = AuthorizationService.create_user_from_sign_in(user_three_dict)
|
||||||
|
assert user_three is not None
|
||||||
|
group_identifiers = sorted([g.identifier for g in user_three.groups])
|
||||||
|
assert group_identifiers == ["everybody", "group_one"]
|
||||||
|
self.assert_user_has_permission(user_three, "read", "/v1.0/process-groups/hey")
|
||||||
|
|
||||||
|
# removing the regex removes permissions as well
|
||||||
|
group_info = [
|
||||||
|
{
|
||||||
|
"users": ["second_user_to_not_match_regex"],
|
||||||
|
"name": "group_one",
|
||||||
|
"permissions": [{"actions": ["create", "read"], "uri": "PG:hey"}],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
AuthorizationService.refresh_permissions(group_info)
|
||||||
|
waiting_assignments = UserGroupAssignmentWaitingModel.query.filter_by(username=user_regex).all()
|
||||||
|
assert len(waiting_assignments) == 0
|
||||||
|
self.assert_user_has_permission(user, "read", "/v1.0/process-groups/hey", expected_result=False)
|
||||||
|
self.assert_user_has_permission(user_three, "read", "/v1.0/process-groups/hey", expected_result=False)
|
||||||
|
self.assert_user_has_permission(user_two, "read", "/v1.0/process-groups/hey", expected_result=True)
|
||||||
|
|
||||||
|
waiting_assignments = UserGroupAssignmentWaitingModel.query.all()
|
||||||
|
# ensure we didn't delete all of the user group assignments
|
||||||
|
assert len(waiting_assignments) > 0
|
||||||
|
@ -6,6 +6,7 @@ from flask.app import Flask
|
|||||||
from flask.testing import FlaskClient
|
from flask.testing import FlaskClient
|
||||||
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.error import UserDoesNotHaveAccessToTaskError
|
||||||
from spiffworkflow_backend.models.bpmn_process import BpmnProcessModel
|
from spiffworkflow_backend.models.bpmn_process import BpmnProcessModel
|
||||||
from spiffworkflow_backend.models.db import db
|
from spiffworkflow_backend.models.db import db
|
||||||
from spiffworkflow_backend.models.group import GroupModel
|
from spiffworkflow_backend.models.group import GroupModel
|
||||||
@ -16,7 +17,6 @@ from spiffworkflow_backend.models.process_instance_event import ProcessInstanceE
|
|||||||
from spiffworkflow_backend.models.task import TaskModel # noqa: F401
|
from spiffworkflow_backend.models.task import TaskModel # noqa: F401
|
||||||
from spiffworkflow_backend.models.task_definition import TaskDefinitionModel
|
from spiffworkflow_backend.models.task_definition import TaskDefinitionModel
|
||||||
from spiffworkflow_backend.services.authorization_service import AuthorizationService
|
from spiffworkflow_backend.services.authorization_service import AuthorizationService
|
||||||
from spiffworkflow_backend.services.authorization_service import UserDoesNotHaveAccessToTaskError
|
|
||||||
from spiffworkflow_backend.services.process_instance_processor import ProcessInstanceProcessor
|
from spiffworkflow_backend.services.process_instance_processor import ProcessInstanceProcessor
|
||||||
from spiffworkflow_backend.services.process_instance_service import ProcessInstanceService
|
from spiffworkflow_backend.services.process_instance_service import ProcessInstanceService
|
||||||
from spiffworkflow_backend.services.workflow_execution_service import WorkflowExecutionServiceError
|
from spiffworkflow_backend.services.workflow_execution_service import WorkflowExecutionServiceError
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
"""Process Model."""
|
"""Process Model."""
|
||||||
from flask.app import Flask
|
from flask.app import Flask
|
||||||
from flask.testing import FlaskClient
|
from flask.testing import FlaskClient
|
||||||
from spiffworkflow_backend.models.user_group_assignment_waiting import UserGroupAssignmentWaitingModel
|
|
||||||
from spiffworkflow_backend.services.user_service import UserService
|
from spiffworkflow_backend.services.user_service import UserService
|
||||||
|
|
||||||
from tests.spiffworkflow_backend.helpers.base_test import BaseTest
|
from tests.spiffworkflow_backend.helpers.base_test import BaseTest
|
||||||
@ -26,7 +25,7 @@ class TestUserService(BaseTest):
|
|||||||
with_db_and_bpmn_file_cleanup: None,
|
with_db_and_bpmn_file_cleanup: None,
|
||||||
) -> None:
|
) -> None:
|
||||||
everybody_group = UserService.find_or_create_group("everybodyGroup")
|
everybody_group = UserService.find_or_create_group("everybodyGroup")
|
||||||
UserService.add_waiting_group_assignment(UserGroupAssignmentWaitingModel.MATCH_ALL_USERS, everybody_group)
|
UserService.add_waiting_group_assignment("REGEX:.*", everybody_group)
|
||||||
initiator_user = self.find_or_create_user("initiator_user")
|
initiator_user = self.find_or_create_user("initiator_user")
|
||||||
assert initiator_user.groups[0] == everybody_group
|
assert initiator_user.groups[0] == everybody_group
|
||||||
|
|
||||||
@ -38,5 +37,5 @@ class TestUserService(BaseTest):
|
|||||||
) -> None:
|
) -> None:
|
||||||
initiator_user = self.find_or_create_user("initiator_user")
|
initiator_user = self.find_or_create_user("initiator_user")
|
||||||
everybody_group = UserService.find_or_create_group("everybodyGroup")
|
everybody_group = UserService.find_or_create_group("everybodyGroup")
|
||||||
UserService.add_waiting_group_assignment(UserGroupAssignmentWaitingModel.MATCH_ALL_USERS, everybody_group)
|
UserService.add_waiting_group_assignment("REGEX:.*", everybody_group)
|
||||||
assert initiator_user.groups[0] == everybody_group
|
assert initiator_user.groups[0] == everybody_group
|
||||||
|
Loading…
x
Reference in New Issue
Block a user