-
-
Notifications
You must be signed in to change notification settings - Fork 167
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
Lint, test, fix mongo helper partial permissions #4776
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,13 @@ | ||
# coding: utf-8 | ||
from __future__ import annotations | ||
|
||
import json | ||
from collections import defaultdict | ||
from dataclasses import dataclass | ||
from typing import Optional | ||
|
||
from django.db import transaction | ||
from django.contrib.auth.models import Permission, User | ||
from django.db import transaction | ||
from django.urls import Resolver404 | ||
from django.utils.translation import gettext as t | ||
from rest_framework import serializers | ||
|
@@ -27,24 +28,22 @@ | |
) | ||
from kpi.utils.urls import absolute_resolve | ||
|
||
|
||
ASSIGN_OWNER_ERROR_MESSAGE = "Owner's permissions cannot be assigned explicitly" | ||
|
||
|
||
class AssetPermissionAssignmentSerializer(serializers.ModelSerializer): | ||
|
||
url = serializers.SerializerMethodField() | ||
user = RelativePrefixHyperlinkedRelatedField( | ||
view_name='user-detail', | ||
lookup_field='username', | ||
queryset=User.objects.all(), | ||
style={'base_template': 'input.html'} # Render as a simple text box | ||
style={'base_template': 'input.html'}, # Render as a simple text box | ||
) | ||
permission = RelativePrefixHyperlinkedRelatedField( | ||
view_name='permission-detail', | ||
lookup_field='codename', | ||
queryset=Permission.objects.all(), | ||
style={'base_template': 'input.html'} # Render as a simple text box | ||
style={'base_template': 'input.html'}, # Render as a simple text box | ||
) | ||
partial_permissions = serializers.SerializerMethodField() | ||
label = serializers.SerializerMethodField() | ||
|
@@ -65,9 +64,7 @@ def create(self, validated_data): | |
user = validated_data['user'] | ||
asset = validated_data['asset'] | ||
if asset.owner_id == user.id: | ||
raise serializers.ValidationError( | ||
{'user': t(ASSIGN_OWNER_ERROR_MESSAGE)} | ||
) | ||
raise serializers.ValidationError({'user': t(ASSIGN_OWNER_ERROR_MESSAGE)}) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's going over the 80 characters limit (but below 90*). Did you do this in purpose or your linter is configured this way?
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It appears screwed up my local config to use default 88 when it was already set to 80. Using the formatter with 80 is OK right? I will update. Note the formatter greatly helps in the (soon to be) large permission dict (") and json (') that we need in docs and tests. |
||
permission = validated_data['permission'] | ||
partial_permissions = validated_data.get('partial_permissions', None) | ||
|
||
|
@@ -85,9 +82,7 @@ def get_label(self, object_permission): | |
except KeyError: | ||
return object_permission.label | ||
else: | ||
return asset.get_label_for_permission( | ||
object_permission.permission.codename | ||
) | ||
return asset.get_label_for_permission(object_permission.permission.codename) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same comment as #4776 (comment) |
||
|
||
def get_partial_permissions(self, object_permission): | ||
codename = object_permission.permission.codename | ||
|
@@ -99,26 +94,26 @@ def get_partial_permissions(self, object_permission): | |
# TODO: optimize `asset.get_partial_perms()` so it doesn't execute | ||
# a new query for each assignment | ||
partial_perms = asset.get_partial_perms( | ||
object_permission.user_id, with_filters=True) | ||
object_permission.user_id, with_filters=True | ||
) | ||
if not partial_perms: | ||
return None | ||
|
||
if partial_perms: | ||
hyperlinked_partial_perms = [] | ||
for perm_codename, filters in partial_perms.items(): | ||
url = self.__get_permission_hyperlink(perm_codename) | ||
hyperlinked_partial_perms.append({ | ||
'url': url, | ||
'filters': filters | ||
}) | ||
hyperlinked_partial_perms.append({'url': url, 'filters': filters}) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
return hyperlinked_partial_perms | ||
return None | ||
|
||
def get_url(self, object_permission): | ||
asset_uid = self.context.get('asset_uid') | ||
return reverse('asset-permission-assignment-detail', | ||
args=(asset_uid, object_permission.uid), | ||
request=self.context.get('request', None)) | ||
return reverse( | ||
'asset-permission-assignment-detail', | ||
args=(asset_uid, object_permission.uid), | ||
request=self.context.get('request', None), | ||
) | ||
|
||
def validate(self, attrs): | ||
# Because `partial_permissions` is a `SerializerMethodField`, | ||
|
@@ -145,9 +140,7 @@ def validate_partial_permissions(self, attrs): | |
return attrs | ||
|
||
def _invalid_partial_permissions(message): | ||
raise serializers.ValidationError( | ||
{'partial_permissions': message} | ||
) | ||
raise serializers.ValidationError({'partial_permissions': message}) | ||
|
||
request = self.context['request'] | ||
partial_permissions = None | ||
|
@@ -166,12 +159,11 @@ def _invalid_partial_permissions(message): | |
|
||
partial_permissions_attr = defaultdict(list) | ||
|
||
for partial_permission, filters_ in \ | ||
self.__get_partial_permissions_generator(partial_permissions): | ||
for partial_permission, filters_ in self.__get_partial_permissions_generator( | ||
partial_permissions | ||
): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about for (
partial_permission,
filters_,
) in self.__get_partial_permissions_generator(partial_permissions): instead. It's |
||
try: | ||
resolver_match = absolute_resolve( | ||
partial_permission.get('url') | ||
) | ||
resolver_match = absolute_resolve(partial_permission.get('url')) | ||
except (TypeError, Resolver404): | ||
_invalid_partial_permissions(t('Invalid `url`')) | ||
|
||
|
@@ -181,8 +173,7 @@ def _invalid_partial_permissions(message): | |
_invalid_partial_permissions(t('Invalid `url`')) | ||
|
||
# Permission must valid and must be assignable. | ||
if not self._validate_permission(codename, | ||
SUFFIX_SUBMISSIONS_PERMS): | ||
if not self._validate_permission(codename, SUFFIX_SUBMISSIONS_PERMS): | ||
_invalid_partial_permissions(t('Invalid `url`')) | ||
|
||
# No need to validate Mongo syntax, query will fail | ||
|
@@ -249,11 +240,11 @@ def _validate_permission(self, codename, suffix=None): | |
""" | ||
return ( | ||
# DONOTMERGE abusive to the database server? | ||
codename in Asset.objects.only('asset_type').get( | ||
uid=self.context['asset_uid'] | ||
).get_assignable_permissions( | ||
with_partial=True | ||
) and (suffix is None or codename.endswith(suffix)) | ||
codename | ||
in Asset.objects.only('asset_type') | ||
.get(uid=self.context['asset_uid']) | ||
.get_assignable_permissions(with_partial=True) | ||
and (suffix is None or codename.endswith(suffix)) | ||
) | ||
|
||
def __get_partial_permissions_generator(self, partial_permissions): | ||
|
@@ -275,13 +266,14 @@ def __get_permission_hyperlink(self, codename): | |
:param codename: str | ||
:return: str. url | ||
""" | ||
return reverse('permission-detail', | ||
args=(codename,), | ||
request=self.context.get('request', None)) | ||
return reverse( | ||
'permission-detail', | ||
args=(codename,), | ||
request=self.context.get('request', None), | ||
) | ||
|
||
|
||
class PartialPermissionField(serializers.Field): | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💔 again. |
||
default_error_messages = { | ||
'invalid': t('Not a valid list.'), | ||
'blank': t('This field may not be blank.'), | ||
|
@@ -301,7 +293,6 @@ def to_representation(self, value): | |
|
||
|
||
class PermissionAssignmentSerializer(serializers.Serializer): | ||
|
||
user = serializers.CharField() | ||
permission = serializers.CharField() | ||
partial_permissions = PartialPermissionField() | ||
|
@@ -318,11 +309,13 @@ class AssetBulkInsertPermissionSerializer(serializers.Serializer): | |
Warning: If less queries are sent to DB, it consumes more CPU and memory. | ||
The bigger the assignments are, the bigger the resources footprint will be. | ||
""" | ||
|
||
assignments = serializers.ListField(child=PermissionAssignmentSerializer()) | ||
|
||
@dataclass(frozen=True) | ||
class PermissionAssignment: | ||
""" A more-explicit alternative to a simple tuple """ | ||
"""A more-explicit alternative to a simple tuple""" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Your linter really don't like when the code breaths a little :-P """
A more-explicit alternative to a simple tuple
""" I prefer this way even if the sentence is short. Moreover if there is a diff at a later time, it does not involve the triple quotes. |
||
|
||
user_pk: int | ||
permission_codename: str | ||
partial_permissions_json: Optional[str] = None | ||
|
@@ -355,7 +348,7 @@ def create(self, validated_data): | |
partial_perms = json.loads(addition.partial_permissions_json) | ||
else: | ||
partial_perms = None | ||
perm = asset.assign_perm( | ||
asset.assign_perm( | ||
user_obj=user_pk_to_obj_cache[addition.user_pk], | ||
perm=addition.permission_codename, | ||
partial_perms=partial_perms, | ||
|
@@ -392,9 +385,7 @@ def get_set_of_existing_assignments( | |
): | ||
# Expand the stupid cache to include any users present in the | ||
# existing assignments but not in the incoming assignments | ||
user_pk_to_obj_cache[ | ||
assignment_in_db.user_id | ||
] = assignment_in_db.user | ||
user_pk_to_obj_cache[assignment_in_db.user_id] = assignment_in_db.user | ||
|
||
if assignment_in_db.permission.codename == PERM_PARTIAL_SUBMISSIONS: | ||
partial_permissions_json = json.dumps( | ||
|
@@ -430,9 +421,9 @@ def get_set_of_incoming_assignments( | |
# always require a fully-fledged `User` object? Until then, keep a | ||
# stupid object cache thing because `assign_perm()` and | ||
# `remove_perm()` REQUIRE user objects | ||
user_pk_to_obj_cache[ | ||
incoming_assignment['user'].pk | ||
] = incoming_assignment['user'] | ||
user_pk_to_obj_cache[incoming_assignment['user'].pk] = incoming_assignment[ | ||
'user' | ||
] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same comment as #4776 (comment) |
||
|
||
# Expand to include implied permissions | ||
for implied_codename in asset.get_implied_perms( | ||
|
@@ -448,8 +439,7 @@ def get_set_of_incoming_assignments( | |
# Expand to include implied partial permissions | ||
if incoming_permission.codename == PERM_PARTIAL_SUBMISSIONS: | ||
partial_permissions = json.dumps( | ||
AssetUserPartialPermission\ | ||
.update_partial_perms_to_include_implied( | ||
AssetUserPartialPermission.update_partial_perms_to_include_implied( | ||
asset, incoming_assignment['partial_permissions'] | ||
), | ||
sort_keys=True, | ||
|
@@ -475,22 +465,18 @@ def validate(self, attrs): | |
# …for looking up by API permission URLs by codename | ||
codename_to_url = dict() | ||
|
||
assignable_permissions = self.context[ | ||
'asset' | ||
].get_assignable_permissions(with_partial=True) | ||
assignable_permissions = self.context['asset'].get_assignable_permissions( | ||
with_partial=True | ||
) | ||
|
||
# Perhaps not the best error messages, but they're what DRF was already | ||
# returning | ||
INVALID_PERMISSION_ERROR = { | ||
'permission': t('Invalid hyperlink - Object does not exist.') | ||
} | ||
INVALID_USER_ERROR = { | ||
'user': t('Invalid hyperlink - Object does not exist.') | ||
} | ||
INVALID_USER_ERROR = {'user': t('Invalid hyperlink - Object does not exist.')} | ||
# This matches the behavior of `AssetPermissionAssignmentSerializer` | ||
INVALID_PARTIAL_PERMISSION_ERROR = { | ||
'partial_permissions': t('Invalid `url`') | ||
} | ||
INVALID_PARTIAL_PERMISSION_ERROR = {'partial_permissions': t('Invalid `url`')} | ||
|
||
# Fill in the dictionaries by parsing the incoming assignments | ||
for assignment in attrs['assignments']: | ||
|
@@ -503,16 +489,19 @@ def validate(self, attrs): | |
username = self._get_arg_from_url('username', user_url) | ||
username_to_url[username] = user_url | ||
for partial_assignment in assignment.get('partial_permissions', []): | ||
if 'filters' not in partial_assignment: | ||
# Instead of this, we should validate using DRF | ||
raise serializers.ValidationError( | ||
'Permission assignment must contain filters' | ||
) | ||
partial_codename = self._get_arg_from_url( | ||
'codename', partial_assignment['url'] | ||
) | ||
if not ( | ||
partial_codename in assignable_permissions | ||
and partial_codename.endswith(SUFFIX_SUBMISSIONS_PERMS) | ||
): | ||
raise serializers.ValidationError( | ||
INVALID_PARTIAL_PERMISSION_ERROR | ||
) | ||
raise serializers.ValidationError(INVALID_PARTIAL_PERMISSION_ERROR) | ||
codename_to_url[partial_codename] = partial_assignment['url'] | ||
|
||
# Create a dictionary of API user URLs to `User` objects | ||
|
@@ -537,9 +526,7 @@ def validate(self, attrs): | |
if len(url_to_permission) != len(codename_to_url): | ||
# This should never happen since all codenames were found within | ||
# `assignable_permissions` | ||
raise RuntimeError( | ||
'Unexpected mismatch while processing permissions' | ||
) | ||
raise RuntimeError('Unexpected mismatch while processing permissions') | ||
|
||
# Rewrite the incoming assignments, replacing user and permission URLs | ||
# with their corresponding model instance objects | ||
|
@@ -553,16 +540,14 @@ def validate(self, attrs): | |
assignment_with_objects['permission'].codename | ||
== PERM_PARTIAL_SUBMISSIONS | ||
): | ||
assignment_with_objects['partial_permissions'] = defaultdict( | ||
list | ||
) | ||
assignment_with_objects['partial_permissions'] = defaultdict(list) | ||
for partial_assignment in assignment['partial_permissions']: | ||
partial_codename = url_to_permission[ | ||
partial_assignment['url'] | ||
].codename | ||
assignment_with_objects['partial_permissions'][ | ||
partial_codename | ||
] = partial_assignment['filters'] | ||
] = partial_assignment.get('filters') | ||
assignments_with_objects.append(assignment_with_objects) | ||
|
||
attrs['assignments'] = assignments_with_objects | ||
|
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.
💔 . Your linter is going to be crazy on all my code. I do add an extra line in purpose.
IMHO, It's more compliant with separated blocks of docstring and variables declaration.
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 happens several times in this PR. I would keep the extra line at the beginning of classes (and methods).