Django per-model authorization permissions - python

Im facing a problem in Django with authorization permissions (a bit new to Django).
I have a teacher, student and manager models.
When a teacher sends a request to my API they should get different permissions than a student (ie, a student will see all of his own test grades, while a teacher can see all of its own class's students, and a manager can see everything).
My questions are as follows:
How do I make all of my models valid system users? I've tried adding
models.OneToOneField(User, on_delete=models.CASCADE)
But this requires creating a user, and then assigning it to the teacher. What I want is for the actual teacher "instance" to be the used user.
How do I check which "type" is my user ? if they are a teacher, student or manager? do I need to go over all 3 tables every time a user sends a request, and figure out which they belong to ? doesnt sound right.
I thought about creating a global 'user' table with a "type" column, but then I wont be able to add specific columns to my models (ie a student should have an avg grade while a teacher shouldn't) .
Would appreciate any pointers in the right direction.

When you need multiple user types, for example, in your case multiple roles are needed like a student, teacher, manager, etc… then you need a different role for all the persons to categorize.
To have these roles you need to extend AbstractUser(for simple case) in your models.py for your User model also You can specify permissions in your models. Attaching permissions is done on the model's class Meta using the permissions field. You will be able to specify as many permissions as you need, but it must be in a tuple like below:
from django.db import models
from django.contrib.auth.models import AbstractUser
from django.db.models.fields.related import ForeignKey
from django.utils.translation import gettext as _
class Role(models.Model):
STUDENT = 1
TEACHER = 2
MANAGER = 3
ROLE_CHOICES = (
(STUDENT, 'student'),
(TEACHER, 'teacher'),
(MANAGER, 'manager'),
)
id = models.PositiveSmallIntegerField(choices=ROLE_CHOICES, primary_key=True)
def __str__(self):
return self.get_id_display()
class User(AbstractUser):
roles = models.ManyToManyField(Role)
username = models.CharField(max_length = 50, blank = True, null = True, unique = True)
email = models.EmailField(_('email address'), unique = True)
native_name = models.CharField(max_length = 5)
phone_no = models.CharField(max_length = 10)
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['username', 'first_name', 'last_name']
def __str__(self):
return "{}".format(self.email)
class Student(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True, related_name='students')
sample_field_name = models.CharField(max_length = 50, blank = True, null = True)
class Meta:
permissions = (("sample_permission", "can change sth of sth"),)
class Teacher(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True, related_name='teachers')
sample_field_name = models.CharField(max_length = 50, blank = True, null = True)
class Meta:
permissions = (("sample_permission", "can change sth in sth"),)
class Manager(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True, related_name='managers')
sample_field_name = models.CharField(max_length = 50, blank = True, null = True)
class Meta:
permissions = (("sample_permission", "can change sth in sth"),)
After that you should have your permissions for your views and Adding permissions to restrict a function to only users that have that particular permission can be done by using a Django built-in decorator, permission_required for function-based views::
from django.contrib.auth.decorators import permission_required
#permission_required('students.sample_permission')
def student_sample_view(request):
"""Raise permission denied exception or redirect user"""
And if you are using a class-based view, you just need to use a mixin, PermissionRequiredMixin:
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.views.generic import ListView
class SampleListView(PermissionRequiredMixin, ListView):
permission_required = 'students.sample_permission'
# Or multiple permissions
permission_required = ('students.sample_permission', 'teachers.other_sample_permission')
This was one way you can manage multiple roles in your Django project,
you can also find more ways in below blogs and references:
How to Implement Multiple User Types with Django
Managing User Permissions in Django
Supporting Multiple Roles Using Django’s User Model
Django Roles, Groups and Permissions Introduction
django-multiple-user-types-example GitHub repository

Related

Need to edit django get request to include list of models given a list of id's

I'm currently creating the backend for a website in which users can add public events/gatherings to their favorites list. I'm currently creating a model in Django which models a user created account named Account. I want to implement it so that Account includes a field called "favorites" which stores the id's of the Events that the specific account added to their favorites.
That favorites table is supposed to be a many to many relationship table especially that you have no additional fields, but what you have done is only link it to the account model, and I m a little bit confused by that favorite field which is just a positive integer? what you should have done instead is
class Account(models.Model):
username = models.CharField(max_length=20, null=True)
.....
favorites = models.ManyToManyField(Event)
this way if you have an account object you can access its favorite events just like this
// create an account
account = new Account(username='foo', firstname....)
account.save()
// create an event
event = new Event(event_name='bar',...)
event.save()
// add an event to an account's favorite events list
account.favorites.add(event)
// access an account's favorite events
account.favorites.all()
You should also be consistent with naming your models and always use singular nouns
Before answering the question, I would like to talk about the models. First, a good practice is to extend or substitute an User model. And, secondly a many-to-many relationship would be more adequate (one user has many favorite events, and one event is favorited by many users).
An example substituting the model, using an app named core:
settings.py
AUTH_USER_MODEL = 'core.Account'
models.py
class Account(AbstractUser):
phone_number = models.CharField(max_length=255, null=True)
favorites = models.ManyToManyField('Event')
class Event(models.Model):
image = models.ImageField(null=True) # will have to figure out where this image is uploaded to
name = models.CharField(max_length=50, null=True)
date = models.DateTimeField(max_length=50, null=True)
location = models.CharField(max_length=50, null=True)
description = models.TextField(null=True)
Now, related to the question. The implementation method will depend on the kind of service, if it is either a plain web service or an API.
For instance, for a plain web service you can build the response yourself using Python lists and dictionaries, in this case a list containing all users with their favorite events:
views.py
from django.forms.models import model_to_dict
from .models import Event, Account
def favorites(request):
data = []
accounts = Account.objects.all()
for account in accounts:
obj = model_to_dict(account, fields=('username', 'first_name', 'last_name', 'email', 'phone_number'))
obj['favorites'] = []
for favorite in account.favorites.all():
obj['favorites'].append(model_to_dict(favorite, fields=('id', 'name', 'data', 'location', 'description')))
data.append(obj)
return HttpResponse(data)
On the other hand, if it is an API service and you are using DRF, then it would be something similar to:
serializers.py
from rest_framework import serializers
from django.contrib.auth import get_user_model
from .models import Event
class EventSerializer(serializers.ModelSerializer):
class Meta:
model = Event
fields = ['id', 'image', 'name', 'date', 'location', 'description']
class AccountSerializer(serializers.ModelSerializer):
favorites = serializers.SerializerMethodField()
class Meta:
model = get_user_model()
fields = ['username', 'first_name', 'last_name', 'email', 'phone_number', 'favorites']
def get_favorites(self, obj):
return EventSerializer(obj.favorites.all(), many=True).data
views.py
from rest_framework import generics
from .serializers import AccountSerializer
from django.contrib.auth import get_user_model
class UserListAPIView(generics.ListAPIView):
queryset = get_user_model().objects.all()
serializer_class = AccountSerializer
Obs.: I only implemented GET requests for both scenarios. You would still have to write POST (for plain service) or have a ListCreateAPIView and attach an event to an account e.g. account.favorites.add(event)

Django: implement multiple user levels / roles / types

I have been using Django for quite a while but never have I thought of this until now.
Currently, I have a project that contains different user levels. Usually, in my past experience, I only developed systems using Django with only two user levels which are superuser and normal/regular user. So my question is what are the effective ways to present these different user levels in the model/database? Here, I'm going to use a school system as an example and also provide some of my initial thoughts on implementing it.
User levels:
Admin (superuser & staff)
Principal
Teacher
Students
Method #1: Add new tables based on each user level
from django.contrib.auth.models import AbstractUser
from django.db import models
class User(AbstractUser):
user = models.CharfieldField(max_length = 10, unique = True)
class Admin(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True)
class Pricipal(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True)
class Teacher(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True)
class Student(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True)
Method #2: Add additional user types attributes in the User model
from django.contrib.auth.models import AbstractUser
from django.db import models
class User(AbstractUser):
user = models.CharfieldField(max_length = 10, unique = True)
is_superuser = models.BooleanField(default = False)
is_staff = models.BooleanField(default = False)
is_principal = models.BooleanField(default = False)
is_teacher = models.BooleanField(default = False)
is_student = models.BooleanField(default = False
'''
User table in DB:
user | is_superuser | is_staff | is_principal | is_teacher | is_student
'''
My thoughts:
In Method #1, as the built-in User model has two fields, is_staff and is_superuser, Is it possible to implement/change the fields into a SuperUser/Admin table as in the example above? This means that when I create an admin/superuser, I want it to add a new row into the Admin table, instead of adding a new user and updating the user's is_superuser and is_staff fields into 1 in the built-in User model.
In Method #2, the problem with it is that tables with different access privileges are directly connected to it. For example, Salary model (which cannot be accessed by Student user) has a direct link with the User model (contains Student user).
I hope I am able to get some insights and also a proper effective way of implementing this so that to prevent any implementation inconvenience and mistakes in the future. Thank you very much.
I think you are in the right path with method #2. It is lighter, and more straightforward.
I would not use a custom "user-like" model for each permission level. Over-complicated, does not scale, and multiply the number of queries, with no very benefit for your problem. Not your UML schema but its content must guarantee your permission requirements.
If the permission levels are not mutual-exclusive :
from django.db import models
from django.contrib.postgres.fields import ArrayField
class User(AbstractUser):
ADMIN = 0
PRINCIPLE = 1
TEACHER = 2
STUDENT = 3
USER_LEVEL_CHOICES = (
(ADMIN, "Admin"),
(PRINCIPLE, "Principle"),
(TEACHER, "Teacher"),
(STUDENT, "Student"),
)
status = ArrayField(
models.IntegerField(choices=USER_LEVEL_CHOICES, blank=True, default=STUDENT),
)
But you need to have a wider reflexion.
I think you are talking about two separate problems : polymorphism, and permissions
Polymorphism :
Polymorphism is the ability of an object to take on many forms. For a Django model, it can be done with many strategies : OneToOneField -as you mentioned- multi-table inheritance, abstract models, or proxy-models.
Very good resources : this article, and Django doc about model inheritance
This very complex problem all refer to : how much your several forms of a same entity are similar, or different. And which operations are particularly similar or different (data shape, querying, permission, ...etc)
Permissions design :
You can choose among several patterns
Model-oriented permission : A user is granted "add", "view", "edit" or "delete" permission to a Model. This is done in Django with the built-in Permission model, that have a ForeignKey to ContentType
Object-oriented permission : A user is granted "add", "view", "edit" or "delete" permission for each Model instance. Some packages provides this ability, django-guardian for example.
Rule-oriented permission : A user is granted permission to a Model instance through custom logic instead of M2M table. The django rules package provide this kind of architecture.
You can create from AbstractUser (a full User model, complete with fields, including is_superuser and is_staff) a Profile and then, once you have the profile, give the chance of users to create other type of profile (Student, Teacher or Principle) which could have functionalities of its own.
For instances, in your models.py
class Profiles(AbstractUser):
date_of_birth = models.DateField(max_length=128, blank=True, null=True, default=None, verbose_name=_(u'Date of birth'))
principle = models.OneToOneField(Principles, null=True, blank=True, verbose_name=_(u'Principles'), on_delete=models.CASCADE)
teacher = models.OneToOneField(Teachers, null=True, blank=True, verbose_name=_(u'Teachers'), on_delete=models.CASCADE)
student = models.OneToOneField(Students, null=True, blank=True, verbose_name=_(u'Students'), on_delete=models.CASCADE)
class Meta:
db_table = 'profiles'
verbose_name = _('Profile')
verbose_name_plural = _('Profiles')
To that model you can add class methods, such as
def is_teacher(self):
if self.teacher:
return True
else:
return False
Then, your Teachers model could look like this
class Teachers(models.Model):
image = models.FileField(upload_to=UploadToPathAndRename(settings.TEACHERS_IMAGES_DIR), blank=True, null=True, verbose_name=_('Teacher logo'))
name = models.CharField(blank=False, null=False, default=None, max_length=255, validators=[MaxLengthValidator(255)], verbose_name=_('Name'))
street = models.CharField( max_length=128, blank=False, null=True, default=None, verbose_name=_('Street'))
created_by = models.ForeignKey('Profiles', null=True, blank=True, on_delete=models.SET_NULL)
One of the methods that I used in several projects is this (pseudo code):
class User(AbstractUser):
ADMIN = 0
PRINCIPLE = 1
TEACHER = 2
STUDENT = 3
USER_LEVEL_CHOICES = (
(ADMIN, "Admin"),
(PRINCIPLE, "Principle"),
(TEACHER, "Teacher"),
(STUDENT, "Student"),
)
user_level = models.IntgerField(choices=USER_LEVEL_CHOICES)
def lvl_decorator():
def check_lvl(func):
def function_wrapper(self, actor, action_on, *args, **kwargs):
if 'action_lvl' not in action_on: # then action_on is user
if actor.user_lvl < action_on.user_lvl:
return True
return False
else: # then action_on is action of some kind for that user (you can add action_lvl to ... and pas them to this wapper)
if actor.user_lvl < action_on.action_lvl:
return True
return False
return function_wrapper
return check_lvl
Then you can write wrapper function with this logic for any action check if action level is bigger than user level e.g.: if someone wants to change superuser password he/she should be logged-in with level-0-user but for changing normal user's password he/she should be level 0, 1. This logic also can be applied to class, functions, etc actions.
Create base class and then add lvl_decorator to it then inherent from it => this keeps your code super clean and prevents further copy paste.
example of what i mean:
def lvl_decorator():
def check_lvl(func):
def function_wrapper(self, actor, action_on, *args, **kwargs):
if 'action_lvl' not in action_on: # then action_on is user
if actor.user_lvl < action_on.user_lvl:
return True
return False
else:
if actor.user_lvl < action_on.action_lvl:
return True
return False
return function_wrapper
return check_lvl
class BaseClass(type):
def __new__(cls, name, bases, local):
for attr in local:
value = local[attr]
if callable(value):
local[attr] = lvl_decorator()
return type.__new__(cls, name, bases, local)
# in other locations like views.py use this sample
class FooViewDjango(object, ApiView): # don't remove object or this won't work, you can use any Django stuff you need to inherent.
__metaclass__ = BaseClass
def baz(self):
print('hora hora')
Use this base class in any where you want.

Assigning current user in rest framework view

I have been getting my head around these basics but I am not getting it right. I am trying to associate my view to my user model using team which is a foreign key. When I try to create of a gps, I get an error saying "team is a required field" but instead it should be read only. The team attribute should be filled automatically with the id of the currentUser
Model
class User(models.Model):
first_name = models.CharField(max_length=200,blank=False)
last_name = models.CharField(max_length=200, blank=False)
class Gps(models.Model):
location = models.CharField(max_length=200,blank=False)
team= models.ForeignKey(User, on_delete=models.CASCADE)
serializers
class GpsSerializer(serializers.ModelSerializer):
class Meta:
model = Gps
fields = ('id','location','team')
view
class Gps_list(generics.ListCreateAPIView):
queryset = Gps.objects.all()
serializer_class = GpsSerializer
team = serializers.PrimaryKeyRelatedField(
read_only=True,
default=serializers.CurrentUserDefault()
)
There are two changes needed. First, team field definition should be moved to serializer class instead of view. Second, you should use Django's contrib.auth.User model instead of your definition of User, as because serializers.CurrentUserDefault() will bring request.user only. So you should remove your User definition and import that to your models.py:
from django.contrib.auth.models import User
Further steps would be to replace read_only=True with queryset=User.objects.all() to allow create.

Using Custom User DB Structure in Mysql for Django 1.8

I have a question regarding the table structure for User if I am extending its functionalities, using a MySQL database.
Given the models.py file
class LibraryUser(models.Model):
user_id = models.OneToOneField(User)
is_catalogue_subscriber = models.IntegerField(default=1)
is_research_subscriber = models.IntegerField(default=1)
library_membership_number = models.CharField(max_length=64)
I have a table structure for SQL
CREATE TABLE library_user(
user_id int(10) primary key
is_catalogue_subscriber integer(1) DEFAULT 1
is_research_subscriber = integer(1) DEFAULT 1
library_membership_number = varchar(16)
)
So now, when I fire up my server and access the accounts in the admin page, Django is throwing an error:
Exception Type: OperationalError
Exception Value:
(1054, "Unknown column 'library_user.id' in 'where clause'")
Use
user = models.OneToOneField(User, primary_key=True)
i.e. drop the _id in the attribute name.
In case you simply want to define a richer user model (i.e. add more attributes) you can
use a one-to-one relationship to a model containing the fields
for additional information. This one-to-one model is often called a
profile model, as it might store non-auth related information about a
site user. For example you might create a LibraryUser model:
from django.contrib.auth.models import User
class LibraryUser(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
is_catalogue_subscriber = models.IntegerField(default=1)
is_research_subscriber = models.IntegerField(default=1)
library_membership_number = models.CharField(max_length=64)
Assuming an existing LibraryUser Fred Smith who has both a User and LibraryUser model, you can access the related information using Django’s standard related model conventions:
>>> u = User.objects.get(username='fsmith')
>>> freds_department = u.libraryuser.department
Then to add the profile model’s fields to the user page in the admin do
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.models import User
from my_user_profile_app.models import LibraryUser
# Define an inline admin descriptor for Employee model
# which acts a bit like a singleton
class LibraryUserInline(admin.StackedInline):
model = LibraryUser
can_delete = False
verbose_name_plural = 'libraryuser'
# Define a new User admin
class UserAdmin(UserAdmin):
inlines = (LibraryUserInline, )
# Re-register UserAdmin
admin.site.unregister(User)
admin.site.register(User, UserAdmin)
All taken from the official docs
What you are missing is that by default django models automatically include an id field that maps to models.AutoField().
You need to specify that your DB table id is user_id instead of id.
https://docs.djangoproject.com/en/1.8/topics/db/models/#automatic-primary-key-fields
You want to make your user_id the primary key. It should work by adding primary_key=True to that field, like:
class LibraryUser(models.Model):
user_id = models.OneToOneField(User, primary_key=True)
is_catalogue_subscriber = models.IntegerField(default=1)
is_research_subscriber = models.IntegerField(default=1)
library_membership_number = models.CharField(max_length=64)

How to handle multiple user types with Django 1.7 user model

I am very new to Python and Django. I am trying to setup user model for different roles like Agents, Brokers, Company and Customer. Each of these roles can register with the site as a user. Then Agents and Brokers can have public facing profile pages.
Do I have to use custom user model or built in user model will work? I have additional properties like license, location, languages, photo etc for Agents and Brokers.
class MyUser(AbstractBaseUser):
AGENTS = 'AG'
BROKERS = 'BR'
COMPANY = 'CP'
CUSTOMER = 'CM'
ROLE_IN_CHOICES = (
(AGENTS, 'Agent'),
(BROKERS, 'Broker'),
(COMPANY, 'Company'),
(CUSTOMER, 'Customer'))
first_name = models.CharField(max_length=100, blank=True)
second_name = models.CharField(max_length=100, blank=True)
middle_name = models.CharField(max_length=100, blank=True)
dob = models.DateField(blank=True, null=True)
phone = models.CharField(max_length=10)
secondary_phone = models.CharField(max_length=10, blank=True, null=True)
......
#property
def is_agent(self):
return self.role in (self.AGENTS)
#property
def is_customer(self):
return self.role in (self.CUSTOMER)
#property
def is_broker(self):*
return self.role in (self.BROKER)
#property
def is_company(self):
return self.role in (self.COMPANY)
....
Can I not use base User model and achieve same? Am I on write track?
How do I create public facing pages for these roles (Agents, Brokers)?
This is my first attempt with Python and Django. I am using Django 1.7.7 with Python 3.4
You should extend from the Django User model instead and add the extra fields you need:
from django.contrib.auth.models import User as Auth_User
class User(Auth_User):
# add your extra fields here like roles, etc
phone = CharField(max_length=20, null=True, blank=True)
# add your extra functions
def extra_user_function(self):
return "This is an extra function"
This way you have your own fields and also the Django User fields...
After migrating, if you check the database, you'll have auth_user and your_app_user tables.
Just bare in mind that request.user will only give you the super fields... In order to get the extended class you'll need
User.objects.get(id=request.user.id)
The latest will only have the extra fields and its id will be the same as the auth.User if you don't add any auth_user by itself.
Attention: this is important!
Otherwise request.user.id and your_app.User.id don't match, therefore User.objects.get(id=request.user.id) won't work and you'll have to query the db to find the your_app.User.id
User.object.get(user_ptr_id = request.user.id)
Other things to consider
This will work:
# you_app.User objects gets vars from auth.User
user = User.objects.get(id=request.user.id)
first_name = user.first_name
But this won't work
# auth.User trying to get a function from your_app.User
user = request.user
user.extra_user_function()
So the User model could be something like this:
import os
from django.contrib.auth.models import User as Django_User
from django.db.models import CharField, ImageField
class User(Django_User):
phone = CharField(max_length=20, null=True, blank=True)
observations = CharField(max_length=2048, null=True, blank=True)
picture = ImageField(upload_to='users', default='default/avatar.jpg')
class Meta:
# adding extra permissions (default are: add_user, change_user, delete_user)
permissions = (
("access_user_list", "Can access user list"),
("access_user", "Can access user"),
)
ordering = ["-is_staff", "first_name"]
Instead of creating roles on the user, Django already has groups, so you should use them.
The groups follow the same logic:
from django.contrib.auth.models import Group as Auth_Group
from django.db import models
class Group(Auth_Group):
observations = models.CharField(max_length=2048, null=True, blank=True)
def get_users_in_group(self):
return self.user_set.filter(is_active=1).order_by('first_name', 'last_name')
def count_users_in_group(self):
return self.user_set.count()
def __unicode__(self):
return self.name
class Meta:
permissions = (
("access_group_list", "Can access group list"),
("access_group", "Can access group"),
)
ordering = ["name"]
You can clear / add users to a group:
user.groups.clear()
user.groups.add(group)
Clear / add permissions to the group:
group.permissions.clear()
group.permissions.add(permission)
There is also a decorator to check if a user has permissions
from django.contrib.auth.decorators import permission_required
#permission_required(("users.change_user","users.access_user",))
def your_view(request):
...
I've tried many things in the past, but I guess this is the way to go.
If you really need roles, and a user can have more than one role, mayb the best thing would be to create a model Role and add that to the user has a ManyToMany Field
roles = ManyToManyField(Role)
but you could do that with groups

Categories

Resources