Feature/hide private data objects (#581)
* added test and some additional support for deny permissions w/ burnettk * added support for deny through permissions-check api w/ burnettk * support DENY at the beginning of a permission target marcro * do not look up permissions using grant type, only use the uniqueness key * added support in frontend to display a nice error if user does not have access to a data object value w/ burnettk --------- Co-authored-by: jasquat <jasquat@users.noreply.github.com>
This commit is contained in:
parent
36b7b2462e
commit
39edd60fd6
|
@ -19,7 +19,7 @@ mysql -uroot "$database" -e '
|
||||||
JOIN `user_group_assignment` uga ON uga.user_id = u.id
|
JOIN `user_group_assignment` uga ON uga.user_id = u.id
|
||||||
JOIN `group` g ON g.id = uga.group_id;
|
JOIN `group` g ON g.id = uga.group_id;
|
||||||
|
|
||||||
select pa.id, g.identifier group_identifier, pt.uri, permission from permission_assignment pa
|
select pa.id pa_id, g.identifier group_identifier, pt.uri, pa.grant_type, permission, p.id principal_id from permission_assignment pa
|
||||||
JOIN principal p ON p.id = pa.principal_id
|
JOIN principal p ON p.id = pa.principal_id
|
||||||
JOIN `group` g ON g.id = p.group_id
|
JOIN `group` g ON g.id = p.group_id
|
||||||
JOIN permission_target pt ON pt.id = pa.permission_target_id;
|
JOIN permission_target pt ON pt.id = pa.permission_target_id;
|
||||||
|
|
|
@ -1803,7 +1803,7 @@ paths:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/OkTrue"
|
$ref: "#/components/schemas/OkTrue"
|
||||||
|
|
||||||
/process-data/{modified_process_model_identifier}/{process_instance_id}/{process_data_identifier}:
|
/process-data/{modified_process_model_identifier}/{process_data_identifier}/{process_instance_id}:
|
||||||
parameters:
|
parameters:
|
||||||
- name: modified_process_model_identifier
|
- name: modified_process_model_identifier
|
||||||
in: path
|
in: path
|
||||||
|
|
|
@ -121,15 +121,20 @@ class AuthorizationService:
|
||||||
)
|
)
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if len(permission_assignments) == 0:
|
||||||
|
return False
|
||||||
|
|
||||||
|
all_permissions_permit = True
|
||||||
for permission_assignment in permission_assignments:
|
for permission_assignment in permission_assignments:
|
||||||
if permission_assignment.grant_type == "permit":
|
if permission_assignment.grant_type == "permit":
|
||||||
return True
|
pass
|
||||||
elif permission_assignment.grant_type == "deny":
|
elif permission_assignment.grant_type == "deny":
|
||||||
return False
|
all_permissions_permit = False
|
||||||
else:
|
else:
|
||||||
raise Exception(f"Unknown grant type: {permission_assignment.grant_type}")
|
raise Exception(f"Unknown grant type: {permission_assignment.grant_type}")
|
||||||
|
|
||||||
return False
|
return all_permissions_permit
|
||||||
|
|
||||||
@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:
|
||||||
|
@ -151,17 +156,21 @@ class AuthorizationService:
|
||||||
) -> bool:
|
) -> bool:
|
||||||
uri_with_percent = re.sub(r"\*", "%", target_uri)
|
uri_with_percent = re.sub(r"\*", "%", target_uri)
|
||||||
target_uri_normalized = uri_with_percent.removeprefix(V1_API_PATH_PREFIX)
|
target_uri_normalized = uri_with_percent.removeprefix(V1_API_PATH_PREFIX)
|
||||||
|
|
||||||
|
matching_permission_assignments = []
|
||||||
for permission_assignment in permission_assignments:
|
for permission_assignment in permission_assignments:
|
||||||
if permission_assignment.permission == permission and cls.target_uri_matches_actual_uri(
|
if permission_assignment.permission == permission and cls.target_uri_matches_actual_uri(
|
||||||
permission_assignment.permission_target.uri, target_uri_normalized
|
permission_assignment.permission_target.uri, target_uri_normalized
|
||||||
):
|
):
|
||||||
# we might have to rethink this to actually support deny
|
matching_permission_assignments.append(permission_assignment)
|
||||||
if permission_assignment.grant_type == "permit":
|
if len(matching_permission_assignments) == 0:
|
||||||
return True
|
return False
|
||||||
elif permission_assignment.grant_type == "deny":
|
|
||||||
return False
|
all_permissions_permit = True
|
||||||
return True
|
for permission_assignment in matching_permission_assignments:
|
||||||
return False
|
if permission_assignment.grant_type == "deny":
|
||||||
|
all_permissions_permit = False
|
||||||
|
return all_permissions_permit
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def target_uri_matches_actual_uri(cls, target_uri: str, actual_uri: str) -> bool:
|
def target_uri_matches_actual_uri(cls, target_uri: str, actual_uri: str) -> bool:
|
||||||
|
@ -214,6 +223,7 @@ class AuthorizationService:
|
||||||
principal: PrincipalModel,
|
principal: PrincipalModel,
|
||||||
permission_target: PermissionTargetModel,
|
permission_target: PermissionTargetModel,
|
||||||
permission: str,
|
permission: str,
|
||||||
|
grant_type: str = "permit",
|
||||||
) -> PermissionAssignmentModel:
|
) -> PermissionAssignmentModel:
|
||||||
permission_assignment: PermissionAssignmentModel | None = PermissionAssignmentModel.query.filter_by(
|
permission_assignment: PermissionAssignmentModel | None = PermissionAssignmentModel.query.filter_by(
|
||||||
principal_id=principal.id,
|
principal_id=principal.id,
|
||||||
|
@ -225,10 +235,14 @@ class AuthorizationService:
|
||||||
principal_id=principal.id,
|
principal_id=principal.id,
|
||||||
permission_target_id=permission_target.id,
|
permission_target_id=permission_target.id,
|
||||||
permission=permission,
|
permission=permission,
|
||||||
grant_type="permit",
|
grant_type=grant_type,
|
||||||
)
|
)
|
||||||
db.session.add(permission_assignment)
|
db.session.add(permission_assignment)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
elif permission_assignment.grant_type != grant_type:
|
||||||
|
permission_assignment.grant_type = grant_type
|
||||||
|
db.session.add(permission_assignment)
|
||||||
|
db.session.commit()
|
||||||
return permission_assignment
|
return permission_assignment
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -697,13 +711,21 @@ class AuthorizationService:
|
||||||
cls, group_identifier: str, permission: str, target: str
|
cls, group_identifier: str, permission: str, target: str
|
||||||
) -> list[PermissionAssignmentModel]:
|
) -> list[PermissionAssignmentModel]:
|
||||||
group = UserService.find_or_create_group(group_identifier)
|
group = UserService.find_or_create_group(group_identifier)
|
||||||
permissions_to_assign = cls.explode_permissions(permission, target)
|
grant_type = "permit"
|
||||||
|
target_without_deny = target
|
||||||
|
if target.startswith("DENY:"):
|
||||||
|
target_without_deny = target.removeprefix("DENY:")
|
||||||
|
grant_type = "deny"
|
||||||
|
permissions_to_assign = cls.explode_permissions(permission, target_without_deny)
|
||||||
permission_assignments = []
|
permission_assignments = []
|
||||||
for permission_to_assign in permissions_to_assign:
|
for permission_to_assign in permissions_to_assign:
|
||||||
permission_target = cls.find_or_create_permission_target(permission_to_assign.target_uri)
|
permission_target = cls.find_or_create_permission_target(permission_to_assign.target_uri)
|
||||||
permission_assignments.append(
|
permission_assignments.append(
|
||||||
cls.create_permission_for_principal(
|
cls.create_permission_for_principal(
|
||||||
group.principal, permission_target, permission_to_assign.permission
|
principal=group.principal,
|
||||||
|
permission_target=permission_target,
|
||||||
|
permission=permission_to_assign.permission,
|
||||||
|
grant_type=grant_type,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return permission_assignments
|
return permission_assignments
|
||||||
|
|
|
@ -312,9 +312,12 @@ class BaseTest:
|
||||||
username: str,
|
username: str,
|
||||||
target_uri: str = PermissionTargetModel.URI_ALL,
|
target_uri: str = PermissionTargetModel.URI_ALL,
|
||||||
permission_names: list[str] | None = None,
|
permission_names: list[str] | None = None,
|
||||||
|
grant_type: str = "permit",
|
||||||
) -> UserModel:
|
) -> UserModel:
|
||||||
user = BaseTest.find_or_create_user(username=username)
|
user = BaseTest.find_or_create_user(username=username)
|
||||||
return cls.add_permissions_to_user(user, target_uri=target_uri, permission_names=permission_names)
|
return cls.add_permissions_to_user(
|
||||||
|
user, target_uri=target_uri, permission_names=permission_names, grant_type=grant_type
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def add_permissions_to_user(
|
def add_permissions_to_user(
|
||||||
|
@ -322,14 +325,17 @@ class BaseTest:
|
||||||
user: UserModel,
|
user: UserModel,
|
||||||
target_uri: str = PermissionTargetModel.URI_ALL,
|
target_uri: str = PermissionTargetModel.URI_ALL,
|
||||||
permission_names: list[str] | None = None,
|
permission_names: list[str] | None = None,
|
||||||
|
grant_type: str = "permit",
|
||||||
) -> UserModel:
|
) -> UserModel:
|
||||||
principal = user.principal
|
principal = user.principal
|
||||||
cls.add_permissions_to_principal(principal, target_uri=target_uri, permission_names=permission_names)
|
cls.add_permissions_to_principal(
|
||||||
|
principal, target_uri=target_uri, permission_names=permission_names, grant_type=grant_type
|
||||||
|
)
|
||||||
return user
|
return user
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def add_permissions_to_principal(
|
def add_permissions_to_principal(
|
||||||
cls, principal: PrincipalModel, target_uri: str, permission_names: list[str] | None
|
cls, principal: PrincipalModel, target_uri: str, permission_names: list[str] | None, grant_type: str = "permit"
|
||||||
) -> None:
|
) -> None:
|
||||||
permission_target = AuthorizationService.find_or_create_permission_target(target_uri)
|
permission_target = AuthorizationService.find_or_create_permission_target(target_uri)
|
||||||
|
|
||||||
|
@ -341,6 +347,7 @@ class BaseTest:
|
||||||
principal=principal,
|
principal=principal,
|
||||||
permission_target=permission_target,
|
permission_target=permission_target,
|
||||||
permission=permission,
|
permission=permission,
|
||||||
|
grant_type=grant_type,
|
||||||
)
|
)
|
||||||
|
|
||||||
def assert_user_has_permission(
|
def assert_user_has_permission(
|
||||||
|
|
|
@ -106,12 +106,16 @@ class TestProcessApi(BaseTest):
|
||||||
principal = group.principal
|
principal = group.principal
|
||||||
UserService.add_user_to_group(user, group)
|
UserService.add_user_to_group(user, group)
|
||||||
self.add_permissions_to_principal(principal, target_uri="/v1.0/process-groups/%", permission_names=["read"])
|
self.add_permissions_to_principal(principal, target_uri="/v1.0/process-groups/%", permission_names=["read"])
|
||||||
|
self.add_permissions_to_principal(
|
||||||
|
principal, target_uri="/v1.0/process-groups/deny_group:%", permission_names=["create"], grant_type="deny"
|
||||||
|
)
|
||||||
self.add_permissions_to_principal(
|
self.add_permissions_to_principal(
|
||||||
principal, target_uri="/v1.0/process-groups/test_group:%", permission_names=["create"]
|
principal, target_uri="/v1.0/process-groups/test_group:%", permission_names=["create"]
|
||||||
)
|
)
|
||||||
request_body = {
|
request_body = {
|
||||||
"requests_to_check": {
|
"requests_to_check": {
|
||||||
"/v1.0/process-groups": ["GET", "POST"],
|
"/v1.0/process-groups": ["GET", "POST"],
|
||||||
|
"/v1.0/process-groups/deny_group:hey": ["GET", "POST"],
|
||||||
"/v1.0/process-groups/test_group": ["GET", "POST"],
|
"/v1.0/process-groups/test_group": ["GET", "POST"],
|
||||||
"/v1.0/process-models": ["GET"],
|
"/v1.0/process-models": ["GET"],
|
||||||
}
|
}
|
||||||
|
@ -119,6 +123,7 @@ class TestProcessApi(BaseTest):
|
||||||
expected_response_body = {
|
expected_response_body = {
|
||||||
"results": {
|
"results": {
|
||||||
"/v1.0/process-groups": {"GET": True, "POST": False},
|
"/v1.0/process-groups": {"GET": True, "POST": False},
|
||||||
|
"/v1.0/process-groups/deny_group:hey": {"GET": True, "POST": False},
|
||||||
"/v1.0/process-groups/test_group": {"GET": True, "POST": True},
|
"/v1.0/process-groups/test_group": {"GET": True, "POST": True},
|
||||||
"/v1.0/process-models": {"GET": False},
|
"/v1.0/process-models": {"GET": False},
|
||||||
}
|
}
|
||||||
|
@ -3295,7 +3300,7 @@ class TestProcessApi(BaseTest):
|
||||||
assert process_instance_one.status == "user_input_required"
|
assert process_instance_one.status == "user_input_required"
|
||||||
|
|
||||||
response = client.get(
|
response = client.get(
|
||||||
f"/v1.0/process-data/{self.modify_process_identifier_for_path_param(process_model.id)}/{process_instance_one.id}/the_data_object_var",
|
f"/v1.0/process-data/{self.modify_process_identifier_for_path_param(process_model.id)}/the_data_object_var/{process_instance_one.id}",
|
||||||
headers=self.logged_in_headers(with_super_admin_user),
|
headers=self.logged_in_headers(with_super_admin_user),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -592,3 +592,33 @@ class TestAuthorizationService(BaseTest):
|
||||||
waiting_assignments = UserGroupAssignmentWaitingModel.query.all()
|
waiting_assignments = UserGroupAssignmentWaitingModel.query.all()
|
||||||
# ensure we didn't delete all of the user group assignments
|
# ensure we didn't delete all of the user group assignments
|
||||||
assert len(waiting_assignments) > 0
|
assert len(waiting_assignments) > 0
|
||||||
|
|
||||||
|
def test_can_deny_access_with_permission(
|
||||||
|
self,
|
||||||
|
app: Flask,
|
||||||
|
client: FlaskClient,
|
||||||
|
with_db_and_bpmn_file_cleanup: None,
|
||||||
|
) -> None:
|
||||||
|
user = self.find_or_create_user(username="user_one")
|
||||||
|
user_group = UserService.find_or_create_group("group_one")
|
||||||
|
UserService.add_user_to_group(user, user_group)
|
||||||
|
AuthorizationService.add_permission_from_uri_or_macro(user_group.identifier, "read", "PG:hey")
|
||||||
|
AuthorizationService.add_permission_from_uri_or_macro(user_group.identifier, "read", "DENY:PG:hey:yo")
|
||||||
|
AuthorizationService.add_permission_from_uri_or_macro(
|
||||||
|
user_group.identifier, "read", "DENY:/process-groups/hey:new"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assert_user_has_permission(user, "read", "/v1.0/process-groups/hey")
|
||||||
|
|
||||||
|
# test specific uri deny
|
||||||
|
self.assert_user_has_permission(user, "read", "/v1.0/process-groups/hey:yo", expected_result=False)
|
||||||
|
self.assert_user_has_permission(user, "read", "/v1.0/process-groups/hey:yo:me", expected_result=False)
|
||||||
|
|
||||||
|
# test wildcard deny
|
||||||
|
self.assert_user_has_permission(user, "read", "/v1.0/process-groups/hey:new", expected_result=False)
|
||||||
|
self.assert_user_has_permission(user, "read", "/v1.0/process-groups/hey:new:group", expected_result=True)
|
||||||
|
|
||||||
|
# test it can be permitted again
|
||||||
|
AuthorizationService.add_permission_from_uri_or_macro(user_group.identifier, "read", "PG:hey:yo")
|
||||||
|
self.assert_user_has_permission(user, "read", "/v1.0/process-groups/hey:yo", expected_result=True)
|
||||||
|
self.assert_user_has_permission(user, "read", "/v1.0/process-groups/hey:yo:me", expected_result=True)
|
||||||
|
|
|
@ -29,6 +29,8 @@ export interface Onboarding {
|
||||||
export interface ProcessData {
|
export interface ProcessData {
|
||||||
process_data_identifier: string;
|
process_data_identifier: string;
|
||||||
process_data_value: any;
|
process_data_value: any;
|
||||||
|
|
||||||
|
authorized?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RecentProcessModel {
|
export interface RecentProcessModel {
|
||||||
|
|
|
@ -619,6 +619,21 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
|
||||||
|
|
||||||
const processDataDisplayArea = () => {
|
const processDataDisplayArea = () => {
|
||||||
if (processDataToDisplay) {
|
if (processDataToDisplay) {
|
||||||
|
let bodyComponent = (
|
||||||
|
<>
|
||||||
|
<p>Value:</p>
|
||||||
|
<pre>{JSON.stringify(processDataToDisplay.process_data_value)}</pre>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
if (processDataToDisplay.authorized === false) {
|
||||||
|
bodyComponent = (
|
||||||
|
<>
|
||||||
|
{childrenForErrorObject(
|
||||||
|
errorForDisplayFromString(processDataToDisplay.process_data_value)
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
open={!!processDataToDisplay}
|
open={!!processDataToDisplay}
|
||||||
|
@ -627,8 +642,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
|
||||||
>
|
>
|
||||||
<h2>Data Object: {processDataToDisplay.process_data_identifier}</h2>
|
<h2>Data Object: {processDataToDisplay.process_data_identifier}</h2>
|
||||||
<br />
|
<br />
|
||||||
<p>Value:</p>
|
{bodyComponent}
|
||||||
<pre>{JSON.stringify(processDataToDisplay.process_data_value)}</pre>
|
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -639,6 +653,18 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
|
||||||
setProcessDataToDisplay(processData);
|
setProcessDataToDisplay(processData);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleProcessDataShowReponseUnauthorized = (
|
||||||
|
dataObjectIdentifer: string,
|
||||||
|
result: any
|
||||||
|
) => {
|
||||||
|
const processData: ProcessData = {
|
||||||
|
process_data_identifier: dataObjectIdentifer,
|
||||||
|
process_data_value: result.message,
|
||||||
|
authorized: false,
|
||||||
|
};
|
||||||
|
setProcessDataToDisplay(processData);
|
||||||
|
};
|
||||||
|
|
||||||
const handleClickedDiagramTask = (
|
const handleClickedDiagramTask = (
|
||||||
shapeElement: any,
|
shapeElement: any,
|
||||||
bpmnProcessIdentifiers: any
|
bpmnProcessIdentifiers: any
|
||||||
|
@ -646,9 +672,11 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
|
||||||
if (shapeElement.type === 'bpmn:DataObjectReference') {
|
if (shapeElement.type === 'bpmn:DataObjectReference') {
|
||||||
const dataObjectIdentifer = shapeElement.businessObject.dataObjectRef.id;
|
const dataObjectIdentifer = shapeElement.businessObject.dataObjectRef.id;
|
||||||
HttpService.makeCallToBackend({
|
HttpService.makeCallToBackend({
|
||||||
path: `/process-data/${params.process_model_id}/${params.process_instance_id}/${dataObjectIdentifer}`,
|
path: `/process-data/${params.process_model_id}/${dataObjectIdentifer}/${params.process_instance_id}`,
|
||||||
httpMethod: 'GET',
|
httpMethod: 'GET',
|
||||||
successCallback: handleProcessDataShowResponse,
|
successCallback: handleProcessDataShowResponse,
|
||||||
|
onUnauthorized: (result: any) =>
|
||||||
|
handleProcessDataShowReponseUnauthorized(dataObjectIdentifer, result),
|
||||||
});
|
});
|
||||||
} else if (tasks) {
|
} else if (tasks) {
|
||||||
const matchingTask: Task | undefined = tasks.find((task: Task) => {
|
const matchingTask: Task | undefined = tasks.find((task: Task) => {
|
||||||
|
|
Loading…
Reference in New Issue