Skip to content

Instantly share code, notes, and snippets.

@g-cassie
Created August 20, 2015 14:12
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save g-cassie/6b9b0f29371677f48659 to your computer and use it in GitHub Desktop.
Save g-cassie/6b9b0f29371677f48659 to your computer and use it in GitHub Desktop.
This is how we are handling complex permissions right now (this is actually a grossly simplified version). The main problem is permissions logic is split between multiple places. It would be nice to consolidate all permissions logic in permissions.py. Additionally it would be nice to make this more composable so you can have AdminPermission and …
class Organization(models.Model):
name = models.CharField()
class Project(models.Model):
name = models.CharField()
users = models.ManyToManyField('User', related_name='users', through='UserProjectPermission')
organization = models.ForeignKey(Organization)
class User(models.Model):
email = models.EmailField()
organization = models.ForeignKey(Organization)
projects = models.ManyToManyField('Project', related_name='projects', through='UserProjectPermission')
class UserProjectAccess(models.Model):
"""
Associate a User to a Project and specify their access level.
"""
VIEWER = 'VW'
EDITOR = 'ED'
ADMIN = 'AD'
ROLE_CHOICES = ((VIEWER, 'Viewing Only'), (EDITOR, 'Editor'),
(ADMIN, 'Administrator'))
project = models.ForeignKey(Project, related_name='accesses')
user = models.ForeignKey(User, related_name='accesses')
# a bunch of content types that nest under project
class ProjectEvent(models.Model):
project = models.ForeignKey(Project)
class ProjectListItem(models.Model):
project = models.ForeignKey(Project)
class ProjectDeadline(models.Model):
project = models.ForeignKey(Project)
class ProjectObjectPermission(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
"""
Allow access if user is trying to:
1. view an object for a `Project` to which they have viewer or greater
access.
2. edit or create an object for a `Project` to which
they have edit or greater access.
3. delete an object for a `Project` to which they have admin access.
4. Do anything to an object for a `Project` which is owned by an `Organization`
at which the user is an admin.
"""
try:
acccess = request.user.accesses.get(project=obj.project)
except UserProjectAccess.DoesNotExist:
if (obj.project.organization == request.user.organization) and user.is_admin:
return True # user can do anything becuase they are an org admin
return False # user has no affiliation to the project and can do nothing
if request.method in permissions.SAFE_METHODS:
return True
elif request.method == "DELETE":
return access.role == UserProjectAccess.ADMIN
else: # they are trying to edit or create
return access.role in [UserProjectAccess.EDITOR, UserProjectAccess.ADMIN]
ProjectChild = namedtuple('ProjectChild', 'project')
class ProjectObjectViewSet(viewsets.ModelViewSet):
"""
My dream API would take everything in ProjectObjectViewSet and included it in `ProjectObjectPermission`.
"""
permissions = (ProjectObjectPermission,)
def perform_create(self, serializer, **kwargs):
"""
A user cannot create stuff in projects they do not have access to.
"""
obj = ProjectChild(serializer.validated_data['project'])
self.check_object_permissions(self.request, obj)
instance = serializer.save(**kwargs)
return instance
def perform_update(self, serializer, **kwargs):
"""
We use soft deletes (`.is_trashed`) property on every function. As a result we also have custom logic for updates
because only certain users can soft delete certain objects. We use some custom permission methods to try to keep it organized.
"""
pass
def get_queryset(self):
user = self.request.user
query = Q(project__users=user)
if user.is_admin:
query |= Q(deal__organizations=user.organization)
return self.queryset.filter(query).distinct()
class ProjectEventViewSet(ProjectObjectViewSet):
queryset = ProjectEvent.objects.all()
class ProjectListItemViewSet(ProjectObjectViewSet):
queryset = ProjectListItem.objects.all()
class ProjectDeadlineViewSet(ProjectObjectViewSet):
queryset = ProjectDeadline.objects.all()
def perform_delete(self):
# sometimes we will have custom logic for a specific ProjectObject on a specific aciton
# like delete might be possible if a related object is not already deleted.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment