Merge pull request #266 from sartography/feature/support_macros_in_permission_yaml

Feature/support macros in permission yaml
This commit is contained in:
jasquat 2023-05-19 11:49:23 -04:00 committed by GitHub
commit 9ff0169fd3
14 changed files with 208 additions and 287 deletions

View File

@ -1122,7 +1122,7 @@ paths:
- Process Instances - Process Instances
responses: responses:
"200": "200":
description: Empty ok true response on successful resume. description: Empty ok true response on successful reset.
content: content:
application/json: application/json:
schema: schema:

View File

@ -143,6 +143,7 @@ SPIFFWORKFLOW_BACKEND_ALLOW_CONFISCATING_LOCK_AFTER_SECONDS = int(
environ.get("SPIFFWORKFLOW_BACKEND_ALLOW_CONFISCATING_LOCK_AFTER_SECONDS", default="600") environ.get("SPIFFWORKFLOW_BACKEND_ALLOW_CONFISCATING_LOCK_AFTER_SECONDS", default="600")
) )
# FIXME: do not default this but we will need to coordinate release of it since it is a breaking change
SPIFFWORKFLOW_BACKEND_DEFAULT_USER_GROUP = environ.get("SPIFFWORKFLOW_BACKEND_DEFAULT_USER_GROUP", default="everybody") SPIFFWORKFLOW_BACKEND_DEFAULT_USER_GROUP = environ.get("SPIFFWORKFLOW_BACKEND_DEFAULT_USER_GROUP", default="everybody")
SPIFFWORKFLOW_BACKEND_ENGINE_STEP_DEFAULT_STRATEGY_BACKGROUND = environ.get( SPIFFWORKFLOW_BACKEND_ENGINE_STEP_DEFAULT_STRATEGY_BACKGROUND = environ.get(

View File

@ -12,6 +12,5 @@ groups:
permissions: permissions:
admin: admin:
groups: [admin] groups: [admin]
users: []
allowed_permissions: [create, read, update, delete] allowed_permissions: [create, read, update, delete]
uri: /* uri: /*

View File

@ -1,5 +1,3 @@
default_group: everybody
groups: groups:
admin: admin:
users: users:
@ -19,6 +17,5 @@ groups:
permissions: permissions:
admin: admin:
groups: [admin, tech_writers] groups: [admin, tech_writers]
users: []
allowed_permissions: [create, read, update, delete] allowed_permissions: [create, read, update, delete]
uri: /* uri: /*

View File

@ -1,4 +1,3 @@
default_group: everybody
users: users:
admin: admin:
@ -41,52 +40,43 @@ permissions:
# Admins have access to everything. # Admins have access to everything.
admin: admin:
groups: [admin] groups: [admin]
users: []
allowed_permissions: [create, read, update, delete] allowed_permissions: [create, read, update, delete]
uri: /* uri: /*
# Everybody can participate in tasks assigned to them. # Everybody can participate in tasks assigned to them.
tasks-crud: tasks-crud:
groups: [everybody] groups: [everybody]
users: []
allowed_permissions: [create, read, update, delete] allowed_permissions: [create, read, update, delete]
uri: /tasks/* uri: /tasks/*
# Everybody can start all intstances # Everybody can start all intstances
create-test-instances: create-test-instances:
groups: [ everybody ] groups: [ everybody ]
users: [ ]
allowed_permissions: [ create ] allowed_permissions: [ create ]
uri: /process-instances/* uri: /process-instances/*
# Everyone can see everything (all groups, and processes are visible) # Everyone can see everything (all groups, and processes are visible)
read-all-process-groups: read-all-process-groups:
groups: [ everybody ] groups: [ everybody ]
users: [ ]
allowed_permissions: [ read ] allowed_permissions: [ read ]
uri: /process-groups/* uri: /process-groups/*
read-all-process-models: read-all-process-models:
groups: [ everybody ] groups: [ everybody ]
users: [ ]
allowed_permissions: [ read ] allowed_permissions: [ read ]
uri: /process-models/* uri: /process-models/*
read-all-process-instance: read-all-process-instance:
groups: [ everybody ] groups: [ everybody ]
users: [ ]
allowed_permissions: [ read ] allowed_permissions: [ read ]
uri: /process-instances/* uri: /process-instances/*
read-process-instance-reports: read-process-instance-reports:
groups: [ everybody ] groups: [ everybody ]
users: [ ]
allowed_permissions: [ read ] allowed_permissions: [ read ]
uri: /process-instances/reports/* uri: /process-instances/reports/*
processes-read: processes-read:
groups: [ everybody ] groups: [ everybody ]
users: [ ]
allowed_permissions: [ read ] allowed_permissions: [ read ]
uri: /processes uri: /processes
groups-everybody: groups-everybody:
groups: [everybody] groups: [everybody]
users: []
allowed_permissions: [create, read] allowed_permissions: [create, read]
uri: /v1.0/user-groups/for-current-user uri: /v1.0/user-groups/for-current-user

View File

@ -1,84 +1,17 @@
default_group: everybody
groups: groups:
admin: admin:
users: [admin@spiffworkflow.org] users: [admin@spiffworkflow.org]
permissions: permissions:
admin: process-groups-ro:
groups: [admin] groups: [admin]
users: []
allowed_permissions: [read] allowed_permissions: [read]
uri: /* uri: PG:ALL
basic:
tasks-crud:
groups: [admin] groups: [admin]
users: [] allowed_permissions: [ALL]
allowed_permissions: [create, update, delete] uri: BASIC
uri: /tasks/* elevated-operations:
process-instances-crud:
groups: [ admin ]
users: [ ]
allowed_permissions: [create, update, delete]
uri: /process-instances/*
suspend:
groups: [admin] groups: [admin]
users: [] allowed_permissions: [ALL]
allowed_permissions: [create] uri: ELEVATED
uri: /v1.0/process-instance-suspend
terminate:
groups: [admin]
users: []
allowed_permissions: [create]
uri: /v1.0/process-instance-terminate
resume:
groups: [admin]
users: []
allowed_permissions: [create]
uri: /v1.0/process-instance-resume
reset:
groups: [admin]
users: []
allowed_permissions: [create]
uri: /v1.0/process-instance-reset
users-exist:
groups: [admin]
users: []
allowed_permissions: [create]
uri: /v1.0/users/exists/by-username
send-event:
groups: [admin]
users: []
allowed_permissions: [create]
uri: /v1.0/send-event/*
task-complete:
groups: [admin]
users: []
allowed_permissions: [create]
uri: /v1.0/task-complete/*
messages:
groups: [admin]
users: []
allowed_permissions: [create]
uri: /v1.0/messages/*
secrets:
groups: [admin]
users: []
allowed_permissions: [create, update, delete]
uri: /v1.0/secrets/*
task-data:
groups: [admin]
users: []
allowed_permissions: [update]
uri: /v1.0/task-data/*

View File

@ -1,4 +1,3 @@
default_group: everybody
groups: groups:
admin: admin:
@ -11,6 +10,5 @@ groups:
permissions: permissions:
admin: admin:
groups: [admin, group1, group2] groups: [admin, group1, group2]
users: []
allowed_permissions: [create, read, update, delete] allowed_permissions: [create, read, update, delete]
uri: /* uri: /*

View File

@ -1,4 +1,3 @@
default_group: everybody
groups: groups:
admin: admin:
@ -7,6 +6,5 @@ groups:
permissions: permissions:
admin: admin:
groups: [admin] groups: [admin]
users: []
allowed_permissions: [create, read, update, delete] allowed_permissions: [create, read, update, delete]
uri: /* uri: /*

View File

@ -1,5 +1,3 @@
default_group: everybody
users: users:
testadmin1: testadmin1:
service: https://testing/openid/thing service: https://testing/openid/thing
@ -20,49 +18,40 @@ groups:
permissions: permissions:
admin: admin:
groups: [admin] groups: [admin]
users: []
allowed_permissions: [create, read, update, delete] allowed_permissions: [create, read, update, delete]
uri: /* uri: /*
read-all: read-all:
groups: ["Finance Team", hr, admin] groups: ["Finance Team", hr, admin]
users: []
allowed_permissions: [read] allowed_permissions: [read]
uri: /* uri: /*
process-instances-find-by-id: process-instances-find-by-id:
groups: [everybody] groups: [everybody]
users: []
allowed_permissions: [read] allowed_permissions: [read]
uri: /process-instances/find-by-id/* uri: /process-instances/find-by-id/*
tasks-crud: tasks-crud:
groups: [everybody] groups: [everybody]
users: []
allowed_permissions: [create, read, update, delete] allowed_permissions: [create, read, update, delete]
uri: /tasks/* uri: /tasks/*
# TODO: all uris should really have the same structure
finance-admin-group: finance-admin-group:
groups: ["Finance Team"] groups: ["Finance Team"]
users: [testuser4] allowed_permissions: [all]
allowed_permissions: [create, read, update, delete] uri: PG:finance
uri: /process-groups/finance/*
finance-admin-model: finance-hr-start:
groups: ["Finance Team"] groups: ["hr"]
users: [testuser4] allowed_permissions: [start]
allowed_permissions: [create, read, update, delete] uri: PG:finance
uri: /process-models/finance/*
finance-admin-model-lanes: finance-admin-model-lanes:
groups: ["Finance Team"] groups: ["Finance Team"]
users: [testuser4]
allowed_permissions: [create, read, update, delete] allowed_permissions: [create, read, update, delete]
uri: /process-models/finance:model_with_lanes/* uri: /process-models/finance:model_with_lanes/*
finance-admin-instance-run: finance-admin-instance-run:
groups: ["Finance Team"] groups: ["Finance Team"]
users: [testuser4]
allowed_permissions: [create, read, update, delete] allowed_permissions: [create, read, update, delete]
uri: /process-instances/* uri: /process-instances/*

View File

@ -34,6 +34,5 @@ class RefreshPermissions(Script):
*args: Any, *args: Any,
**kwargs: Any, **kwargs: Any,
) -> Any: ) -> Any:
"""Run."""
group_info = args[0] group_info = args[0]
AuthorizationService.refresh_permissions(group_info) AuthorizationService.refresh_permissions(group_info)

View File

@ -1,11 +1,9 @@
"""Authorization_service."""
import inspect import inspect
import re import re
from dataclasses import dataclass from dataclasses import dataclass
from hashlib import sha256 from hashlib import sha256
from hmac import compare_digest from hmac import compare_digest
from hmac import HMAC from hmac import HMAC
from typing import Any
from typing import Optional from typing import Optional
from typing import Set from typing import Set
from typing import TypedDict from typing import TypedDict
@ -29,7 +27,6 @@ from spiffworkflow_backend.models.permission_target import PermissionTargetModel
from spiffworkflow_backend.models.principal import MissingPrincipalError from spiffworkflow_backend.models.principal import MissingPrincipalError
from spiffworkflow_backend.models.principal import PrincipalModel from spiffworkflow_backend.models.principal import PrincipalModel
from spiffworkflow_backend.models.user import UserModel from spiffworkflow_backend.models.user import UserModel
from spiffworkflow_backend.models.user import UserNotFoundError
from spiffworkflow_backend.models.user_group_assignment import UserGroupAssignmentModel from spiffworkflow_backend.models.user_group_assignment import UserGroupAssignmentModel
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 NotAuthorizedError
@ -42,25 +39,23 @@ from spiffworkflow_backend.services.user_service import UserService
class PermissionsFileNotSetError(Exception): class PermissionsFileNotSetError(Exception):
"""PermissionsFileNotSetError.""" pass
class HumanTaskNotFoundError(Exception): class HumanTaskNotFoundError(Exception):
"""HumanTaskNotFoundError.""" pass
class UserDoesNotHaveAccessToTaskError(Exception): class UserDoesNotHaveAccessToTaskError(Exception):
"""UserDoesNotHaveAccessToTaskError.""" pass
class InvalidPermissionError(Exception): class InvalidPermissionError(Exception):
"""InvalidPermissionError.""" pass
@dataclass @dataclass
class PermissionToAssign: class PermissionToAssign:
"""PermissionToAssign."""
permission: str permission: str
target_uri: str target_uri: str
@ -93,21 +88,29 @@ class UserToGroupDict(TypedDict):
group_identifier: str group_identifier: str
class DesiredPermissionDict(TypedDict): class AddedPermissionDict(TypedDict):
"""DesiredPermissionDict."""
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]
class DesiredGroupPermissionDict(TypedDict):
actions: list[str]
uri: str
class GroupPermissionsDict(TypedDict):
users: list[str]
name: str
permissions: list[DesiredGroupPermissionDict]
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 # https://stackoverflow.com/a/71320673/6090676
@classmethod @classmethod
def verify_sha256_token(cls, auth_header: Optional[str]) -> None: def verify_sha256_token(cls, auth_header: Optional[str]) -> None:
"""Verify_sha256_token."""
if auth_header is None: if auth_header is None:
raise TokenNotProvidedError( raise TokenNotProvidedError(
"unauthorized", "unauthorized",
@ -123,7 +126,6 @@ class AuthorizationService:
@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:
"""Has_permission."""
principal_ids = [p.id for p in principals] principal_ids = [p.id for p in principals]
target_uri_normalized = target_uri.removeprefix(V1_API_PATH_PREFIX) target_uri_normalized = target_uri.removeprefix(V1_API_PATH_PREFIX)
@ -153,7 +155,6 @@ class AuthorizationService:
@classmethod @classmethod
def user_has_permission(cls, user: UserModel, permission: str, target_uri: str) -> bool: def user_has_permission(cls, user: UserModel, permission: str, target_uri: str) -> bool:
"""User_has_permission."""
if user.principal is None: if user.principal is None:
raise MissingPrincipalError(f"Missing principal for user with id: {user.id}") raise MissingPrincipalError(f"Missing principal for user with id: {user.id}")
@ -179,7 +180,6 @@ class AuthorizationService:
@classmethod @classmethod
def associate_user_with_group(cls, user: UserModel, group: GroupModel) -> None: def associate_user_with_group(cls, user: UserModel, group: GroupModel) -> None:
"""Associate_user_with_group."""
user_group_assignemnt = UserGroupAssignmentModel.query.filter_by(user_id=user.id, group_id=group.id).first() user_group_assignemnt = UserGroupAssignmentModel.query.filter_by(user_id=user.id, group_id=group.id).first()
if user_group_assignemnt is None: if user_group_assignemnt is None:
user_group_assignemnt = UserGroupAssignmentModel(user_id=user.id, group_id=group.id) user_group_assignemnt = UserGroupAssignmentModel(user_id=user.id, group_id=group.id)
@ -187,88 +187,13 @@ class AuthorizationService:
db.session.commit() db.session.commit()
@classmethod @classmethod
def import_permissions_from_yaml_file(cls, raise_if_missing_user: bool = False) -> DesiredPermissionDict: def import_permissions_from_yaml_file(cls, user_model: Optional[UserModel] = None) -> AddedPermissionDict:
"""Import_permissions_from_yaml_file.""" group_permissions = cls.parse_permissions_yaml_into_group_info()
if current_app.config["SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME"] is None: result = cls.add_permissions_from_group_permissions(group_permissions, user_model)
raise ( return result
PermissionsFileNotSetError(
"SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME needs to be set in order to import permissions"
)
)
permission_configs = None
with open(current_app.config["SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_ABSOLUTE_PATH"]) as file:
permission_configs = yaml.safe_load(file)
default_group = None
unique_user_group_identifiers: Set[str] = set()
user_to_group_identifiers: list[UserToGroupDict] = []
if "default_group" in permission_configs:
default_group_identifier = permission_configs["default_group"]
default_group = GroupService.find_or_create_group(default_group_identifier)
unique_user_group_identifiers.add(default_group_identifier)
if "groups" in permission_configs:
for group_identifier, group_config in permission_configs["groups"].items():
group = GroupService.find_or_create_group(group_identifier)
unique_user_group_identifiers.add(group_identifier)
for username in group_config["users"]:
user = UserModel.query.filter_by(username=username).first()
if user is None:
if raise_if_missing_user:
raise (UserNotFoundError(f"Could not find a user with name: {username}"))
continue
user_to_group_dict: UserToGroupDict = {
"username": user.username,
"group_identifier": group_identifier,
}
user_to_group_identifiers.append(user_to_group_dict)
cls.associate_user_with_group(user, group)
permission_assignments = []
if "permissions" in permission_configs:
for _permission_identifier, permission_config in permission_configs["permissions"].items():
uri = permission_config["uri"]
permission_target = cls.find_or_create_permission_target(uri)
for allowed_permission in permission_config["allowed_permissions"]:
if "groups" in permission_config:
for group_identifier in permission_config["groups"]:
group = GroupService.find_or_create_group(group_identifier)
unique_user_group_identifiers.add(group_identifier)
permission_assignments.append(
cls.create_permission_for_principal(
group.principal,
permission_target,
allowed_permission,
)
)
if "users" in permission_config:
for username in permission_config["users"]:
user = UserModel.query.filter_by(username=username).first()
if user is not None:
principal = (
PrincipalModel.query.join(UserModel).filter(UserModel.username == username).first()
)
permission_assignments.append(
cls.create_permission_for_principal(
principal, permission_target, allowed_permission
)
)
if default_group is not None:
for user in UserModel.query.all():
cls.associate_user_with_group(user, default_group)
return {
"group_identifiers": unique_user_group_identifiers,
"permission_assignments": permission_assignments,
"user_to_group_identifiers": user_to_group_identifiers,
}
@classmethod @classmethod
def find_or_create_permission_target(cls, uri: str) -> PermissionTargetModel: def find_or_create_permission_target(cls, uri: str) -> PermissionTargetModel:
"""Find_or_create_permission_target."""
uri_with_percent = re.sub(r"\*", "%", uri) uri_with_percent = re.sub(r"\*", "%", uri)
target_uri_normalized = uri_with_percent.removeprefix(V1_API_PATH_PREFIX) target_uri_normalized = uri_with_percent.removeprefix(V1_API_PATH_PREFIX)
permission_target: Optional[PermissionTargetModel] = PermissionTargetModel.query.filter_by( permission_target: Optional[PermissionTargetModel] = PermissionTargetModel.query.filter_by(
@ -287,7 +212,6 @@ class AuthorizationService:
permission_target: PermissionTargetModel, permission_target: PermissionTargetModel,
permission: str, permission: str,
) -> PermissionAssignmentModel: ) -> PermissionAssignmentModel:
"""Create_permission_for_principal."""
permission_assignment: Optional[PermissionAssignmentModel] = PermissionAssignmentModel.query.filter_by( permission_assignment: Optional[PermissionAssignmentModel] = PermissionAssignmentModel.query.filter_by(
principal_id=principal.id, principal_id=principal.id,
permission_target_id=permission_target.id, permission_target_id=permission_target.id,
@ -306,7 +230,6 @@ class AuthorizationService:
@classmethod @classmethod
def should_disable_auth_for_request(cls) -> bool: def should_disable_auth_for_request(cls) -> bool:
"""Should_disable_auth_for_request."""
swagger_functions = ["get_json_spec"] swagger_functions = ["get_json_spec"]
authentication_exclusion_list = [ authentication_exclusion_list = [
"status", "status",
@ -344,7 +267,6 @@ class AuthorizationService:
@classmethod @classmethod
def get_permission_from_http_method(cls, http_method: str) -> Optional[str]: def get_permission_from_http_method(cls, http_method: str) -> Optional[str]:
"""Get_permission_from_request_method."""
request_method_mapper = { request_method_mapper = {
"POST": "create", "POST": "create",
"GET": "read", "GET": "read",
@ -363,7 +285,6 @@ class AuthorizationService:
@classmethod @classmethod
def check_for_permission(cls) -> None: def check_for_permission(cls) -> None:
"""Check_for_permission."""
if cls.should_disable_auth_for_request(): if cls.should_disable_auth_for_request():
return None return None
@ -397,11 +318,6 @@ class AuthorizationService:
@staticmethod @staticmethod
def decode_auth_token(auth_token: str) -> dict[str, Union[str, None]]: def decode_auth_token(auth_token: str) -> dict[str, Union[str, None]]:
"""Decode the auth token.
:param auth_token:
:return: integer|string
"""
secret_key = current_app.config.get("SECRET_KEY") secret_key = current_app.config.get("SECRET_KEY")
if secret_key is None: if secret_key is None:
raise KeyError("we need current_app.config to have a SECRET_KEY") raise KeyError("we need current_app.config to have a SECRET_KEY")
@ -445,10 +361,11 @@ class AuthorizationService:
@classmethod @classmethod
def create_user_from_sign_in(cls, user_info: dict) -> UserModel: def create_user_from_sign_in(cls, user_info: dict) -> UserModel:
"""Create_user_from_sign_in.""" """Fields from user_info.
"""Name, family_name, given_name, middle_name, nickname, preferred_username,"""
"""Profile, picture, website, gender, birthdate, zoneinfo, locale, and updated_at. """ name, family_name, given_name, middle_name, nickname, preferred_username,
"""Email.""" profile, picture, website, gender, birthdate, zoneinfo, locale,updated_at, email.
"""
is_new_user = False is_new_user = False
user_attributes = {} user_attributes = {}
@ -506,7 +423,7 @@ class AuthorizationService:
# we are also a little apprehensive about pre-creating users # we are also a little apprehensive about pre-creating users
# before the user signs in, because we won't know things like # before the user signs in, because we won't know things like
# the external service user identifier. # the external service user identifier.
cls.import_permissions_from_yaml_file() cls.import_permissions_from_yaml_file(user_model)
if is_new_user: if is_new_user:
UserService.add_user_to_human_tasks_if_appropriate(user_model) UserService.add_user_to_human_tasks_if_appropriate(user_model)
@ -521,11 +438,6 @@ class AuthorizationService:
process_related_path_segment: str, process_related_path_segment: str,
target_uris: list[str], target_uris: list[str],
) -> list[PermissionToAssign]: ) -> list[PermissionToAssign]:
"""Get_permissions_to_assign."""
permissions = permission_set.split(",")
if permission_set == "all":
permissions = ["create", "read", "update", "delete"]
permissions_to_assign: list[PermissionToAssign] = [] permissions_to_assign: list[PermissionToAssign] = []
# we were thinking that if you can start an instance, you ought to be able to: # we were thinking that if you can start an instance, you ought to be able to:
@ -556,7 +468,9 @@ class AuthorizationService:
]: ]:
permissions_to_assign.append(PermissionToAssign(permission="read", target_uri=target_uri)) permissions_to_assign.append(PermissionToAssign(permission="read", target_uri=target_uri))
else: else:
permissions = permission_set.split(",")
if permission_set == "all": if permission_set == "all":
permissions = ["create", "read", "update", "delete"]
for path_segment_dict in PATH_SEGMENTS_FOR_PERMISSION_ALL: for path_segment_dict in PATH_SEGMENTS_FOR_PERMISSION_ALL:
target_uri = f"{path_segment_dict['path']}/{process_related_path_segment}" target_uri = f"{path_segment_dict['path']}/{process_related_path_segment}"
relevant_permissions = path_segment_dict["relevant_permissions"] relevant_permissions = path_segment_dict["relevant_permissions"]
@ -571,7 +485,6 @@ class AuthorizationService:
@classmethod @classmethod
def set_basic_permissions(cls) -> list[PermissionToAssign]: def set_basic_permissions(cls) -> list[PermissionToAssign]:
"""Set_basic_permissions."""
permissions_to_assign: list[PermissionToAssign] = [] permissions_to_assign: list[PermissionToAssign] = []
permissions_to_assign.append(PermissionToAssign(permission="create", target_uri="/process-instances/for-me")) permissions_to_assign.append(PermissionToAssign(permission="create", target_uri="/process-instances/for-me"))
permissions_to_assign.append( permissions_to_assign.append(
@ -597,9 +510,31 @@ class AuthorizationService:
permissions_to_assign.append(PermissionToAssign(permission=permission, target_uri="/tasks/*")) permissions_to_assign.append(PermissionToAssign(permission=permission, target_uri="/tasks/*"))
return permissions_to_assign return permissions_to_assign
@classmethod
def set_elevated_permissions(cls) -> list[PermissionToAssign]:
permissions_to_assign: list[PermissionToAssign] = []
for process_instance_action in ["resume", "terminate", "suspend", "reset"]:
permissions_to_assign.append(
PermissionToAssign(permission="create", target_uri=f"/process-instances-{process_instance_action}/*")
)
# FIXME: we need to fix so that user that can start a process-model
# can also start through messages as well
permissions_to_assign.append(PermissionToAssign(permission="create", target_uri="/messages/*"))
permissions_to_assign.append(PermissionToAssign(permission="create", target_uri="/send-event/*"))
permissions_to_assign.append(PermissionToAssign(permission="create", target_uri="/task-complete/*"))
# read comes from PG and PM permissions
permissions_to_assign.append(PermissionToAssign(permission="update", target_uri="/task-data/*"))
for permission in ["create", "read", "update", "delete"]:
permissions_to_assign.append(PermissionToAssign(permission=permission, target_uri="/process-instances/*"))
permissions_to_assign.append(PermissionToAssign(permission=permission, target_uri="/secrets/*"))
return permissions_to_assign
@classmethod @classmethod
def set_process_group_permissions(cls, target: str, permission_set: str) -> list[PermissionToAssign]: def set_process_group_permissions(cls, target: str, permission_set: str) -> list[PermissionToAssign]:
"""Set_process_group_permissions."""
permissions_to_assign: list[PermissionToAssign] = [] permissions_to_assign: list[PermissionToAssign] = []
process_group_identifier = target.removeprefix("PG:").replace("/", ":").removeprefix(":") process_group_identifier = target.removeprefix("PG:").replace("/", ":").removeprefix(":")
process_related_path_segment = f"{process_group_identifier}:*" process_related_path_segment = f"{process_group_identifier}:*"
@ -616,7 +551,6 @@ class AuthorizationService:
@classmethod @classmethod
def set_process_model_permissions(cls, target: str, permission_set: str) -> list[PermissionToAssign]: def set_process_model_permissions(cls, target: str, permission_set: str) -> list[PermissionToAssign]:
"""Set_process_model_permissions."""
permissions_to_assign: list[PermissionToAssign] = [] permissions_to_assign: list[PermissionToAssign] = []
process_model_identifier = target.removeprefix("PM:").replace("/", ":").removeprefix(":") process_model_identifier = target.removeprefix("PM:").replace("/", ":").removeprefix(":")
process_related_path_segment = f"{process_model_identifier}/*" process_related_path_segment = f"{process_model_identifier}/*"
@ -644,6 +578,8 @@ class AuthorizationService:
* affects given process-model * affects given process-model
BASIC BASIC
* Basic access to complete tasks and use the site * Basic access to complete tasks and use the site
ELEVATED
* Operations that require elevated permissions
Permission Macros: Permission Macros:
all all
@ -666,6 +602,8 @@ class AuthorizationService:
elif target.startswith("BASIC"): elif target.startswith("BASIC"):
permissions_to_assign += cls.set_basic_permissions() permissions_to_assign += cls.set_basic_permissions()
elif target.startswith("ELEVATED"):
permissions_to_assign += cls.set_elevated_permissions()
elif target == "ALL": elif target == "ALL":
for permission in permissions: for permission in permissions:
permissions_to_assign.append(PermissionToAssign(permission=permission, target_uri="/*")) permissions_to_assign.append(PermissionToAssign(permission=permission, target_uri="/*"))
@ -685,7 +623,6 @@ class AuthorizationService:
def add_permission_from_uri_or_macro( def add_permission_from_uri_or_macro(
cls, group_identifier: str, permission: str, target: str cls, group_identifier: str, permission: str, target: str
) -> list[PermissionAssignmentModel]: ) -> list[PermissionAssignmentModel]:
"""Add_permission_from_uri_or_macro."""
group = GroupService.find_or_create_group(group_identifier) group = GroupService.find_or_create_group(group_identifier)
permissions_to_assign = cls.explode_permissions(permission, target) permissions_to_assign = cls.explode_permissions(permission, target)
permission_assignments = [] permission_assignments = []
@ -699,38 +636,106 @@ class AuthorizationService:
return permission_assignments return permission_assignments
@classmethod @classmethod
def refresh_permissions(cls, group_info: list[dict[str, Any]]) -> None: def parse_permissions_yaml_into_group_info(cls) -> list[GroupPermissionsDict]:
"""Adds new permission assignments and deletes old ones.""" if current_app.config["SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME"] is None:
initial_permission_assignments = PermissionAssignmentModel.query.all() raise (
initial_user_to_group_assignments = UserGroupAssignmentModel.query.all() PermissionsFileNotSetError(
result = cls.import_permissions_from_yaml_file() "SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME needs to be set in order to import permissions"
desired_permission_assignments = result["permission_assignments"] )
desired_group_identifiers = result["group_identifiers"] )
desired_user_to_group_identifiers = result["user_to_group_identifiers"]
for group in group_info: permission_configs = None
with open(current_app.config["SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_ABSOLUTE_PATH"]) as file:
permission_configs = yaml.safe_load(file)
group_permissions_by_group: dict[str, GroupPermissionsDict] = {}
if current_app.config["SPIFFWORKFLOW_BACKEND_DEFAULT_USER_GROUP"]:
default_group_identifier = current_app.config["SPIFFWORKFLOW_BACKEND_DEFAULT_USER_GROUP"]
group_permissions_by_group[default_group_identifier] = {
"name": default_group_identifier,
"users": [],
"permissions": [],
}
if "groups" in permission_configs:
for group_identifier, group_config in permission_configs["groups"].items():
group_info: GroupPermissionsDict = {"name": group_identifier, "users": [], "permissions": []}
for username in group_config["users"]:
group_info["users"].append(username)
group_permissions_by_group[group_identifier] = group_info
if "permissions" in permission_configs:
for _permission_identifier, permission_config in permission_configs["permissions"].items():
uri = permission_config["uri"]
for group_identifier in permission_config["groups"]:
group_permissions_by_group[group_identifier]["permissions"].append(
{"actions": permission_config["allowed_permissions"], "uri": uri}
)
return list(group_permissions_by_group.values())
@classmethod
def add_permissions_from_group_permissions(
cls, group_permissions: list[GroupPermissionsDict], user_model: Optional[UserModel] = None
) -> AddedPermissionDict:
unique_user_group_identifiers: Set[str] = set()
user_to_group_identifiers: list[UserToGroupDict] = []
permission_assignments = []
default_group = None
default_group_identifier = current_app.config["SPIFFWORKFLOW_BACKEND_DEFAULT_USER_GROUP"]
if default_group_identifier:
default_group = GroupService.find_or_create_group(default_group_identifier)
unique_user_group_identifiers.add(default_group_identifier)
for group in group_permissions:
group_identifier = group["name"] group_identifier = group["name"]
GroupService.find_or_create_group(group_identifier)
for username in group["users"]: for username in group["users"]:
user_to_group_dict: UserToGroupDict = { user_to_group_dict: UserToGroupDict = {
"username": username, "username": username,
"group_identifier": group_identifier, "group_identifier": group_identifier,
} }
desired_user_to_group_identifiers.append(user_to_group_dict) user_to_group_identifiers.append(user_to_group_dict)
GroupService.add_user_to_group_or_add_to_waiting(username, group_identifier) GroupService.add_user_to_group_or_add_to_waiting(username, group_identifier)
desired_group_identifiers.add(group_identifier) unique_user_group_identifiers.add(group_identifier)
for permission in group["permissions"]: for permission in group["permissions"]:
for crud_op in permission["actions"]: for crud_op in permission["actions"]:
desired_permission_assignments.extend( permission_assignments.extend(
cls.add_permission_from_uri_or_macro( cls.add_permission_from_uri_or_macro(
group_identifier=group_identifier, group_identifier=group_identifier,
target=permission["uri"], target=permission["uri"],
permission=crud_op, permission=crud_op,
) )
) )
desired_group_identifiers.add(group_identifier) unique_user_group_identifiers.add(group_identifier)
if default_group is not None:
if user_model:
cls.associate_user_with_group(user_model, default_group)
else:
for user in UserModel.query.all():
cls.associate_user_with_group(user, default_group)
return {
"group_identifiers": unique_user_group_identifiers,
"permission_assignments": permission_assignments,
"user_to_group_identifiers": user_to_group_identifiers,
}
@classmethod
def remove_old_permissions_from_added_permissions(
cls,
added_permissions: AddedPermissionDict,
initial_permission_assignments: list[PermissionAssignmentModel],
initial_user_to_group_assignments: list[UserGroupAssignmentModel],
) -> 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"]
for ipa in initial_permission_assignments: for ipa in initial_permission_assignments:
if ipa not in desired_permission_assignments: if ipa not in added_permission_assignments:
db.session.delete(ipa) db.session.delete(ipa)
for iutga in initial_user_to_group_assignments: for iutga in initial_user_to_group_assignments:
@ -743,19 +748,23 @@ class AuthorizationService:
"username": iutga.user.username, "username": iutga.user.username,
"group_identifier": iutga.group.identifier, "group_identifier": iutga.group.identifier,
} }
if current_user_dict not in desired_user_to_group_identifiers: if current_user_dict not in added_user_to_group_identifiers:
db.session.delete(iutga) db.session.delete(iutga)
# do not remove the default user group # do not remove the default user group
desired_group_identifiers.add(current_app.config["SPIFFWORKFLOW_BACKEND_DEFAULT_USER_GROUP"]) added_group_identifiers.add(current_app.config["SPIFFWORKFLOW_BACKEND_DEFAULT_USER_GROUP"])
groups_to_delete = GroupModel.query.filter(GroupModel.identifier.not_in(desired_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)
db.session.commit() db.session.commit()
@classmethod
class KeycloakAuthorization: def refresh_permissions(cls, group_permissions: list[GroupPermissionsDict]) -> None:
"""Interface with Keycloak server.""" """Adds new permission assignments and deletes old ones."""
initial_permission_assignments = PermissionAssignmentModel.query.all()
initial_user_to_group_assignments = UserGroupAssignmentModel.query.all()
# class KeycloakClient: group_permissions = group_permissions + cls.parse_permissions_yaml_into_group_info()
added_permissions = cls.add_permissions_from_group_permissions(group_permissions)
cls.remove_old_permissions_from_added_permissions(
added_permissions, initial_permission_assignments, initial_user_to_group_assignments
)

View File

@ -1,4 +1,3 @@
"""Group_service."""
from typing import Optional from typing import Optional
from spiffworkflow_backend.models.db import db from spiffworkflow_backend.models.db import db
@ -8,11 +7,8 @@ from spiffworkflow_backend.services.user_service import UserService
class GroupService: class GroupService:
"""GroupService."""
@classmethod @classmethod
def find_or_create_group(cls, group_identifier: str) -> GroupModel: def find_or_create_group(cls, group_identifier: str) -> GroupModel:
"""Find_or_create_group."""
group: Optional[GroupModel] = GroupModel.query.filter_by(identifier=group_identifier).first() group: Optional[GroupModel] = GroupModel.query.filter_by(identifier=group_identifier).first()
if group is None: if group is None:
group = GroupModel(identifier=group_identifier) group = GroupModel(identifier=group_identifier)
@ -23,7 +19,6 @@ class GroupService:
@classmethod @classmethod
def add_user_to_group_or_add_to_waiting(cls, username: str, group_identifier: str) -> None: def add_user_to_group_or_add_to_waiting(cls, username: str, group_identifier: str) -> None:
"""Add_user_to_group_or_add_to_waiting."""
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:

View File

@ -2349,7 +2349,6 @@ class TestProcessApi(BaseTest):
with_db_and_bpmn_file_cleanup: None, with_db_and_bpmn_file_cleanup: None,
with_super_admin_user: UserModel, with_super_admin_user: UserModel,
) -> None: ) -> None:
"""Test_correct_user_can_get_and_update_a_task."""
initiator_user = self.find_or_create_user("testuser4") initiator_user = self.find_or_create_user("testuser4")
finance_user = self.find_or_create_user("testuser2") finance_user = self.find_or_create_user("testuser2")
assert initiator_user.principal is not None assert initiator_user.principal is not None
@ -2372,15 +2371,8 @@ class TestProcessApi(BaseTest):
bpmn_file_location=bpmn_file_location, bpmn_file_location=bpmn_file_location,
) )
# process_model = load_test_spec(
# process_model_id="model_with_lanes",
# bpmn_file_name="lanes.bpmn",
# process_group_id="finance",
# )
response = self.create_process_instance_from_process_model_id_with_api( response = self.create_process_instance_from_process_model_id_with_api(
client, client,
# process_model.process_group_id,
process_model_identifier, process_model_identifier,
headers=self.logged_in_headers(initiator_user), headers=self.logged_in_headers(initiator_user),
) )

View File

@ -6,8 +6,8 @@ from tests.spiffworkflow_backend.helpers.base_test import BaseTest
from spiffworkflow_backend.models.group import GroupModel from spiffworkflow_backend.models.group import GroupModel
from spiffworkflow_backend.models.user import UserModel from spiffworkflow_backend.models.user import UserModel
from spiffworkflow_backend.models.user import UserNotFoundError
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 InvalidPermissionError from spiffworkflow_backend.services.authorization_service import InvalidPermissionError
from spiffworkflow_backend.services.group_service import GroupService from spiffworkflow_backend.services.group_service import GroupService
from spiffworkflow_backend.services.process_instance_processor import ( from spiffworkflow_backend.services.process_instance_processor import (
@ -21,19 +21,10 @@ from spiffworkflow_backend.services.user_service import UserService
class TestAuthorizationService(BaseTest): class TestAuthorizationService(BaseTest):
"""TestAuthorizationService."""
def test_can_raise_if_missing_user(self, app: Flask, with_db_and_bpmn_file_cleanup: None) -> None:
"""Test_can_raise_if_missing_user."""
with pytest.raises(UserNotFoundError):
AuthorizationService.import_permissions_from_yaml_file(raise_if_missing_user=True)
def test_does_not_fail_if_user_not_created(self, app: Flask, with_db_and_bpmn_file_cleanup: None) -> None: def test_does_not_fail_if_user_not_created(self, app: Flask, with_db_and_bpmn_file_cleanup: None) -> None:
"""Test_does_not_fail_if_user_not_created."""
AuthorizationService.import_permissions_from_yaml_file() AuthorizationService.import_permissions_from_yaml_file()
def test_can_import_permissions_from_yaml(self, app: Flask, with_db_and_bpmn_file_cleanup: None) -> None: def test_can_import_permissions_from_yaml(self, app: Flask, with_db_and_bpmn_file_cleanup: None) -> None:
"""Test_can_import_permissions_from_yaml."""
usernames = [ usernames = [
"testadmin1", "testadmin1",
"testadmin2", "testadmin2",
@ -56,15 +47,13 @@ class TestAuthorizationService(BaseTest):
assert testuser1_group_identifiers == ["Finance Team", "everybody"] assert testuser1_group_identifiers == ["Finance Team", "everybody"]
assert len(users["testuser2"].groups) == 3 assert len(users["testuser2"].groups) == 3
self.assert_user_has_permission(users["testuser1"], "update", "/v1.0/process-groups/finance/model1") self.assert_user_has_permission(users["testuser1"], "update", "/v1.0/process-groups/finance:model1")
self.assert_user_has_permission(users["testuser1"], "update", "/v1.0/process-groups/finance/") self.assert_user_has_permission(users["testuser1"], "update", "/v1.0/process-groups/finance")
self.assert_user_has_permission(users["testuser1"], "update", "/v1.0/process-groups/", expected_result=False) self.assert_user_has_permission(users["testuser1"], "update", "/v1.0/process-groups/", expected_result=False)
self.assert_user_has_permission(users["testuser4"], "update", "/v1.0/process-groups/finance/model1") self.assert_user_has_permission(users["testuser4"], "read", "/v1.0/process-groups/finance:model1")
# via the user, not the group self.assert_user_has_permission(users["testuser2"], "update", "/v1.0/process-groups/finance:model1")
self.assert_user_has_permission(users["testuser4"], "read", "/v1.0/process-groups/finance/model1") self.assert_user_has_permission(users["testuser2"], "update", "/v1.0/process-groups", expected_result=False)
self.assert_user_has_permission(users["testuser2"], "update", "/v1.0/process-groups/finance/model1") self.assert_user_has_permission(users["testuser2"], "read", "/v1.0/process-groups")
self.assert_user_has_permission(users["testuser2"], "update", "/v1.0/process-groups/", expected_result=False)
self.assert_user_has_permission(users["testuser2"], "read", "/v1.0/process-groups/")
def test_user_can_be_added_to_human_task_on_first_login( def test_user_can_be_added_to_human_task_on_first_login(
self, self,
@ -121,7 +110,6 @@ class TestAuthorizationService(BaseTest):
client: FlaskClient, client: FlaskClient,
with_db_and_bpmn_file_cleanup: None, with_db_and_bpmn_file_cleanup: None,
) -> None: ) -> None:
"""Test_explode_permissions_all_on_process_group."""
expected_permissions = sorted( expected_permissions = sorted(
[ [
("/event-error-details/some-process-group:some-process-model:*", "read"), ("/event-error-details/some-process-group:some-process-model:*", "read"),
@ -313,6 +301,34 @@ class TestAuthorizationService(BaseTest):
permissions_to_assign_tuples = sorted([(p.target_uri, p.permission) for p in permissions_to_assign]) permissions_to_assign_tuples = sorted([(p.target_uri, p.permission) for p in permissions_to_assign])
assert permissions_to_assign_tuples == expected_permissions assert permissions_to_assign_tuples == expected_permissions
def test_explode_permissions_elevated(
self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,
) -> None:
expected_permissions = [
("/messages/*", "create"),
("/process-instances-reset/*", "create"),
("/process-instances-resume/*", "create"),
("/process-instances-suspend/*", "create"),
("/process-instances-terminate/*", "create"),
("/process-instances/*", "create"),
("/process-instances/*", "delete"),
("/process-instances/*", "read"),
("/process-instances/*", "update"),
("/secrets/*", "create"),
("/secrets/*", "delete"),
("/secrets/*", "read"),
("/secrets/*", "update"),
("/send-event/*", "create"),
("/task-complete/*", "create"),
("/task-data/*", "update"),
]
permissions_to_assign = AuthorizationService.explode_permissions("all", "ELEVATED")
permissions_to_assign_tuples = sorted([(p.target_uri, p.permission) for p in permissions_to_assign])
assert permissions_to_assign_tuples == expected_permissions
def test_explode_permissions_all( def test_explode_permissions_all(
self, self,
app: Flask, app: Flask,
@ -387,7 +403,6 @@ class TestAuthorizationService(BaseTest):
client: FlaskClient, client: FlaskClient,
with_db_and_bpmn_file_cleanup: None, with_db_and_bpmn_file_cleanup: None,
) -> None: ) -> None:
"""Test_can_refresh_permissions."""
user = self.find_or_create_user(username="user_one") user = self.find_or_create_user(username="user_one")
user_two = self.find_or_create_user(username="user_two") user_two = self.find_or_create_user(username="user_two")
admin_user = self.find_or_create_user(username="testadmin1") admin_user = self.find_or_create_user(username="testadmin1")
@ -399,7 +414,7 @@ class TestAuthorizationService(BaseTest):
GroupService.find_or_create_group("group_three") GroupService.find_or_create_group("group_three")
assert GroupModel.query.filter_by(identifier="group_three").first() is not None assert GroupModel.query.filter_by(identifier="group_three").first() is not None
group_info = [ group_info: list[GroupPermissionsDict] = [
{ {
"users": ["user_one", "user_two"], "users": ["user_one", "user_two"],
"name": "group_one", "name": "group_one",
@ -410,6 +425,11 @@ class TestAuthorizationService(BaseTest):
"name": "group_three", "name": "group_three",
"permissions": [{"actions": ["create", "read"], "uri": "PG:hey2"}], "permissions": [{"actions": ["create", "read"], "uri": "PG:hey2"}],
}, },
{
"users": [],
"name": "everybody",
"permissions": [{"actions": ["read"], "uri": "PG:hey2everybody"}],
},
] ]
AuthorizationService.refresh_permissions(group_info) AuthorizationService.refresh_permissions(group_info)
assert GroupModel.query.filter_by(identifier="group_two").first() is None assert GroupModel.query.filter_by(identifier="group_two").first() is None
@ -418,6 +438,7 @@ class TestAuthorizationService(BaseTest):
self.assert_user_has_permission(user, "read", "/v1.0/process-groups/hey") self.assert_user_has_permission(user, "read", "/v1.0/process-groups/hey")
self.assert_user_has_permission(user, "read", "/v1.0/process-groups/hey:yo") self.assert_user_has_permission(user, "read", "/v1.0/process-groups/hey:yo")
self.assert_user_has_permission(user, "create", "/v1.0/process-groups/hey:yo") self.assert_user_has_permission(user, "create", "/v1.0/process-groups/hey:yo")
self.assert_user_has_permission(user, "read", "/v1.0/process-groups/hey2everybody:yo")
self.assert_user_has_permission(user_two, "read", "/v1.0/process-groups/hey") self.assert_user_has_permission(user_two, "read", "/v1.0/process-groups/hey")
self.assert_user_has_permission(user_two, "read", "/v1.0/process-groups/hey:yo") self.assert_user_has_permission(user_two, "read", "/v1.0/process-groups/hey:yo")