Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rest interface to manage roles on objects #1693

Merged
merged 3 commits into from
Dec 9, 2021
Merged

Conversation

mdellweg
Copy link
Member

This is based on #1627 .

@mdellweg mdellweg force-pushed the roles_rest branch 2 times, most recently from 41b8627 to 6bad367 Compare November 9, 2021 12:25
@mdellweg mdellweg force-pushed the roles_rest branch 10 times, most recently from 789efc5 to a24e479 Compare November 12, 2021 10:58
return qs.distinct()


def get_groups_with_perms_roles(obj, attach_perms=False, only_with_perms_in=None):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we get a version of this that has attach_roles? I'd like to know what roles a group has on an object.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dislike the fact, that by switching the a flag, your functions return type changes. It is here in that way only because guardian introduced this interface. I think, i am going to make a second function for this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 to putting this in a different function

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, that one is different. That one is attaching the role names. This here is attaching the corresponding permissions (to match the guardian interface). Maybe both are interesting to us.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do the get_{users,groups}_with_perms_attached_{perms, roles} serve your needs?

from pulpcore.app.models.role import GroupRole, Role, UserRole

User = get_user_model()

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add aget_perms_for_model that returns a list of permissions for a model (should be pretty easy: https://stackoverflow.com/questions/38391729/how-to-retrieve-all-permissions-of-a-specific-model-in-django)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would you return the Permissions? Would a QuerySet be OK?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mdellweg a queryset is fine. This is one of the guardian functions that we use. https://django-guardian.readthedocs.io/en/stable/api/guardian.shortcuts.html#get-perms-for-model

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be readily available now.

@mdellweg mdellweg force-pushed the roles_rest branch 2 times, most recently from 365776b to 2967765 Compare November 15, 2021 13:45
@mdellweg mdellweg marked this pull request as ready for review November 15, 2021 13:46
User = get_user_model()


def assign_role(rolename, entity, obj=None):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've noticed that this allows setting roles on objects that don't have any permissions related to the object. For example, I can create a role with perms = [galaxy.change_namespace] to a pulp container repository, which doesn't make any sense.

It might be a good idea for this function to either fail if none of the permissions are relevant to the object.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it raised an exception now.

@@ -440,3 +475,157 @@ def destroy(self, request, group_pk, pk):
group.user_set.remove(user)
group.save()
return Response(status=status.HTTP_204_NO_CONTENT)


class RoleFilter(BaseFilterSet):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to request a filter where we can specify a list of permissions and return any role that has at least one permission in the list.

Thinking forward to the UI changes we'll need to make, our UI is going to have to query the API for a list of roles whenever a user assigns a role to a specific object, and the user experience will be pretty bad if the UI has to display all the roles in the system.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you can express this as a filter() expression on the Role.objects manager, i can turn it into a drf_filter. I just don't see it from the top of my head.
Querying all roles with a single permission should be manageble somehow.

return qs.filter(Q(pk__in=user_role_pks) | Q(pk__in=group_role_pks))


def get_objects_for_user(user, permission_name, qs):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

implemented now.

@newswangerd
Copy link
Contributor

Here's the PoC so far for removing guardian: ansible/galaxy_ng#1057

So far everything is working. We're just missing get_objects_for_group

@mdellweg mdellweg changed the title WIP/RFC Rest interface to manage roles on objects RFC Rest interface to manage roles on objects Nov 16, 2021
@mdellweg mdellweg force-pushed the roles_rest branch 3 times, most recently from 60b8037 to 2b22993 Compare November 16, 2021 13:12
mdellweg added a commit to mdellweg/pulp_container that referenced this pull request Nov 17, 2021
mdellweg added a commit to mdellweg/pulp_container that referenced this pull request Nov 17, 2021
@mdellweg mdellweg changed the title RFC Rest interface to manage roles on objects Rest interface to manage roles on objects Dec 2, 2021
class RolesMixin:
@extend_schema(description="List user roles assigned to this object.")
@action(detail=True, methods=["get"], serializer_class=NestedUserRoleSerializer)
def list_user_roles(self, request, pk):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we're adding groups I imagined the API call names would be:

add_roles
remove_roles
list_roles
my_permissions

@mdellweg mdellweg force-pushed the roles_rest branch 2 times, most recently from bc4581e to a35dc18 Compare December 3, 2021 17:05
@bmbouter
Copy link
Member

bmbouter commented Dec 3, 2021

One thing I realized as I wrote this story was that we need to use these interfaces for managing role assignment to both objects directly and "model-level" roles. So the 4 methods in RoleMixin need to be callable both on specific objects for detail views and also non-detail views for model-level roles. See the story for more details on this, and let me know what you think about this also.

@mdellweg
Copy link
Member Author

mdellweg commented Dec 3, 2021

One thing I realized as I wrote this story was that we need to use these interfaces for managing role assignment to both objects directly and "model-level" roles. So the 4 methods in RoleMixin need to be callable both on specific objects for detail views and also non-detail views for model-level roles. See the story for more details on this, and let me know what you think about this also.

The module level roles is covered by the corresponding viewsets nested under user and groups. (They are obviously admin interfaces.)

@pulp pulp deleted a comment from pulpbot Dec 6, 2021
@pulpbot
Copy link
Member

pulpbot commented Dec 6, 2021

Attached issue: https://pulp.plan.io/issues/9604

Attached issue: https://pulp.plan.io/issues/9604

@mdellweg mdellweg force-pushed the roles_rest branch 2 times, most recently from 22cb46d to 8223177 Compare December 6, 2021 16:29
Copy link
Member

@bmbouter bmbouter left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall this is looking good to me. I left one little nitpick question about the name of a role. I ran this and the openAPI schema it produces looks good too. Thanks!

@mdellweg mdellweg force-pushed the roles_rest branch 2 times, most recently from ea0d712 to cad5d66 Compare December 8, 2021 18:38
Copy link
Member

@goosemania goosemania left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left some non-blocking comments. Up to you whether they are of any concern.
Thanks for your work!

@@ -4,6 +4,7 @@
# Import Viewsets in platform that are potentially useful to plugin writers
from pulpcore.app.viewsets import ( # noqa
AsyncUpdateMixin,
RolesMixin,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The list of imports is in alphabetic order.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then AlternateContentSourceViewSet should have gone first. I figured Mixins in alphabetical order, then viewsets.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it should have been first, yes

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:'<,'>!sort
There were more inconsistencies. 😆

Comment on lines 563 to 572
UserRole.objects.bulk_create(
[
UserRole(
content_object=serializer.validated_data["content_object"],
user=user,
role=serializer.validated_data["role"],
)
for user in serializer.validated_data["users"]
]
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I read it correctly, if I try to add the same role for the same object, this bulk create will fail. Am I right? Is it intentional? Should we use ignore_duplicates=True?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a chance for a race if we rely here on the check in the serializer?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Failing is purposeful. You should not create a roles association that already exists.
Can you explain more what you expect / how you see it fail?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think so, there is a race if both reach the serializer at the same time, one will succeed in the bulk create and the other will 500 error.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, 500 error is the one I'm concerned about.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a common thing in the whole pulpcore codebase.

Copy link
Member

@goosemania goosemania Dec 9, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did git grep bulk_create in pulpcore. One potentially racy place is in artifact_stages when remote artifacts are being created, the rest of bulk_create usage is done in a safe manner, imo. JFYI, I'm fine if it's not taken care of in this PR, probability of that race is low, hard to imagine a concurrent role addition to the same object.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alas, the 500 is not particularly nice to users, but with the transaction, i added now, at least it's either success, or nothing happened, and the user will probably retry immediately.

pulpcore/app/viewsets/base.py Outdated Show resolved Hide resolved
CHANGES/9604.feature Outdated Show resolved Hide resolved
CHANGES/plugin_api/9604.feature Outdated Show resolved Hide resolved
pulpcore/app/serializers/user.py Show resolved Hide resolved
Comment on lines +404 to +418
for user in data["users"]:
qs = UserRole.objects.filter(
content_type_id=obj_type.id, object_id=obj.pk, user=user, role=data["role"]
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mental note that this is an area for improvement if we find that our users are supplying this endpoint with hundreds of users or groups.

pulpcore/app/viewsets/base.py Outdated Show resolved Hide resolved
Comment on lines 544 to 550
if group_role.role.name not in roles:
roles[group_role.role.name] = {
"role": user_role.role.name,
"users": [],
"groups": [],
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if group_role.role.name not in roles:
roles[group_role.role.name] = {
"role": user_role.role.name,
"users": [],
"groups": [],
}
if group_role.role.name not in roles:
roles[group_role.role.name] = {
"role": group_role.role.name,
"users": [],
"groups": [],
}

Copy link
Contributor

@gerrod3 gerrod3 Dec 8, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if this could be shorten with a defaultdict. eg.

roles = defaultdict(lambda: dict(users=list(), groups=list()))
for user_role in user_qs:
    roles[user_role.role.name]["users"].append(user_role.user.username)
for group_role in group_qs:
    roles[group_role.role.name]["groups"].append(group_role.group.name)
result = {"roles": [dict(users_and_groups, role=role) for role, users_and_groups in roles.items()]}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the default dict could inject the new key in the lambda, that would be cool. Also i am a bit weary this may reuse one of the dicts or lists from your lambda.
I'm on the fence.

pulpcore/app/serializers/user.py Outdated Show resolved Hide resolved
Comment on lines 563 to 572
UserRole.objects.bulk_create(
[
UserRole(
content_object=serializer.validated_data["content_object"],
user=user,
role=serializer.validated_data["role"],
)
for user in serializer.validated_data["users"]
]
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think so, there is a race if both reach the serializer at the same time, one will succeed in the bulk create and the other will 500 error.

pulpcore/app/viewsets/base.py Outdated Show resolved Hide resolved
@mdellweg mdellweg force-pushed the roles_rest branch 4 times, most recently from 77934bc to fbcbb3c Compare December 9, 2021 13:35
This mixin provides endpoints to assign and revoke roles on objects to
users or groups.

fixes #9604
@mdellweg mdellweg merged commit 1e8203f into pulp:main Dec 9, 2021
@mdellweg mdellweg deleted the roles_rest branch December 9, 2021 15:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants