-
Notifications
You must be signed in to change notification settings - Fork 119
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
Conversation
41b8627
to
6bad367
Compare
789efc5
to
a24e479
Compare
pulpcore/app/role_util.py
Outdated
return qs.distinct() | ||
|
||
|
||
def get_groups_with_perms_roles(obj, attach_perms=False, only_with_perms_in=None): |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is what I'm using right now: https://github.com/ansible/galaxy_ng/pull/1057/files#diff-f8da9f185c5feab360dce962eae8b91d337ebf9210018e5a5c5b629202be2ed9R11
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
pulpcore/app/role_util.py
Outdated
from pulpcore.app.models.role import GroupRole, Role, UserRole | ||
|
||
User = get_user_model() | ||
|
There was a problem hiding this comment.
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)?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
365776b
to
2967765
Compare
pulpcore/app/role_util.py
Outdated
User = get_user_model() | ||
|
||
|
||
def assign_role(rolename, entity, obj=None): |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
pulpcore/app/viewsets/user.py
Outdated
@@ -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): |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
pulpcore/app/role_util.py
Outdated
return qs.filter(Q(pk__in=user_role_pks) | Q(pk__in=group_role_pks)) | ||
|
||
|
||
def get_objects_for_user(user, permission_name, qs): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like we need get_objects_for_group
too (https://django-guardian.readthedocs.io/en/stable/api/guardian.shortcuts.html#get-objects-for-group)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
implemented now.
Here's the PoC so far for removing guardian: ansible/galaxy_ng#1057 So far everything is working. We're just missing |
60b8037
to
2b22993
Compare
Required PR: pulp/pulpcore#1693 [noissue]
Required PR: pulp/pulpcore#1693 [noissue]
pulpcore/app/viewsets/base.py
Outdated
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): |
There was a problem hiding this comment.
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
bc4581e
to
a35dc18
Compare
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 |
The module level roles is covered by the corresponding viewsets nested under user and groups. (They are obviously admin interfaces.) |
Attached issue: https://pulp.plan.io/issues/9604 Attached issue: https://pulp.plan.io/issues/9604 |
22cb46d
to
8223177
Compare
There was a problem hiding this 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!
ea0d712
to
cad5d66
Compare
There was a problem hiding this 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!
pulpcore/plugin/viewsets/__init__.py
Outdated
@@ -4,6 +4,7 @@ | |||
# Import Viewsets in platform that are potentially useful to plugin writers | |||
from pulpcore.app.viewsets import ( # noqa | |||
AsyncUpdateMixin, | |||
RolesMixin, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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. 😆
pulpcore/app/viewsets/base.py
Outdated
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"] | ||
] | ||
) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
for user in data["users"]: | ||
qs = UserRole.objects.filter( | ||
content_type_id=obj_type.id, object_id=obj.pk, user=user, role=data["role"] | ||
) |
There was a problem hiding this comment.
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
if group_role.role.name not in roles: | ||
roles[group_role.role.name] = { | ||
"role": user_role.role.name, | ||
"users": [], | ||
"groups": [], | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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": [], | |
} |
There was a problem hiding this comment.
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()]}
There was a problem hiding this comment.
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/viewsets/base.py
Outdated
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"] | ||
] | ||
) |
There was a problem hiding this comment.
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.
77934bc
to
fbcbb3c
Compare
This mixin provides endpoints to assign and revoke roles on objects to users or groups. fixes #9604
[noissue]
re #9604
This is based on #1627 .