I am trying to add object level permission to my django REST project using django-guardian, but I am getting
http://127.0.0.1:8000/api/v1/tasks/
HTTP 403 Forbidden
Allow: GET, POST, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept
{
"detail": "You do not have permission to perform this action."
}
The user joe is logged in.
settings.py:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.sites',
'guardian',
'rest_framework',
'rest_framework.authtoken',
'rest_auth',
'task.apps.TaskConfig',
]
models.py:
class Task(models.Model):
summary = models.CharField(max_length=32)
content = models.TextField()
reported_by = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
permissions = (
('view_task', 'View task'),
)
serializers.py:
class TaskSerializer(serializers.ModelSerializer):
class Meta:
model = Task
fields = '__all__'
permissions.py:
class CustomObjectPermissions(permissions.DjangoObjectPermissions):
perms_map = {
'GET': ['%(app_label)s.view_%(model_name)s'],
'OPTIONS': ['%(app_label)s.view_%(model_name)s'],
'HEAD': ['%(app_label)s.view_%(model_name)s'],
'POST': ['%(app_label)s.add_%(model_name)s'],
'PUT': ['%(app_label)s.change_%(model_name)s'],
'PATCH': ['%(app_label)s.change_%(model_name)s'],
'DELETE': ['%(app_label)s.delete_%(model_name)s'],
}
filters.py:
class DjangoObjectPermissionsFilter(BaseFilterBackend):
perm_format = '%(app_label)s.view_%(model_name)s'
shortcut_kwargs = {
'accept_global_perms': False,
}
def __init__(self):
assert 'guardian' in settings.INSTALLED_APPS, (
'Using DjangoObjectPermissionsFilter, '
'but django-guardian is not installed.')
def filter_queryset(self, request, queryset, view):
from guardian.shortcuts import get_objects_for_user
user = request.user
permission = self.perm_format % {
'app_label': queryset.model._meta.app_label,
'model_name': queryset.model._meta.model_name,
}
return get_objects_for_user(
user, permission, queryset,
**self.shortcut_kwargs)
views.py:
class TaskViewSet(viewsets.ModelViewSet):
queryset = Task.objects.all()
serializer_class = TaskSerializer
permission_classes = (CustomObjectPermissions,)
filter_backends = (DjangoObjectPermissionsFilter,)
urls.py:
router = DefaultRouter()
router.register('tasks', TaskViewSet, base_name='tasks')
urlpatterns = router.urls
But it works fine in shell
> python manage.py shell -i ipython
In [1]: from django.contrib.auth.models import User
In [2]: joe = User.objects.all().filter(username="joe")[0]
In [3]: import task.models as task_models
In [4]: task = task_models.Task.objects.all()[0]
In [5]: joe.has_perm('view_task', task)
Out[5]: True
The API first checks model-level permissions, then object-level permissions if they apply. Since the custom permissions class requires the user to have read-permissions, you need to ensure that Joe has been assigned model-level read access. If you check joe.has_perm('tasks.view_task'), I would bet that it returns False. To fix this, you either need to directly assign his user the permission, or add him to a group that has been assigned the appropriate permissions.
Also, note that Django 2.1 recently added the "view" permission, and it shouldn't be necessary to add it to your models anymore.
Related
I am trying to create a multi tenant app (with shared database and isolated schema) using this Django-Tenants package.
And I've followed the example videos to setup a demo: video1 and video2
app clients (models.py)
from django.db import models
from django_tenants.models import TenantMixin, DomainMixin
class Client(TenantMixin):
name = models.CharField("Customer name", max_length=100)
created_on = models.DateField(auto_now_add=True)
# default true, schema will be automatically created and synced when it is saved
auto_create_schema = True
class Domain(DomainMixin):
pass
app sweet_tenant (models.py)
from django.db import models
from applications.sweet_shared.models import SweetType
class Sweet(models.Model):
sweet_type = models.ForeignKey(SweetType, on_delete=models.CASCADE)
name = models.CharField(max_length=50)
price = models.DecimalField(default=0, decimal_places=3, max_digits=8)
def __str__(self):
return self.name
app sweet_shared (models.py)
from django.db import models
class SweetType(models.Model):
name = models.CharField(max_length=20)
def __str__(self):
return self.name
settings
# Application definition
SHARED_APPS = [
"django_tenants", # mandatory
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# shared apps
"applications.clients",
"applications.sweet_shared",
]
TENANT_APPS = (
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# your tenant-specific apps
"applications.sweet_tenant",
)
INSTALLED_APPS = SHARED_APPS + [app for app in TENANT_APPS if app not in SHARED_APPS]
BEHAVIOUR
Public schema are shared with all tenants. Any tenant can see any data form public schema if you don't filter. This is a normal behavior
Tenants (clients) are created in the public schema.
NEEDS
I dont want that these data (tenants) can be seen by any tenant, because it will contain private client data. Each tenant only needs to see his data and only the SaaS owner can see all data (superuser in public schema).
PROBLEM
I have added this code (see code below) into the admin.py file to filter tenant's data. This works fine. But the problem is that I have not been able to detect when it is not a tenant and is the superuser of the public schema. Or is there a better way to achieve it?
from django.contrib import admin
from django_tenants.admin import TenantAdminMixin
from django.db import connection
from .models import Client
class ClientAdmin(TenantAdminMixin, admin.ModelAdmin):
list_display = (
"name",
"schema_name",
"created_on",
)
def get_readonly_fields(self, request, obj=None):
if obj: # editing an existing object
return self.readonly_fields + ('schema_name', 'created_on')
return self.readonly_fields
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.filter(name=connection.tenant)
admin.site.register(Client, ClientAdmin)
You can add the following Mixin to the ClientAdmin to only allow public tenants to see your clients and domains data.
from django.contrib import admin
from django_tenants.admin import TenantAdminMixin
from django_tenants.utils import get_public_schema_name
from backend.customers.models import Client, Domain
class PublicTenantOnlyMixin:
"""Allow Access to Public Tenant Only."""
def _only_public_tenant_access(self, request):
return True if request.tenant.schema_name == get_public_schema_name() else False
def has_view_permission(self,request, view=None):
return self._only_public_tenant_access( request)
def has_add_permission(self,request, view=None):
return self._only_public_tenant_access(request)
def has_change_permission(self,request, view=None):
return self._only_public_tenant_access( request)
def has_delete_permission(self,request, view=None):
return self._only_public_tenant_access( request)
def has_view_or_change_permission(self,request, view=None):
return self._only_public_tenant_access( request)
#admin.register(Client)
class ClientAdmin(PublicTenantOnlyMixin,TenantAdminMixin, admin.ModelAdmin):
list_display = ('name', 'paid_until')
#admin.register(Domain)
class DomainAdmin(PublicTenantOnlyMixin,TenantAdminMixin, admin.ModelAdmin):
list_display = ('domain',)
I'm creating API for Twitter like app in Django and since I implemented token authentication (rest-auth) I have problem with creating new tweets as it throws:
{
"author": [
"This field is required."
]
}
I've tried CreateAPIView:
class TweetCreateAPIView(CreateAPIView):
serializer_class = TweetModelSerializer
permission_classes = (IsAuthenticated,)
# I've also tried to add the author field mannualy
def perform_create(self, serializer):
serializer.save(author=self.request.user)
but it didn't work so I created custom post method:
class TweetCreateAPIView(APIView):
permission_classes = (IsAuthenticated,)
def post(self, request, format=None):
serializer = TweetModelSerializer(data=request.data)
if serializer.is_valid():
serializer.save(author=request.user)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
but it still can't identify the user creating the tweet
Model:
class Tweet(models.Model):
retweeted_by = models.ManyToManyField(
TwitterUser, blank=True, related_name='retweets')
commented_tweet = models.ManyToManyField(
'self', related_name='comments', blank=True, symmetrical=False)
author = models.ForeignKey(
TwitterUser, on_delete=models.CASCADE, related_name='tweets')
content = models.CharField(max_length=280)
created_at = models.DateTimeField(auto_now_add=True)
liked_by = models.ManyToManyField(
TwitterUser, blank=True, related_name='likes')
objects = TweetManager()
def __str__(self):
return self.content
def get_comments(self):
comments = Tweet.objects.filter(commented_tweet__pk=self.pk)
return comments
def get_retweets(self):
retweets = TwitterUser.retweets(tweet__id=self.id)
def get_absolute_url(self):
return reverse('tweets:pk', kwargs={'pk': self.pk})
Serializer:
class TweetModelSerializer(serializers.ModelSerializer):
likes_count = serializers.SerializerMethodField()
already_liked = serializers.SerializerMethodField()
def get_likes_count(self, tweet):
return tweet.liked_by.all().count()
def get_already_liked(self, tweet):
user = None
request = self.context.get("request")
if request and hasattr(request, "user"):
user = request.user
if user is not None:
if user in tweet.liked_by.all():
return True
else:
return False
else:
pass
class Meta:
model = Tweet
fields = [
'id',
'author',
'commented_tweet',
'content',
'retweeted_by',
'likes_count',
'already_liked',
'created_at',
]
Settings:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# 3rd party
'rest_framework',
'rest_framework.authtoken',
'rest_auth',
'django.contrib.sites',
'channels',
'allauth',
'allauth.account',
'rest_auth.registration',
'reset_migrations',
'corsheaders',
]
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly'
],
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.TokenAuthentication',
),
'DEFAULT_PAGINATION_CLASS': 'twitter.paginations.StandardResultsSetPagination',
'TEST_REQUEST_DEFAULT_FORMAT': 'json',
}
Authentication itself works fine, including registration, so I don't think that it's a problem with tokens, yet I have no other idea where the bug is when even CreateAPIView doesn't work.
In your serializer you have author in the list of fields. Since it's a ModelSerializer, drf is picking up details for that field from the model, and in the model author is null = False by default, hence drf is making it a required field, since it can't be null. But since you want the author to be automatically the user who is making the request, you don't need that field to be editable. Hence make it a read only field in your serializer like this:
class TweetModelSerializer(serializers.ModelSerializer):
...
class Meta:
model = Tweet
fields = ('id', 'author', 'commented_tweet', 'content', 'retweeted_by',
'likes_count', 'already_liked', 'created_at', )
read_only_fields = ('author', ) # it's read only now, so drf go looking for it in POST data
Now you just need to override the perform_create method, no need to change the post method.
If you need even more customizations like superuser can edit authors but no one else, you can override the __init__ method your serializer and make fields read_only based on request.user which gets a bit complicated.
Also, you should consider using tuple instead of list for fields and read_only_fields to get minor performance boost.
I'm trying to customize auth backend while customized auth model but keep facing this error because i'm using get_user_model() function.
django.core.exceptions.ImproperlyConfigured: AUTH_USER_MODEL refers to model 'base.User' that has not been installed
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'base.apps.BaseConfig',
'core.apps.AccountsConfig',
'system.apps.SystemConfig',
]
custom Backend:
class UserBackend(object):
def authenticate(self, request, username=None, password=None, **kwargs):
usermodel = User
try:
usr = usermodel.objects.get(username=username)
password_valid = usr.check_password(password)
if usr and password_valid:
return usr
raise PermissionDenied
except usermodel.DoesNotExist:
return PermissionDenied
return None
def get_user(self, user_id):
usermodel = User
try:
return usermodel.objects.get(pk=user_id)
except usermodel.DoesNotExist:
return None
Edit:
settings:
AUTH_USER_MODEL = 'base.User'
AUTHENTICATION_BACKENDS = (
'base.models.UserBackend',
)
base.User model:
class User(AbstractUser):
fullname = models.CharField(max_length=35, null=True, blank=True)
picture = models.ManyToManyField('ImageFile', verbose_name="ProfilePicture", blank=True)
bio = models.CharField(max_length=255, null=True, blank=True)
link = models.URLField(null=True, blank=True, default="")
is_private = models.BooleanField(default=False)
is_official = models.BooleanField(default=False)
Note: UserBackend is on the end of file and class User(AbstractUser) is above it
There was an import in base.models file, from django.contrib.auth.backends import ModelBackend which caused this error even when i removed custom AUTHENTICATION_BACKENDS.after i removed this import, everything works fine although i moved backend class from base.models to backend file in the base app (i think its not necessary, i just did it for more readable codes)
For me it was the same. It took me over an hour to find out that you cannot have the CustomBackend(BaseBackend) and the CustomUser(AbstractUser) in the models.py of your app. This info is nowhere to be found in the official Django docs.
Django Version: 3.1.2
Python Version: 3.8.2
models.py of the app "Users":
from django.db import models
from django.contrib.auth.models import AbstractUser
# Create your models here.
class User(AbstractUser):
last_name = models.CharField(max_length=50)
first_name = models.CharField(max_length=50)
auth.py (arbitrary name, living in the "Users" app):
from django.db import models
from django.contrib.auth.backends import BaseBackend
class UserAuth(BaseBackend):
def authenticate(self, request, username, password):
pass
def get_user(self, user_id):
pass
settings.py:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'users.apps.UsersConfig'
]
AUTH_USER_MODEL = 'users.User'
AUTHENTICATION_BACKENDS = [
'users.auth.UserAuth'
]
I've installed django-registration-redux and custom_user. I have a UserProfile model with fullname,dob and photo as extra fields.
I have connected the UserProfile with user-registration so as to save the additional fields to UserProfile table.
Here my questions are...
How do I validate fields in UserProfile model at the time of registration so that I can prevent addition of a user to the emailuser table. Say, if the dob given is invalid or not in allowed range, then stop adding a user to emailuser table.
How can I make an EditProfile system that allows authenticated users to edit their profile (/user/edit - id to be obtained from session).
Below is my settings.py file
INSTALLED_APPS = [
'django.contrib.admin',
'custom_user',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
#------------
'registration',
'userprofile',
]
AUTH_USER_MODEL = 'custom_user.EmailUser'
models.py
class UserProfile(models.Model):
user=models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE,
primary_key=True,)
fullname=models.CharField(max_length=70, blank=False)
dob=models.DateField(blank=False)
photo=models.ImageField(upload_to='profile_images', blank=False)
def __str__(self):
return self.user.email
def user_registered_callback(sender, user, request, **kwargs):
profile = UserProfile(user = user)
profile.fullname =request.POST["fullname"]
profile.dob ="%s-%s-%s" % (request.POST["dob_year"],request.POST["dob_month"],request.POST["dob_day"])
if 'photo' in request.FILES:
profile.photo = request.FILES['photo']
profile.save()
user_registered.connect(user_registered_callback)
forms.py
class UserProfileForm(RegistrationForm):
fullname=forms.CharField(required=True,label="Full name", min_length=3, max_length=75)
dob=forms.DateField(required=True,label="Date of birth",
widget=forms.SelectDateWidget(years=range(now.year-settings.AGE_UPPER,now.year-settings.AGE_LOWER)))
photo=forms.ImageField(required=False)
views.py
def profile(request):
if not request.user.is_authenticated():
return HttpResponseRedirect('/user/login')
else:
return render(request, 'userprofile/profile.html', {"user":request.user})
class EditProfile(UpdateView):
template_name = 'userprofile/edit_profile.html'
fields = ['fullname','dob','photo']
main urls.py
urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'user/register/$',
RegistrationView.as_view(form_class = UserProfileForm),
name = 'registration_register'),
url(r'^user/', include('registration.backends.default.urls')),
url(r'^user/', include('userprofile.urls')),
userprofile.urls.py
urlpatterns = [
url(r'^profile/$', views.profile, name='profile'),
url(r'^profile/edit/$', views.EditProfile.as_view(), name='edit_profile'),
]
Right now I'm, getting EditProfile is missing a QuerySet. Define EditProfile.model, EditProfile.queryset, or override EditProfile.get_queryset().
Is it the right way to proceed? Any help would be appreciated.
Thanks
I'm a Python/Django noob. So any help will be appreciated.
Trying to use the django-fluent-contents
models.py
from django.core.urlresolvers import reverse
from django.db import models
from fluent_contents.models.fields import PlaceholderField, PlaceholderRelation, ContentItemRelation
from fluent_contents.models import ContentItem
class Article(models.Model):
title = models.CharField("Title", max_length=200)
slug = models.SlugField("Slug", unique=True)
content = PlaceholderField("article_content")
placeholder_set = PlaceholderRelation()
contentitem_set = ContentItemRelation()
class Meta:
verbose_name = "Article"
verbose_name_plural = "Articles"
def __unicode__(self):
return self.title
def get_absolute_url(self):
return reverse('article-details', kwargs={'slug': self.slug})
admin.py
from django.contrib import admin
from article.models import Article
from fluent_contents.admin import PlaceholderFieldAdmin
class ArticleAdmin(PlaceholderFieldAdmin):
prepopulated_fields = {'slug': ('title',)}
fieldsets = (
(None, {
'fields': ('title', 'slug', ),
}),
("Contents", {
'fields': ('content',),
'classes': ('plugin-holder',),
})
)
admin.site.register(Article, ArticleAdmin)
I'm using South for migration.
db.create_table(u'article_article', (
(u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('title', self.gf('django.db.models.fields.CharField')(max_length=200)),
('slug', self.gf('django.db.models.fields.SlugField')(unique=True, max_length=50)),
))
db.send_create_signal(u'article', ['Article'])
It looks like, no column is being created for the 'content' field.
So when I try to add a new 'Article' via django admin —
FieldError at /manage/article/article/add/
Unknown field(s) (content) specified for Article. Check fields/fieldsets/exclude attributes of class ArticleAdmin.
If I remove the fieldset from admin.py
class ArticleAdmin(PlaceholderFieldAdmin):
prepopulated_fields = {'slug': ('title',)}
admin.site.register(Article, ArticleAdmin)
The 'content' field is not shown in django admin
In reply to #vdboor.. Here's my installed apps ...
INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.admin',
# Uncomment the next line to enable admin documentation:
# 'django.contrib.admindocs',
# 3rd party apps
'south',
'django_extensions',
'compressor',
'debug_toolbar',
'fluent_contents',
'fluent_contents.plugins.text',
'django_wysiwyg',
# Project specific apps go here
'article',
)
Also I'm using the example app from the repo as a guide... just removed the extra plugin model
FYI I'm using
Django==1.6
MySQL-python==1.2.4
South==0.8.4
Thank you for all the help :-)
It looks like, no column is being created for the 'content' field.
That is correct, the PlaceholderField becomes a reverse-generic-relation.
You can try removing the fieldsets declaration for now, and see what other error you get.
The repository also contains an example application, which you can run, and compare with your app.
Silly question, but is fluent_contents in the INSTALLED_APPS?
Still need to add extra fields. In the example shown, because the content is stored in a separate table - model ContentItem.
from fluent_contents.models.fields import PlaceholderField, PlaceholderRelation, ContentItemRelation
class Article(models.Model):
title = models.CharField("Title", max_length=200)
slug = models.SlugField("Slug", unique=True)
content = PlaceholderField("article_content")
placeholder_set = PlaceholderRelation() # <-- this
contentitem_set = ContentItemRelation() # <-- and this
class Meta:
verbose_name = "Article"
verbose_name_plural = "Articles"