Merge pull request #266 from sartography/feature/support_macros_in_permission_yaml
Feature/support macros in permission yaml
This commit is contained in:
commit
9ff0169fd3
|
@ -1122,7 +1122,7 @@ paths:
|
|||
- Process Instances
|
||||
responses:
|
||||
"200":
|
||||
description: Empty ok true response on successful resume.
|
||||
description: Empty ok true response on successful reset.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
|
|
|
@ -143,6 +143,7 @@ SPIFFWORKFLOW_BACKEND_ALLOW_CONFISCATING_LOCK_AFTER_SECONDS = int(
|
|||
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_ENGINE_STEP_DEFAULT_STRATEGY_BACKGROUND = environ.get(
|
||||
|
|
|
@ -12,6 +12,5 @@ groups:
|
|||
permissions:
|
||||
admin:
|
||||
groups: [admin]
|
||||
users: []
|
||||
allowed_permissions: [create, read, update, delete]
|
||||
uri: /*
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
default_group: everybody
|
||||
|
||||
groups:
|
||||
admin:
|
||||
users:
|
||||
|
@ -19,6 +17,5 @@ groups:
|
|||
permissions:
|
||||
admin:
|
||||
groups: [admin, tech_writers]
|
||||
users: []
|
||||
allowed_permissions: [create, read, update, delete]
|
||||
uri: /*
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
default_group: everybody
|
||||
|
||||
users:
|
||||
admin:
|
||||
|
@ -41,52 +40,43 @@ permissions:
|
|||
# Admins have access to everything.
|
||||
admin:
|
||||
groups: [admin]
|
||||
users: []
|
||||
allowed_permissions: [create, read, update, delete]
|
||||
uri: /*
|
||||
|
||||
# Everybody can participate in tasks assigned to them.
|
||||
tasks-crud:
|
||||
groups: [everybody]
|
||||
users: []
|
||||
allowed_permissions: [create, read, update, delete]
|
||||
uri: /tasks/*
|
||||
|
||||
# Everybody can start all intstances
|
||||
create-test-instances:
|
||||
groups: [ everybody ]
|
||||
users: [ ]
|
||||
allowed_permissions: [ create ]
|
||||
uri: /process-instances/*
|
||||
|
||||
# Everyone can see everything (all groups, and processes are visible)
|
||||
read-all-process-groups:
|
||||
groups: [ everybody ]
|
||||
users: [ ]
|
||||
allowed_permissions: [ read ]
|
||||
uri: /process-groups/*
|
||||
read-all-process-models:
|
||||
groups: [ everybody ]
|
||||
users: [ ]
|
||||
allowed_permissions: [ read ]
|
||||
uri: /process-models/*
|
||||
read-all-process-instance:
|
||||
groups: [ everybody ]
|
||||
users: [ ]
|
||||
allowed_permissions: [ read ]
|
||||
uri: /process-instances/*
|
||||
read-process-instance-reports:
|
||||
groups: [ everybody ]
|
||||
users: [ ]
|
||||
allowed_permissions: [ read ]
|
||||
uri: /process-instances/reports/*
|
||||
processes-read:
|
||||
groups: [ everybody ]
|
||||
users: [ ]
|
||||
allowed_permissions: [ read ]
|
||||
uri: /processes
|
||||
groups-everybody:
|
||||
groups: [everybody]
|
||||
users: []
|
||||
allowed_permissions: [create, read]
|
||||
uri: /v1.0/user-groups/for-current-user
|
||||
|
|
|
@ -1,84 +1,17 @@
|
|||
default_group: everybody
|
||||
|
||||
groups:
|
||||
admin:
|
||||
users: [admin@spiffworkflow.org]
|
||||
|
||||
permissions:
|
||||
admin:
|
||||
process-groups-ro:
|
||||
groups: [admin]
|
||||
users: []
|
||||
allowed_permissions: [read]
|
||||
uri: /*
|
||||
|
||||
tasks-crud:
|
||||
uri: PG:ALL
|
||||
basic:
|
||||
groups: [admin]
|
||||
users: []
|
||||
allowed_permissions: [create, update, delete]
|
||||
uri: /tasks/*
|
||||
|
||||
process-instances-crud:
|
||||
groups: [ admin ]
|
||||
users: [ ]
|
||||
allowed_permissions: [create, update, delete]
|
||||
uri: /process-instances/*
|
||||
|
||||
suspend:
|
||||
allowed_permissions: [ALL]
|
||||
uri: BASIC
|
||||
elevated-operations:
|
||||
groups: [admin]
|
||||
users: []
|
||||
allowed_permissions: [create]
|
||||
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/*
|
||||
allowed_permissions: [ALL]
|
||||
uri: ELEVATED
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
default_group: everybody
|
||||
|
||||
groups:
|
||||
admin:
|
||||
|
@ -11,6 +10,5 @@ groups:
|
|||
permissions:
|
||||
admin:
|
||||
groups: [admin, group1, group2]
|
||||
users: []
|
||||
allowed_permissions: [create, read, update, delete]
|
||||
uri: /*
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
default_group: everybody
|
||||
|
||||
groups:
|
||||
admin:
|
||||
|
@ -7,6 +6,5 @@ groups:
|
|||
permissions:
|
||||
admin:
|
||||
groups: [admin]
|
||||
users: []
|
||||
allowed_permissions: [create, read, update, delete]
|
||||
uri: /*
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
default_group: everybody
|
||||
|
||||
users:
|
||||
testadmin1:
|
||||
service: https://testing/openid/thing
|
||||
|
@ -20,49 +18,40 @@ groups:
|
|||
permissions:
|
||||
admin:
|
||||
groups: [admin]
|
||||
users: []
|
||||
allowed_permissions: [create, read, update, delete]
|
||||
uri: /*
|
||||
|
||||
read-all:
|
||||
groups: ["Finance Team", hr, admin]
|
||||
users: []
|
||||
allowed_permissions: [read]
|
||||
uri: /*
|
||||
|
||||
process-instances-find-by-id:
|
||||
groups: [everybody]
|
||||
users: []
|
||||
allowed_permissions: [read]
|
||||
uri: /process-instances/find-by-id/*
|
||||
|
||||
tasks-crud:
|
||||
groups: [everybody]
|
||||
users: []
|
||||
allowed_permissions: [create, read, update, delete]
|
||||
uri: /tasks/*
|
||||
|
||||
# TODO: all uris should really have the same structure
|
||||
finance-admin-group:
|
||||
groups: ["Finance Team"]
|
||||
users: [testuser4]
|
||||
allowed_permissions: [create, read, update, delete]
|
||||
uri: /process-groups/finance/*
|
||||
allowed_permissions: [all]
|
||||
uri: PG:finance
|
||||
|
||||
finance-admin-model:
|
||||
groups: ["Finance Team"]
|
||||
users: [testuser4]
|
||||
allowed_permissions: [create, read, update, delete]
|
||||
uri: /process-models/finance/*
|
||||
finance-hr-start:
|
||||
groups: ["hr"]
|
||||
allowed_permissions: [start]
|
||||
uri: PG:finance
|
||||
|
||||
finance-admin-model-lanes:
|
||||
groups: ["Finance Team"]
|
||||
users: [testuser4]
|
||||
allowed_permissions: [create, read, update, delete]
|
||||
uri: /process-models/finance:model_with_lanes/*
|
||||
|
||||
finance-admin-instance-run:
|
||||
groups: ["Finance Team"]
|
||||
users: [testuser4]
|
||||
allowed_permissions: [create, read, update, delete]
|
||||
uri: /process-instances/*
|
||||
|
|
|
@ -34,6 +34,5 @@ class RefreshPermissions(Script):
|
|||
*args: Any,
|
||||
**kwargs: Any,
|
||||
) -> Any:
|
||||
"""Run."""
|
||||
group_info = args[0]
|
||||
AuthorizationService.refresh_permissions(group_info)
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
"""Authorization_service."""
|
||||
import inspect
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from hashlib import sha256
|
||||
from hmac import compare_digest
|
||||
from hmac import HMAC
|
||||
from typing import Any
|
||||
from typing import Optional
|
||||
from typing import Set
|
||||
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 PrincipalModel
|
||||
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.routes.openid_blueprint import openid_blueprint
|
||||
from spiffworkflow_backend.services.authentication_service import NotAuthorizedError
|
||||
|
@ -42,25 +39,23 @@ from spiffworkflow_backend.services.user_service import UserService
|
|||
|
||||
|
||||
class PermissionsFileNotSetError(Exception):
|
||||
"""PermissionsFileNotSetError."""
|
||||
pass
|
||||
|
||||
|
||||
class HumanTaskNotFoundError(Exception):
|
||||
"""HumanTaskNotFoundError."""
|
||||
pass
|
||||
|
||||
|
||||
class UserDoesNotHaveAccessToTaskError(Exception):
|
||||
"""UserDoesNotHaveAccessToTaskError."""
|
||||
pass
|
||||
|
||||
|
||||
class InvalidPermissionError(Exception):
|
||||
"""InvalidPermissionError."""
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class PermissionToAssign:
|
||||
"""PermissionToAssign."""
|
||||
|
||||
permission: str
|
||||
target_uri: str
|
||||
|
||||
|
@ -93,21 +88,29 @@ class UserToGroupDict(TypedDict):
|
|||
group_identifier: str
|
||||
|
||||
|
||||
class DesiredPermissionDict(TypedDict):
|
||||
"""DesiredPermissionDict."""
|
||||
|
||||
class AddedPermissionDict(TypedDict):
|
||||
group_identifiers: Set[str]
|
||||
permission_assignments: list[PermissionAssignmentModel]
|
||||
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:
|
||||
"""Determine whether a user has permission to perform their request."""
|
||||
|
||||
# https://stackoverflow.com/a/71320673/6090676
|
||||
@classmethod
|
||||
def verify_sha256_token(cls, auth_header: Optional[str]) -> None:
|
||||
"""Verify_sha256_token."""
|
||||
if auth_header is None:
|
||||
raise TokenNotProvidedError(
|
||||
"unauthorized",
|
||||
|
@ -123,7 +126,6 @@ class AuthorizationService:
|
|||
|
||||
@classmethod
|
||||
def has_permission(cls, principals: list[PrincipalModel], permission: str, target_uri: str) -> bool:
|
||||
"""Has_permission."""
|
||||
principal_ids = [p.id for p in principals]
|
||||
target_uri_normalized = target_uri.removeprefix(V1_API_PATH_PREFIX)
|
||||
|
||||
|
@ -153,7 +155,6 @@ class AuthorizationService:
|
|||
|
||||
@classmethod
|
||||
def user_has_permission(cls, user: UserModel, permission: str, target_uri: str) -> bool:
|
||||
"""User_has_permission."""
|
||||
if user.principal is None:
|
||||
raise MissingPrincipalError(f"Missing principal for user with id: {user.id}")
|
||||
|
||||
|
@ -179,7 +180,6 @@ class AuthorizationService:
|
|||
|
||||
@classmethod
|
||||
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()
|
||||
if user_group_assignemnt is None:
|
||||
user_group_assignemnt = UserGroupAssignmentModel(user_id=user.id, group_id=group.id)
|
||||
|
@ -187,88 +187,13 @@ class AuthorizationService:
|
|||
db.session.commit()
|
||||
|
||||
@classmethod
|
||||
def import_permissions_from_yaml_file(cls, raise_if_missing_user: bool = False) -> DesiredPermissionDict:
|
||||
"""Import_permissions_from_yaml_file."""
|
||||
if current_app.config["SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME"] is None:
|
||||
raise (
|
||||
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,
|
||||
}
|
||||
def import_permissions_from_yaml_file(cls, user_model: Optional[UserModel] = None) -> AddedPermissionDict:
|
||||
group_permissions = cls.parse_permissions_yaml_into_group_info()
|
||||
result = cls.add_permissions_from_group_permissions(group_permissions, user_model)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def find_or_create_permission_target(cls, uri: str) -> PermissionTargetModel:
|
||||
"""Find_or_create_permission_target."""
|
||||
uri_with_percent = re.sub(r"\*", "%", uri)
|
||||
target_uri_normalized = uri_with_percent.removeprefix(V1_API_PATH_PREFIX)
|
||||
permission_target: Optional[PermissionTargetModel] = PermissionTargetModel.query.filter_by(
|
||||
|
@ -287,7 +212,6 @@ class AuthorizationService:
|
|||
permission_target: PermissionTargetModel,
|
||||
permission: str,
|
||||
) -> PermissionAssignmentModel:
|
||||
"""Create_permission_for_principal."""
|
||||
permission_assignment: Optional[PermissionAssignmentModel] = PermissionAssignmentModel.query.filter_by(
|
||||
principal_id=principal.id,
|
||||
permission_target_id=permission_target.id,
|
||||
|
@ -306,7 +230,6 @@ class AuthorizationService:
|
|||
|
||||
@classmethod
|
||||
def should_disable_auth_for_request(cls) -> bool:
|
||||
"""Should_disable_auth_for_request."""
|
||||
swagger_functions = ["get_json_spec"]
|
||||
authentication_exclusion_list = [
|
||||
"status",
|
||||
|
@ -344,7 +267,6 @@ class AuthorizationService:
|
|||
|
||||
@classmethod
|
||||
def get_permission_from_http_method(cls, http_method: str) -> Optional[str]:
|
||||
"""Get_permission_from_request_method."""
|
||||
request_method_mapper = {
|
||||
"POST": "create",
|
||||
"GET": "read",
|
||||
|
@ -363,7 +285,6 @@ class AuthorizationService:
|
|||
|
||||
@classmethod
|
||||
def check_for_permission(cls) -> None:
|
||||
"""Check_for_permission."""
|
||||
if cls.should_disable_auth_for_request():
|
||||
return None
|
||||
|
||||
|
@ -397,11 +318,6 @@ class AuthorizationService:
|
|||
|
||||
@staticmethod
|
||||
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")
|
||||
if secret_key is None:
|
||||
raise KeyError("we need current_app.config to have a SECRET_KEY")
|
||||
|
@ -445,10 +361,11 @@ class AuthorizationService:
|
|||
|
||||
@classmethod
|
||||
def create_user_from_sign_in(cls, user_info: dict) -> UserModel:
|
||||
"""Create_user_from_sign_in."""
|
||||
"""Name, family_name, given_name, middle_name, nickname, preferred_username,"""
|
||||
"""Profile, picture, website, gender, birthdate, zoneinfo, locale, and updated_at. """
|
||||
"""Email."""
|
||||
"""Fields from user_info.
|
||||
|
||||
name, family_name, given_name, middle_name, nickname, preferred_username,
|
||||
profile, picture, website, gender, birthdate, zoneinfo, locale,updated_at, email.
|
||||
"""
|
||||
is_new_user = False
|
||||
user_attributes = {}
|
||||
|
||||
|
@ -506,7 +423,7 @@ class AuthorizationService:
|
|||
# we are also a little apprehensive about pre-creating users
|
||||
# before the user signs in, because we won't know things like
|
||||
# the external service user identifier.
|
||||
cls.import_permissions_from_yaml_file()
|
||||
cls.import_permissions_from_yaml_file(user_model)
|
||||
|
||||
if is_new_user:
|
||||
UserService.add_user_to_human_tasks_if_appropriate(user_model)
|
||||
|
@ -521,11 +438,6 @@ class AuthorizationService:
|
|||
process_related_path_segment: str,
|
||||
target_uris: list[str],
|
||||
) -> list[PermissionToAssign]:
|
||||
"""Get_permissions_to_assign."""
|
||||
permissions = permission_set.split(",")
|
||||
if permission_set == "all":
|
||||
permissions = ["create", "read", "update", "delete"]
|
||||
|
||||
permissions_to_assign: list[PermissionToAssign] = []
|
||||
|
||||
# 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))
|
||||
else:
|
||||
permissions = permission_set.split(",")
|
||||
if permission_set == "all":
|
||||
permissions = ["create", "read", "update", "delete"]
|
||||
for path_segment_dict in PATH_SEGMENTS_FOR_PERMISSION_ALL:
|
||||
target_uri = f"{path_segment_dict['path']}/{process_related_path_segment}"
|
||||
relevant_permissions = path_segment_dict["relevant_permissions"]
|
||||
|
@ -571,7 +485,6 @@ class AuthorizationService:
|
|||
|
||||
@classmethod
|
||||
def set_basic_permissions(cls) -> list[PermissionToAssign]:
|
||||
"""Set_basic_permissions."""
|
||||
permissions_to_assign: list[PermissionToAssign] = []
|
||||
permissions_to_assign.append(PermissionToAssign(permission="create", target_uri="/process-instances/for-me"))
|
||||
permissions_to_assign.append(
|
||||
|
@ -597,9 +510,31 @@ class AuthorizationService:
|
|||
permissions_to_assign.append(PermissionToAssign(permission=permission, target_uri="/tasks/*"))
|
||||
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
|
||||
def set_process_group_permissions(cls, target: str, permission_set: str) -> list[PermissionToAssign]:
|
||||
"""Set_process_group_permissions."""
|
||||
permissions_to_assign: list[PermissionToAssign] = []
|
||||
process_group_identifier = target.removeprefix("PG:").replace("/", ":").removeprefix(":")
|
||||
process_related_path_segment = f"{process_group_identifier}:*"
|
||||
|
@ -616,7 +551,6 @@ class AuthorizationService:
|
|||
|
||||
@classmethod
|
||||
def set_process_model_permissions(cls, target: str, permission_set: str) -> list[PermissionToAssign]:
|
||||
"""Set_process_model_permissions."""
|
||||
permissions_to_assign: list[PermissionToAssign] = []
|
||||
process_model_identifier = target.removeprefix("PM:").replace("/", ":").removeprefix(":")
|
||||
process_related_path_segment = f"{process_model_identifier}/*"
|
||||
|
@ -644,6 +578,8 @@ class AuthorizationService:
|
|||
* affects given process-model
|
||||
BASIC
|
||||
* Basic access to complete tasks and use the site
|
||||
ELEVATED
|
||||
* Operations that require elevated permissions
|
||||
|
||||
Permission Macros:
|
||||
all
|
||||
|
@ -666,6 +602,8 @@ class AuthorizationService:
|
|||
|
||||
elif target.startswith("BASIC"):
|
||||
permissions_to_assign += cls.set_basic_permissions()
|
||||
elif target.startswith("ELEVATED"):
|
||||
permissions_to_assign += cls.set_elevated_permissions()
|
||||
elif target == "ALL":
|
||||
for permission in permissions:
|
||||
permissions_to_assign.append(PermissionToAssign(permission=permission, target_uri="/*"))
|
||||
|
@ -685,7 +623,6 @@ class AuthorizationService:
|
|||
def add_permission_from_uri_or_macro(
|
||||
cls, group_identifier: str, permission: str, target: str
|
||||
) -> list[PermissionAssignmentModel]:
|
||||
"""Add_permission_from_uri_or_macro."""
|
||||
group = GroupService.find_or_create_group(group_identifier)
|
||||
permissions_to_assign = cls.explode_permissions(permission, target)
|
||||
permission_assignments = []
|
||||
|
@ -699,38 +636,106 @@ class AuthorizationService:
|
|||
return permission_assignments
|
||||
|
||||
@classmethod
|
||||
def refresh_permissions(cls, group_info: list[dict[str, Any]]) -> None:
|
||||
"""Adds new permission assignments and deletes old ones."""
|
||||
initial_permission_assignments = PermissionAssignmentModel.query.all()
|
||||
initial_user_to_group_assignments = UserGroupAssignmentModel.query.all()
|
||||
result = cls.import_permissions_from_yaml_file()
|
||||
desired_permission_assignments = result["permission_assignments"]
|
||||
desired_group_identifiers = result["group_identifiers"]
|
||||
desired_user_to_group_identifiers = result["user_to_group_identifiers"]
|
||||
def parse_permissions_yaml_into_group_info(cls) -> list[GroupPermissionsDict]:
|
||||
if current_app.config["SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME"] is None:
|
||||
raise (
|
||||
PermissionsFileNotSetError(
|
||||
"SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME needs to be set in order to import permissions"
|
||||
)
|
||||
)
|
||||
|
||||
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"]
|
||||
GroupService.find_or_create_group(group_identifier)
|
||||
for username in group["users"]:
|
||||
user_to_group_dict: UserToGroupDict = {
|
||||
"username": username,
|
||||
"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)
|
||||
desired_group_identifiers.add(group_identifier)
|
||||
unique_user_group_identifiers.add(group_identifier)
|
||||
for permission in group["permissions"]:
|
||||
for crud_op in permission["actions"]:
|
||||
desired_permission_assignments.extend(
|
||||
permission_assignments.extend(
|
||||
cls.add_permission_from_uri_or_macro(
|
||||
group_identifier=group_identifier,
|
||||
target=permission["uri"],
|
||||
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:
|
||||
if ipa not in desired_permission_assignments:
|
||||
if ipa not in added_permission_assignments:
|
||||
db.session.delete(ipa)
|
||||
|
||||
for iutga in initial_user_to_group_assignments:
|
||||
|
@ -743,19 +748,23 @@ class AuthorizationService:
|
|||
"username": iutga.user.username,
|
||||
"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)
|
||||
|
||||
# do not remove the default user group
|
||||
desired_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()
|
||||
added_group_identifiers.add(current_app.config["SPIFFWORKFLOW_BACKEND_DEFAULT_USER_GROUP"])
|
||||
groups_to_delete = GroupModel.query.filter(GroupModel.identifier.not_in(added_group_identifiers)).all()
|
||||
for gtd in groups_to_delete:
|
||||
db.session.delete(gtd)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
class KeycloakAuthorization:
|
||||
"""Interface with Keycloak server."""
|
||||
|
||||
|
||||
# class KeycloakClient:
|
||||
@classmethod
|
||||
def refresh_permissions(cls, group_permissions: list[GroupPermissionsDict]) -> None:
|
||||
"""Adds new permission assignments and deletes old ones."""
|
||||
initial_permission_assignments = PermissionAssignmentModel.query.all()
|
||||
initial_user_to_group_assignments = UserGroupAssignmentModel.query.all()
|
||||
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
|
||||
)
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
"""Group_service."""
|
||||
from typing import Optional
|
||||
|
||||
from spiffworkflow_backend.models.db import db
|
||||
|
@ -8,11 +7,8 @@ from spiffworkflow_backend.services.user_service import UserService
|
|||
|
||||
|
||||
class GroupService:
|
||||
"""GroupService."""
|
||||
|
||||
@classmethod
|
||||
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()
|
||||
if group is None:
|
||||
group = GroupModel(identifier=group_identifier)
|
||||
|
@ -23,7 +19,6 @@ class GroupService:
|
|||
|
||||
@classmethod
|
||||
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)
|
||||
user = UserModel.query.filter_by(username=username).first()
|
||||
if user:
|
||||
|
|
|
@ -2349,7 +2349,6 @@ class TestProcessApi(BaseTest):
|
|||
with_db_and_bpmn_file_cleanup: None,
|
||||
with_super_admin_user: UserModel,
|
||||
) -> None:
|
||||
"""Test_correct_user_can_get_and_update_a_task."""
|
||||
initiator_user = self.find_or_create_user("testuser4")
|
||||
finance_user = self.find_or_create_user("testuser2")
|
||||
assert initiator_user.principal is not None
|
||||
|
@ -2372,15 +2371,8 @@ class TestProcessApi(BaseTest):
|
|||
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(
|
||||
client,
|
||||
# process_model.process_group_id,
|
||||
process_model_identifier,
|
||||
headers=self.logged_in_headers(initiator_user),
|
||||
)
|
||||
|
|
|
@ -6,8 +6,8 @@ from tests.spiffworkflow_backend.helpers.base_test import BaseTest
|
|||
|
||||
from spiffworkflow_backend.models.group import GroupModel
|
||||
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 GroupPermissionsDict
|
||||
from spiffworkflow_backend.services.authorization_service import InvalidPermissionError
|
||||
from spiffworkflow_backend.services.group_service import GroupService
|
||||
from spiffworkflow_backend.services.process_instance_processor import (
|
||||
|
@ -21,19 +21,10 @@ from spiffworkflow_backend.services.user_service import UserService
|
|||
|
||||
|
||||
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:
|
||||
"""Test_does_not_fail_if_user_not_created."""
|
||||
AuthorizationService.import_permissions_from_yaml_file()
|
||||
|
||||
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 = [
|
||||
"testadmin1",
|
||||
"testadmin2",
|
||||
|
@ -56,15 +47,13 @@ class TestAuthorizationService(BaseTest):
|
|||
assert testuser1_group_identifiers == ["Finance Team", "everybody"]
|
||||
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/")
|
||||
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/", expected_result=False)
|
||||
self.assert_user_has_permission(users["testuser4"], "update", "/v1.0/process-groups/finance/model1")
|
||||
# via the user, not the group
|
||||
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/finance/model1")
|
||||
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/")
|
||||
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/finance:model1")
|
||||
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(
|
||||
self,
|
||||
|
@ -121,7 +110,6 @@ class TestAuthorizationService(BaseTest):
|
|||
client: FlaskClient,
|
||||
with_db_and_bpmn_file_cleanup: None,
|
||||
) -> None:
|
||||
"""Test_explode_permissions_all_on_process_group."""
|
||||
expected_permissions = sorted(
|
||||
[
|
||||
("/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])
|
||||
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(
|
||||
self,
|
||||
app: Flask,
|
||||
|
@ -387,7 +403,6 @@ class TestAuthorizationService(BaseTest):
|
|||
client: FlaskClient,
|
||||
with_db_and_bpmn_file_cleanup: None,
|
||||
) -> None:
|
||||
"""Test_can_refresh_permissions."""
|
||||
user = self.find_or_create_user(username="user_one")
|
||||
user_two = self.find_or_create_user(username="user_two")
|
||||
admin_user = self.find_or_create_user(username="testadmin1")
|
||||
|
@ -399,7 +414,7 @@ class TestAuthorizationService(BaseTest):
|
|||
GroupService.find_or_create_group("group_three")
|
||||
assert GroupModel.query.filter_by(identifier="group_three").first() is not None
|
||||
|
||||
group_info = [
|
||||
group_info: list[GroupPermissionsDict] = [
|
||||
{
|
||||
"users": ["user_one", "user_two"],
|
||||
"name": "group_one",
|
||||
|
@ -410,6 +425,11 @@ class TestAuthorizationService(BaseTest):
|
|||
"name": "group_three",
|
||||
"permissions": [{"actions": ["create", "read"], "uri": "PG:hey2"}],
|
||||
},
|
||||
{
|
||||
"users": [],
|
||||
"name": "everybody",
|
||||
"permissions": [{"actions": ["read"], "uri": "PG:hey2everybody"}],
|
||||
},
|
||||
]
|
||||
AuthorizationService.refresh_permissions(group_info)
|
||||
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: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:yo")
|
||||
|
|
Loading…
Reference in New Issue