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.models.db import db
|
||||
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.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.services.authorization_service import AuthorizationService
|
||||
from spiffworkflow_backend.services.background_processing_service import BackgroundProcessingService
|
||||
|
|
|
@ -29,7 +29,7 @@ paths:
|
|||
type: string
|
||||
get:
|
||||
summary: redirect to open id authentication server
|
||||
operationId: spiffworkflow_backend.routes.user.login
|
||||
operationId: spiffworkflow_backend.routes.authentication_controller.login
|
||||
tags:
|
||||
- Authentication
|
||||
responses:
|
||||
|
@ -53,7 +53,7 @@ paths:
|
|||
schema:
|
||||
type: string
|
||||
get:
|
||||
operationId: spiffworkflow_backend.routes.user.login_return
|
||||
operationId: spiffworkflow_backend.routes.authentication_controller.login_return
|
||||
tags:
|
||||
- Authentication
|
||||
responses:
|
||||
|
@ -72,7 +72,7 @@ paths:
|
|||
schema:
|
||||
type: string
|
||||
get:
|
||||
operationId: spiffworkflow_backend.routes.user.logout
|
||||
operationId: spiffworkflow_backend.routes.authentication_controller.logout
|
||||
summary: Logout authenticated user
|
||||
tags:
|
||||
- Authentication
|
||||
|
@ -81,7 +81,7 @@ paths:
|
|||
description: Logout Authenticated User
|
||||
/logout_return:
|
||||
get:
|
||||
operationId: spiffworkflow_backend.routes.user.logout_return
|
||||
operationId: spiffworkflow_backend.routes.authentication_controller.logout_return
|
||||
summary: Logout authenticated user
|
||||
tags:
|
||||
- Authentication
|
||||
|
@ -97,7 +97,7 @@ paths:
|
|||
schema:
|
||||
type: string
|
||||
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.
|
||||
tags:
|
||||
- Authentication
|
||||
|
@ -111,7 +111,7 @@ paths:
|
|||
|
||||
/login_api:
|
||||
get:
|
||||
operationId: spiffworkflow_backend.routes.user.login_api
|
||||
operationId: spiffworkflow_backend.routes.authentication_controller.login_api
|
||||
summary: Authenticate user for API access
|
||||
tags:
|
||||
- Authentication
|
||||
|
@ -136,7 +136,7 @@ paths:
|
|||
schema:
|
||||
type: string
|
||||
get:
|
||||
operationId: spiffworkflow_backend.routes.user.login_api_return
|
||||
operationId: spiffworkflow_backend.routes.authentication_controller.login_api_return
|
||||
tags:
|
||||
- Authentication
|
||||
responses:
|
||||
|
@ -2628,8 +2628,8 @@ components:
|
|||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
x-bearerInfoFunc: spiffworkflow_backend.routes.user.verify_token
|
||||
x-scopeValidateFunc: spiffworkflow_backend.routes.user.validate_scope
|
||||
x-bearerInfoFunc: spiffworkflow_backend.routes.authentication_controller.verify_token
|
||||
x-scopeValidateFunc: spiffworkflow_backend.routes.authentication_controller.validate_scope
|
||||
|
||||
oAuth2AuthCode:
|
||||
type: oauth2
|
||||
|
@ -2640,7 +2640,7 @@ components:
|
|||
tokenUrl: /v1.0/login_api_return
|
||||
scopes:
|
||||
read_email: read email
|
||||
x-tokenInfoFunc: spiffworkflow_backend.routes.user.get_scope
|
||||
x-tokenInfoFunc: spiffworkflow_backend.routes.authentication_controller.get_scope
|
||||
|
||||
schemas:
|
||||
OkTrue:
|
||||
|
|
|
@ -20,11 +20,11 @@ from SpiffWorkflow.exceptions import SpiffWorkflowException # type: ignore
|
|||
from SpiffWorkflow.exceptions import WorkflowException
|
||||
from SpiffWorkflow.specs.base import TaskSpec # 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.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 TaskService
|
||||
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.
|
||||
"""
|
||||
|
||||
MATCH_ALL_USERS = "*"
|
||||
__tablename__ = "user_group_assignment_waiting"
|
||||
__table_args__ = (db.UniqueConstraint("username", "group_id", name="user_group_assignment_staged_unique"),)
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
username = db.Column(db.String(255), nullable=False)
|
||||
group_id = db.Column(ForeignKey(GroupModel.id), nullable=False, index=True)
|
||||
id: int = db.Column(db.Integer, primary_key=True)
|
||||
username: str = db.Column(db.String(255), nullable=False)
|
||||
group_id: int = db.Column(ForeignKey(GroupModel.id), nullable=False, index=True)
|
||||
|
||||
group = relationship("GroupModel", overlaps="groups,user_group_assignments_waiting,users") # type: ignore
|
||||
|
||||
def is_match_all(self) -> bool:
|
||||
if self.username == self.MATCH_ALL_USERS:
|
||||
def is_wildcard(self) -> bool:
|
||||
if self.username.startswith("REGEX:"):
|
||||
return True
|
||||
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 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.models.group import SPIFF_GUEST_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 UserModel
|
||||
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.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:
|
||||
if current_app.config.get("SPIFFWORKFLOW_BACKEND_AUTHENTICATION_DISABLED"):
|
||||
AuthorizationService.create_guest_token(
|
||||
AuthenticationService.create_guest_token(
|
||||
username=SPIFF_NO_AUTH_USER,
|
||||
group_identifier=SPIFF_NO_AUTH_GROUP,
|
||||
permission_target="/*",
|
||||
|
@ -99,7 +99,7 @@ def login(redirect_url: str = "/", process_instance_id: int | None = None, task_
|
|||
return redirect(redirect_url)
|
||||
|
||||
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,
|
||||
group_identifier=SPIFF_GUEST_GROUP,
|
||||
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.reference_cache import ReferenceCacheModel
|
||||
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.git_service import GitService
|
||||
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
|
||||
def github_webhook_receive(body: dict) -> Response:
|
||||
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)
|
||||
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 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.secret_service import SecretService
|
||||
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 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 db
|
||||
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 _get_process_model
|
||||
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.file_system_service import FileSystemService
|
||||
from spiffworkflow_backend.services.git_service import GitCommandError
|
||||
|
|
|
@ -2,52 +2,29 @@ import base64
|
|||
import enum
|
||||
import json
|
||||
import time
|
||||
from hashlib import sha256
|
||||
from hmac import HMAC
|
||||
from hmac import compare_digest
|
||||
|
||||
import jwt
|
||||
import requests
|
||||
from flask import current_app
|
||||
from flask import g
|
||||
from flask import redirect
|
||||
from flask import request
|
||||
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.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 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):
|
||||
open_id = "open_id"
|
||||
internal = "internal"
|
||||
|
@ -249,3 +226,57 @@ class AuthenticationService:
|
|||
response = requests.post(request_url, data=data, headers=headers, timeout=HTTP_REQUEST_TIMEOUT_SECONDS)
|
||||
auth_token_object: dict = json.loads(response.text)
|
||||
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 re
|
||||
from dataclasses import dataclass
|
||||
from hashlib import sha256
|
||||
from hmac import HMAC
|
||||
from hmac import compare_digest
|
||||
from typing import TypedDict
|
||||
|
||||
import jwt
|
||||
import yaml
|
||||
from flask import current_app
|
||||
from flask import g
|
||||
from flask import request
|
||||
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.models.db import db
|
||||
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 UserModel
|
||||
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.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 sqlalchemy import and_
|
||||
from sqlalchemy import or_
|
||||
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
|
||||
class PermissionToAssign:
|
||||
permission: str
|
||||
|
@ -104,6 +83,7 @@ class AddedPermissionDict(TypedDict):
|
|||
group_identifiers: set[str]
|
||||
permission_assignments: list[PermissionAssignmentModel]
|
||||
user_to_group_identifiers: list[UserToGroupDict]
|
||||
waiting_user_group_assignments: list[UserGroupAssignmentWaitingModel]
|
||||
|
||||
|
||||
class DesiredGroupPermissionDict(TypedDict):
|
||||
|
@ -120,40 +100,6 @@ class GroupPermissionsDict(TypedDict):
|
|||
class AuthorizationService:
|
||||
"""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
|
||||
def has_permission(cls, principals: list[PrincipalModel], permission: str, target_uri: str) -> bool:
|
||||
principal_ids = [p.id for p in principals]
|
||||
|
@ -390,24 +336,6 @@ class AuthorizationService:
|
|||
return True
|
||||
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
|
||||
def assert_user_can_complete_task(
|
||||
process_instance_id: int,
|
||||
|
@ -827,6 +755,7 @@ class AuthorizationService:
|
|||
) -> AddedPermissionDict:
|
||||
unique_user_group_identifiers: set[str] = set()
|
||||
user_to_group_identifiers: list[UserToGroupDict] = []
|
||||
waiting_user_group_assignments: list[UserGroupAssignmentWaitingModel] = []
|
||||
permission_assignments = []
|
||||
|
||||
default_group = None
|
||||
|
@ -847,8 +776,11 @@ class AuthorizationService:
|
|||
"group_identifier": group_identifier,
|
||||
}
|
||||
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)
|
||||
|
||||
for group in group_permissions:
|
||||
group_identifier = group["name"]
|
||||
if user_model and group_identifier not in unique_user_group_identifiers:
|
||||
|
@ -875,6 +807,7 @@ class AuthorizationService:
|
|||
"group_identifiers": unique_user_group_identifiers,
|
||||
"permission_assignments": permission_assignments,
|
||||
"user_to_group_identifiers": user_to_group_identifiers,
|
||||
"waiting_user_group_assignments": waiting_user_group_assignments,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
|
@ -883,11 +816,13 @@ class AuthorizationService:
|
|||
added_permissions: AddedPermissionDict,
|
||||
initial_permission_assignments: list[PermissionAssignmentModel],
|
||||
initial_user_to_group_assignments: list[UserGroupAssignmentModel],
|
||||
initial_waiting_group_assignments: list[UserGroupAssignmentWaitingModel],
|
||||
group_permissions_only: bool = False,
|
||||
) -> None:
|
||||
added_permission_assignments = added_permissions["permission_assignments"]
|
||||
added_group_identifiers = added_permissions["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:
|
||||
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()
|
||||
for gtd in groups_to_delete:
|
||||
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()
|
||||
|
||||
@classmethod
|
||||
|
@ -930,6 +870,7 @@ class AuthorizationService:
|
|||
.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()
|
||||
added_permissions = cls.add_permissions_from_group_permissions(
|
||||
group_permissions, group_permissions_only=group_permissions_only
|
||||
|
@ -938,5 +879,6 @@ class AuthorizationService:
|
|||
added_permissions,
|
||||
initial_permission_assignments,
|
||||
initial_user_to_group_assignments,
|
||||
initial_waiting_group_assignments,
|
||||
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_backend import db
|
||||
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.human_task import HumanTaskModel
|
||||
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.user import UserModel
|
||||
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 GitService
|
||||
from spiffworkflow_backend.services.process_instance_processor import ProcessInstanceProcessor
|
||||
|
|
|
@ -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
|
||||
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.reference_cache import ReferenceCacheModel
|
||||
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.file_system_service import FileSystemService
|
||||
from spiffworkflow_backend.services.process_caller_service import ProcessCallerService
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import re
|
||||
from typing import Any
|
||||
|
||||
from flask import current_app
|
||||
|
@ -125,22 +126,28 @@ class UserService:
|
|||
db.session.commit()
|
||||
|
||||
@classmethod
|
||||
def add_waiting_group_assignment(cls, username: str, group: GroupModel) -> None:
|
||||
wugam = (
|
||||
def add_waiting_group_assignment(cls, username: str, group: GroupModel) -> UserGroupAssignmentWaitingModel:
|
||||
"""Only called from set-permissions."""
|
||||
wugam: UserGroupAssignmentWaitingModel | None = (
|
||||
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)
|
||||
db.session.add(wugam)
|
||||
db.session.commit()
|
||||
|
||||
# to handle people who are already signed in
|
||||
if wugam.is_match_all():
|
||||
for user in UserModel.query.all():
|
||||
# backfill existing users
|
||||
wildcard_pattern = wugam.pattern_from_wildcard_username()
|
||||
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)
|
||||
|
||||
return wugam
|
||||
|
||||
@classmethod
|
||||
def apply_waiting_group_assignments(cls, user: UserModel) -> None:
|
||||
"""Only called from create_user which is normally called at sign-in time"""
|
||||
waiting = (
|
||||
UserGroupAssignmentWaitingModel()
|
||||
.query.filter(UserGroupAssignmentWaitingModel.username == user.username)
|
||||
|
@ -149,13 +156,14 @@ class UserService:
|
|||
for assignment in waiting:
|
||||
cls.add_user_to_group(user, assignment.group)
|
||||
db.session.delete(assignment)
|
||||
wildcard = (
|
||||
wildcards = (
|
||||
UserGroupAssignmentWaitingModel()
|
||||
.query.filter(UserGroupAssignmentWaitingModel.username == UserGroupAssignmentWaitingModel.MATCH_ALL_USERS)
|
||||
.query.filter(UserGroupAssignmentWaitingModel.username.regexp_match("^REGEX:")) # type: ignore
|
||||
.all()
|
||||
)
|
||||
for assignment in wildcard:
|
||||
cls.add_user_to_group(user, assignment.group)
|
||||
for wildcard in wildcards:
|
||||
if re.match(wildcard.pattern_from_wildcard_username(), user.username):
|
||||
cls.add_user_to_group(user, wildcard.group)
|
||||
db.session.commit()
|
||||
|
||||
@staticmethod
|
||||
|
@ -226,13 +234,16 @@ class UserService:
|
|||
return group
|
||||
|
||||
@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)
|
||||
user = UserModel.query.filter_by(username=username).first()
|
||||
if user:
|
||||
cls.add_user_to_group(user, group)
|
||||
else:
|
||||
cls.add_waiting_group_assignment(username, group)
|
||||
return cls.add_waiting_group_assignment(username, group)
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def add_user_to_group_by_group_identifier(cls, user: UserModel, group_identifier: str) -> None:
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import pytest
|
||||
from flask import Flask
|
||||
from flask.testing import FlaskClient
|
||||
from spiffworkflow_backend.exceptions.error import InvalidPermissionError
|
||||
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 GroupPermissionsDict
|
||||
from spiffworkflow_backend.services.authorization_service import InvalidPermissionError
|
||||
from spiffworkflow_backend.services.process_instance_processor import ProcessInstanceProcessor
|
||||
from spiffworkflow_backend.services.process_instance_service import ProcessInstanceService
|
||||
from spiffworkflow_backend.services.user_service import UserService
|
||||
|
@ -527,3 +528,67 @@ class TestAuthorizationService(BaseTest):
|
|||
("/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 SpiffWorkflow.task import Task as SpiffTask # 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.db import db
|
||||
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_definition import TaskDefinitionModel
|
||||
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_service import ProcessInstanceService
|
||||
from spiffworkflow_backend.services.workflow_execution_service import WorkflowExecutionServiceError
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
"""Process Model."""
|
||||
from flask.app import Flask
|
||||
from flask.testing import FlaskClient
|
||||
from spiffworkflow_backend.models.user_group_assignment_waiting import UserGroupAssignmentWaitingModel
|
||||
from spiffworkflow_backend.services.user_service import UserService
|
||||
|
||||
from tests.spiffworkflow_backend.helpers.base_test import BaseTest
|
||||
|
@ -26,7 +25,7 @@ class TestUserService(BaseTest):
|
|||
with_db_and_bpmn_file_cleanup: None,
|
||||
) -> None:
|
||||
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")
|
||||
assert initiator_user.groups[0] == everybody_group
|
||||
|
||||
|
@ -38,5 +37,5 @@ class TestUserService(BaseTest):
|
|||
) -> None:
|
||||
initiator_user = self.find_or_create_user("initiator_user")
|
||||
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
|
||||
|
|
Loading…
Reference in New Issue