✨(backend) add max ancestors role field to document access endpoint
This field is set only on the list view when all accesses for a given document and all its ancestors are listed. It gives the highest role among all accesses related to each document.
This commit is contained in:
committed by
Anthony LC
parent
f782a0236b
commit
1ab237af3b
@@ -124,6 +124,7 @@ class DocumentAccessSerializer(BaseAccessSerializer):
|
|||||||
allow_null=True,
|
allow_null=True,
|
||||||
)
|
)
|
||||||
user = UserSerializer(read_only=True)
|
user = UserSerializer(read_only=True)
|
||||||
|
max_ancestors_role = serializers.SerializerMethodField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.DocumentAccess
|
model = models.DocumentAccess
|
||||||
@@ -136,8 +137,13 @@ class DocumentAccessSerializer(BaseAccessSerializer):
|
|||||||
"team",
|
"team",
|
||||||
"role",
|
"role",
|
||||||
"abilities",
|
"abilities",
|
||||||
|
"max_ancestors_role",
|
||||||
]
|
]
|
||||||
read_only_fields = ["id", "document_id", "abilities"]
|
read_only_fields = ["id", "document_id", "abilities", "max_ancestors_role"]
|
||||||
|
|
||||||
|
def get_max_ancestors_role(self, instance):
|
||||||
|
"""Return max_ancestors_role if annotated; else None."""
|
||||||
|
return getattr(instance, "max_ancestors_role", None)
|
||||||
|
|
||||||
|
|
||||||
class DocumentAccessLightSerializer(DocumentAccessSerializer):
|
class DocumentAccessLightSerializer(DocumentAccessSerializer):
|
||||||
@@ -155,6 +161,7 @@ class DocumentAccessLightSerializer(DocumentAccessSerializer):
|
|||||||
"team",
|
"team",
|
||||||
"role",
|
"role",
|
||||||
"abilities",
|
"abilities",
|
||||||
|
"max_ancestors_role",
|
||||||
]
|
]
|
||||||
read_only_fields = [
|
read_only_fields = [
|
||||||
"id",
|
"id",
|
||||||
@@ -162,6 +169,7 @@ class DocumentAccessLightSerializer(DocumentAccessSerializer):
|
|||||||
"team",
|
"team",
|
||||||
"role",
|
"role",
|
||||||
"abilities",
|
"abilities",
|
||||||
|
"max_ancestors_role",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1551,23 +1551,35 @@ class DocumentAccessViewSet(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Annotate more information on roles
|
# Annotate more information on roles
|
||||||
|
path_to_key_to_max_ancestors_role = defaultdict(
|
||||||
|
lambda: defaultdict(lambda: None)
|
||||||
|
)
|
||||||
path_to_ancestors_roles = defaultdict(list)
|
path_to_ancestors_roles = defaultdict(list)
|
||||||
path_to_role = defaultdict(lambda: None)
|
path_to_role = defaultdict(lambda: None)
|
||||||
for access in accesses:
|
for access in accesses:
|
||||||
if access.user_id == user.id or access.team in user.teams:
|
key = access.target_key
|
||||||
parent_path = access.document_path[: -models.Document.steplen]
|
path = access.document.path
|
||||||
if parent_path:
|
parent_path = path[: -models.Document.steplen]
|
||||||
path_to_ancestors_roles[access.document_path].extend(
|
|
||||||
path_to_ancestors_roles[parent_path]
|
|
||||||
)
|
|
||||||
path_to_ancestors_roles[access.document_path].append(
|
|
||||||
path_to_role[parent_path]
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
path_to_ancestors_roles[access.document_path] = []
|
|
||||||
|
|
||||||
path_to_role[access.document_path] = choices.RoleChoices.max(
|
path_to_key_to_max_ancestors_role[path][key] = choices.RoleChoices.max(
|
||||||
path_to_role[access.document_path], access.role
|
path_to_key_to_max_ancestors_role[path][key], access.role
|
||||||
|
)
|
||||||
|
|
||||||
|
if parent_path:
|
||||||
|
path_to_key_to_max_ancestors_role[path][key] = choices.RoleChoices.max(
|
||||||
|
path_to_key_to_max_ancestors_role[parent_path][key],
|
||||||
|
path_to_key_to_max_ancestors_role[path][key],
|
||||||
|
)
|
||||||
|
path_to_ancestors_roles[path].extend(
|
||||||
|
path_to_ancestors_roles[parent_path]
|
||||||
|
)
|
||||||
|
path_to_ancestors_roles[path].append(path_to_role[parent_path])
|
||||||
|
else:
|
||||||
|
path_to_ancestors_roles[path] = []
|
||||||
|
|
||||||
|
if access.user_id == user.id or access.team in user.teams:
|
||||||
|
path_to_role[path] = choices.RoleChoices.max(
|
||||||
|
path_to_role[path], access.role
|
||||||
)
|
)
|
||||||
|
|
||||||
# serialize and return the response
|
# serialize and return the response
|
||||||
@@ -1575,9 +1587,16 @@ class DocumentAccessViewSet(
|
|||||||
serializer_class = self.get_serializer_class()
|
serializer_class = self.get_serializer_class()
|
||||||
serialized_data = []
|
serialized_data = []
|
||||||
for access in accesses:
|
for access in accesses:
|
||||||
|
path = access.document.path
|
||||||
|
parent_path = path[: -models.Document.steplen]
|
||||||
|
access.max_ancestors_role = (
|
||||||
|
path_to_key_to_max_ancestors_role[parent_path][access.target_key]
|
||||||
|
if parent_path
|
||||||
|
else None
|
||||||
|
)
|
||||||
access.set_user_roles_tuple(
|
access.set_user_roles_tuple(
|
||||||
choices.RoleChoices.max(*path_to_ancestors_roles[access.document_path]),
|
choices.RoleChoices.max(*path_to_ancestors_roles[path]),
|
||||||
path_to_role.get(access.document_path),
|
path_to_role.get(path),
|
||||||
)
|
)
|
||||||
serializer = serializer_class(access, context=context)
|
serializer = serializer_class(access, context=context)
|
||||||
serialized_data.append(serializer.data)
|
serialized_data.append(serializer.data)
|
||||||
|
|||||||
@@ -1140,7 +1140,9 @@ class DocumentAccess(BaseAccess):
|
|||||||
set_role_to.append(RoleChoices.OWNER)
|
set_role_to.append(RoleChoices.OWNER)
|
||||||
|
|
||||||
# Filter out roles that would be lower than the one the user already has
|
# Filter out roles that would be lower than the one the user already has
|
||||||
ancestors_role_priority = RoleChoices.get_priority(ancestors_role)
|
ancestors_role_priority = RoleChoices.get_priority(
|
||||||
|
getattr(self, "max_ancestors_role", None)
|
||||||
|
)
|
||||||
set_role_to = [
|
set_role_to = [
|
||||||
candidate_role
|
candidate_role
|
||||||
for candidate_role in set_role_to
|
for candidate_role in set_role_to
|
||||||
|
|||||||
@@ -148,6 +148,7 @@ def test_api_document_accesses_list_authenticated_related_non_privileged(
|
|||||||
else None,
|
else None,
|
||||||
"team": access.team,
|
"team": access.team,
|
||||||
"role": access.role,
|
"role": access.role,
|
||||||
|
"max_ancestors_role": None,
|
||||||
"abilities": {
|
"abilities": {
|
||||||
"destroy": False,
|
"destroy": False,
|
||||||
"partial_update": False,
|
"partial_update": False,
|
||||||
@@ -248,6 +249,7 @@ def test_api_document_accesses_list_authenticated_related_privileged(
|
|||||||
}
|
}
|
||||||
if access.user
|
if access.user
|
||||||
else None,
|
else None,
|
||||||
|
"max_ancestors_role": None,
|
||||||
"team": access.team,
|
"team": access.team,
|
||||||
"role": access.role,
|
"role": access.role,
|
||||||
"abilities": access.get_abilities(user),
|
"abilities": access.get_abilities(user),
|
||||||
@@ -258,6 +260,245 @@ def test_api_document_accesses_list_authenticated_related_privileged(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_document_accesses_retrieve_set_role_to_child():
|
||||||
|
"""Check set_role_to for an access with no access on the ancestor."""
|
||||||
|
user, other_user = factories.UserFactory.create_batch(2)
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
parent = factories.DocumentFactory()
|
||||||
|
parent_access = factories.UserDocumentAccessFactory(
|
||||||
|
document=parent, user=user, role="owner"
|
||||||
|
)
|
||||||
|
|
||||||
|
document = factories.DocumentFactory(parent=parent)
|
||||||
|
document_access_other_user = factories.UserDocumentAccessFactory(
|
||||||
|
document=document, user=other_user, role="editor"
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.get(f"/api/v1.0/documents/{document.id!s}/accesses/")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
content = response.json()
|
||||||
|
assert len(content) == 2
|
||||||
|
|
||||||
|
result_dict = {
|
||||||
|
result["id"]: result["abilities"]["set_role_to"] for result in content
|
||||||
|
}
|
||||||
|
assert result_dict[str(document_access_other_user.id)] == [
|
||||||
|
"reader",
|
||||||
|
"editor",
|
||||||
|
"administrator",
|
||||||
|
"owner",
|
||||||
|
]
|
||||||
|
assert result_dict[str(parent_access.id)] == []
|
||||||
|
|
||||||
|
# Add an access for the other user on the parent
|
||||||
|
parent_access_other_user = factories.UserDocumentAccessFactory(
|
||||||
|
document=parent, user=other_user, role="editor"
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.get(f"/api/v1.0/documents/{document.id!s}/accesses/")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
content = response.json()
|
||||||
|
assert len(content) == 3
|
||||||
|
|
||||||
|
result_dict = {
|
||||||
|
result["id"]: result["abilities"]["set_role_to"] for result in content
|
||||||
|
}
|
||||||
|
assert result_dict[str(document_access_other_user.id)] == [
|
||||||
|
"editor",
|
||||||
|
"administrator",
|
||||||
|
"owner",
|
||||||
|
]
|
||||||
|
assert result_dict[str(parent_access.id)] == []
|
||||||
|
assert result_dict[str(parent_access_other_user.id)] == [
|
||||||
|
"reader",
|
||||||
|
"editor",
|
||||||
|
"administrator",
|
||||||
|
"owner",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"roles,results",
|
||||||
|
[
|
||||||
|
[
|
||||||
|
["administrator", "reader", "reader", "reader"],
|
||||||
|
[
|
||||||
|
["reader", "editor", "administrator"],
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
["reader", "editor", "administrator"],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
["owner", "reader", "reader", "reader"],
|
||||||
|
[[], [], [], ["reader", "editor", "administrator", "owner"]],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
["owner", "reader", "reader", "owner"],
|
||||||
|
[
|
||||||
|
["reader", "editor", "administrator", "owner"],
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
["reader", "editor", "administrator", "owner"],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_api_document_accesses_list_authenticated_related_same_user(roles, results):
|
||||||
|
"""
|
||||||
|
The maximum role across ancestor documents and set_role_to optionsfor
|
||||||
|
a given user should be filled as expected.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
# Create documents structured as a tree
|
||||||
|
grand_parent = factories.DocumentFactory(link_reach="authenticated")
|
||||||
|
parent = factories.DocumentFactory(parent=grand_parent)
|
||||||
|
document = factories.DocumentFactory(parent=parent)
|
||||||
|
|
||||||
|
# Create accesses for another user
|
||||||
|
other_user = factories.UserFactory()
|
||||||
|
accesses = [
|
||||||
|
factories.UserDocumentAccessFactory(
|
||||||
|
document=document, user=user, role=roles[0]
|
||||||
|
),
|
||||||
|
factories.UserDocumentAccessFactory(
|
||||||
|
document=grand_parent, user=other_user, role=roles[1]
|
||||||
|
),
|
||||||
|
factories.UserDocumentAccessFactory(
|
||||||
|
document=parent, user=other_user, role=roles[2]
|
||||||
|
),
|
||||||
|
factories.UserDocumentAccessFactory(
|
||||||
|
document=document, user=other_user, role=roles[3]
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
response = client.get(f"/api/v1.0/documents/{document.id!s}/accesses/")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
content = response.json()
|
||||||
|
assert len(content) == 4
|
||||||
|
|
||||||
|
for result in content:
|
||||||
|
assert (
|
||||||
|
result["max_ancestors_role"] is None
|
||||||
|
if result["user"]["id"] == str(user.id)
|
||||||
|
else choices.RoleChoices.max(roles[1], roles[2])
|
||||||
|
)
|
||||||
|
|
||||||
|
result_dict = {
|
||||||
|
result["id"]: result["abilities"]["set_role_to"] for result in content
|
||||||
|
}
|
||||||
|
assert [result_dict[str(access.id)] for access in accesses] == results
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"roles,results",
|
||||||
|
[
|
||||||
|
[
|
||||||
|
["administrator", "reader", "reader", "reader"],
|
||||||
|
[
|
||||||
|
["reader", "editor", "administrator"],
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
["reader", "editor", "administrator"],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
["owner", "reader", "reader", "reader"],
|
||||||
|
[[], [], [], ["reader", "editor", "administrator", "owner"]],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
["owner", "reader", "reader", "owner"],
|
||||||
|
[
|
||||||
|
["reader", "editor", "administrator", "owner"],
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
["reader", "editor", "administrator", "owner"],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
["reader", "reader", "reader", "owner"],
|
||||||
|
[["reader", "editor", "administrator", "owner"], [], [], []],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
["reader", "administrator", "reader", "editor"],
|
||||||
|
[
|
||||||
|
["reader", "editor", "administrator"],
|
||||||
|
["reader", "editor", "administrator"],
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
["editor", "editor", "administrator", "editor"],
|
||||||
|
[
|
||||||
|
["reader", "editor", "administrator"],
|
||||||
|
[],
|
||||||
|
["editor", "administrator"],
|
||||||
|
[],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_api_document_accesses_list_authenticated_related_same_team(
|
||||||
|
roles, results, mock_user_teams
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
The maximum role across ancestor documents and set_role_to optionsfor
|
||||||
|
a given team should be filled as expected.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
# Create documents structured as a tree
|
||||||
|
grand_parent = factories.DocumentFactory(link_reach="authenticated")
|
||||||
|
parent = factories.DocumentFactory(parent=grand_parent)
|
||||||
|
document = factories.DocumentFactory(parent=parent)
|
||||||
|
|
||||||
|
mock_user_teams.return_value = ["lasuite", "unknown"]
|
||||||
|
accesses = [
|
||||||
|
factories.UserDocumentAccessFactory(
|
||||||
|
document=document, user=user, role=roles[0]
|
||||||
|
),
|
||||||
|
# Create accesses for a team
|
||||||
|
factories.TeamDocumentAccessFactory(
|
||||||
|
document=grand_parent, team="lasuite", role=roles[1]
|
||||||
|
),
|
||||||
|
factories.TeamDocumentAccessFactory(
|
||||||
|
document=parent, team="lasuite", role=roles[2]
|
||||||
|
),
|
||||||
|
factories.TeamDocumentAccessFactory(
|
||||||
|
document=document, team="lasuite", role=roles[3]
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
response = client.get(f"/api/v1.0/documents/{document.id!s}/accesses/")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
content = response.json()
|
||||||
|
assert len(content) == 4
|
||||||
|
|
||||||
|
for result in content:
|
||||||
|
assert (
|
||||||
|
result["max_ancestors_role"] is None
|
||||||
|
if result["user"] and result["user"]["id"] == str(user.id)
|
||||||
|
else choices.RoleChoices.max(roles[1], roles[2])
|
||||||
|
)
|
||||||
|
|
||||||
|
result_dict = {
|
||||||
|
result["id"]: result["abilities"]["set_role_to"] for result in content
|
||||||
|
}
|
||||||
|
assert [result_dict[str(access.id)] for access in accesses] == results
|
||||||
|
|
||||||
|
|
||||||
def test_api_document_accesses_retrieve_anonymous():
|
def test_api_document_accesses_retrieve_anonymous():
|
||||||
"""
|
"""
|
||||||
Anonymous users should not be allowed to retrieve a document access.
|
Anonymous users should not be allowed to retrieve a document access.
|
||||||
@@ -353,6 +594,7 @@ def test_api_document_accesses_retrieve_authenticated_related(
|
|||||||
"user": access_user,
|
"user": access_user,
|
||||||
"team": "",
|
"team": "",
|
||||||
"role": access.role,
|
"role": access.role,
|
||||||
|
"max_ancestors_role": None,
|
||||||
"abilities": access.get_abilities(user),
|
"abilities": access.get_abilities(user),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -169,6 +169,7 @@ def test_api_document_accesses_create_authenticated_administrator(via, mock_user
|
|||||||
"id": str(new_document_access.id),
|
"id": str(new_document_access.id),
|
||||||
"team": "",
|
"team": "",
|
||||||
"role": role,
|
"role": role,
|
||||||
|
"max_ancestors_role": None,
|
||||||
"user": other_user,
|
"user": other_user,
|
||||||
}
|
}
|
||||||
assert len(mail.outbox) == 1
|
assert len(mail.outbox) == 1
|
||||||
@@ -228,6 +229,7 @@ def test_api_document_accesses_create_authenticated_owner(via, mock_user_teams):
|
|||||||
"user": other_user,
|
"user": other_user,
|
||||||
"team": "",
|
"team": "",
|
||||||
"role": role,
|
"role": role,
|
||||||
|
"max_ancestors_role": None,
|
||||||
"abilities": new_document_access.get_abilities(user),
|
"abilities": new_document_access.get_abilities(user),
|
||||||
}
|
}
|
||||||
assert len(mail.outbox) == 1
|
assert len(mail.outbox) == 1
|
||||||
@@ -293,6 +295,7 @@ def test_api_document_accesses_create_email_in_receivers_language(via, mock_user
|
|||||||
"user": other_user_data,
|
"user": other_user_data,
|
||||||
"team": "",
|
"team": "",
|
||||||
"role": role,
|
"role": role,
|
||||||
|
"max_ancestors_role": None,
|
||||||
"abilities": new_document_access.get_abilities(user),
|
"abilities": new_document_access.get_abilities(user),
|
||||||
}
|
}
|
||||||
assert len(mail.outbox) == index + 1
|
assert len(mail.outbox) == index + 1
|
||||||
|
|||||||
Reference in New Issue
Block a user