Flask-admin: differentiate accessability between views - flask

I would like to differentiate accessability to the views (index, create, edit) in flask-admin. It can be done at the level of all views concerning particular model by overriding the method: is_accessible.
def is_accessible(self):
return current_user.is_authenticated # using flask-login
I need that some users will be able to browse data, but without permission to create new records. On the othe rhand other users should be able to create
and edit records. Any help will be appreciated.
Solution
I have overriden _handle_view method which is called before every view.
def _handle_view(self, name, **kwargs):
if not current_user.is_authenticated:
return self.unauthorized_access()
permissions = self.get_permissions(name)
if not current_user.can(permissions):
return self.forbidden_access()
return None #access granted

It isn't terribly well documented, but I think you can override the is_action_allowed method on a ModelView class to get the behavior you want. The API documentation doesn't say much about this, but I found a better example from the changenotes when it was introduced:
You can control which actions are available for current request by
overriding is_action_allowed method:
from flask.ext.admin.actions import action
class MyModelAdmin(ModelAdmin):
def is_action_allowed(self, name):
if name == 'merge' and not user.superadmin:
return False
if name == 'delete' and not user.admin:
return False
return super(MyModelAdmin, self).is_action_allowed(name)
I haven't tried this myself, so I can't attest to whether the example actually works without other changes.

Flask-User seems like what you want. You specify a UserRoles class, a Roles class, and a User class which you already have. You assign a UserRole to a User and then in your is_accessible method can differentiate what you want to do (read/write/edit) based on your role.
https://pythonhosted.org/Flask-User/
# Define User model
class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(50), nullable=True, unique=True)
...
roles = db.relationship('Role', secondary='user_roles',
backref=db.backref('users', lazy='dynamic'))
# Define Role model
class Role(db.Model):
id = db.Column(db.Integer(), primary_key=True)
name = db.Column(db.String(50), unique=True)
# Define UserRoles model
class UserRoles(db.Model):
id = db.Column(db.Integer(), primary_key=True)
user_id = db.Column(db.Integer(), db.ForeignKey('user.id', ondelete='CASCADE'))
role_id = db.Column(db.Integer(), db.ForeignKey('role.id', ondelete='CASCADE'))

Related

Flask Admin One to One Relationship and Edit Form

I am building an admin dashboard for my web app using Flask-Admin. For the user/address relationship, I am using a one to one relationship. On the user edit form, I'd like to be able to edit the individual components of the address (i.e. street address, city or zip) similar to what inline_models provides. Instead, flask-admin generates a select field and only allows me to select a different addresses.
I tried using inline_models = ['address'] in the UserModelView definition. However, I got the address object not iterable error due to the user/address relationship being configured to uselist=False. Switching uselist to True would affect other parts of my code, so I'd prefer to leave it as False.
From looking in flask-admin/contrib/sqla/forms, within the function get_forms, its being assigned a one to many tag which is what drives the use of a select field.
Before diving in further, I figured it best to see if anyone else has come across this or has a recommended fix/workaround.
models.py
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64))
address = db.relationship("Address", backref="user",
cascade="all, delete-orphan", lazy=False,
uselist=False, passive_deletes=True)
class Address(db.Model):
id = db.Column(db.Integer, primary_key=True)
line1 = db.Column(db.String(128))
zip = db.Column(db.String(20), index=True)
city = db.Column(db.String(64), index=True, nullable=False)
user_id = db.Column(db.Integer, db.ForeignKey("user.id",
ondelete="CASCADE"))
admin.py
class UserModelView(ModelView):
column_list = [User.username, 'address']
form_columns = (User.username, 'address')
admin = Admin(name='Ask', template_mode='bootstrap3')
admin.add_view(UserModelView(User, db.session))
You can create 2 relations
# Relation for flask admin inline model
address_cms_relationsip = db.relationship(
"Address", backref="user", cascade="all, delete-orphan", lazy=False,
uselist=True, passive_deletes=True)
address_relationship = db.relationship(
"Address", cascade="all, delete-orphan", lazy=False,
uselist=False, passive_deletes=True)
#property
def address(self):
return self.address_relationship
In your code you can use property address
user: User # some User object
user.address.city

How to let user fetch only those objects they are related to (by many-to-many relationship)?

I have a project in which there are workspaces, and each workspace can have any number of users. Users can also belong to multiple workspaces, so there's a many-to-many relationship between them.
Now, I have workspace creation and membership working, but I'm having trouble setting the permissions so that only the workspace members can see the workspace. I've tried with a custom object-level permission, but it doesn't seem to work.
The workspace model looks like this:
class Workspace(models.Model):
name = models.CharField(max_length=100)
users = models.ManyToManyField(User, related_name='workspaces')
The view looks like this:
class WorkspaceViewSet(viewsets.ModelViewSet):
queryset = Workspace.objects.all().order_by('name')
serializer_class = WorkspaceSerializer
permission_classes = [permissions.IsAuthenticated|BelongsInWorkspace]
The serializer is like this:
class WorkspaceSerializer(serializers.ModelSerializer):
class Meta:
model = Workspace
fields = ('name', 'users')
def create(self, validated_data):
instance = super(WorkspaceSerializer, self)
instance.users.add(self.context['request'].user)
return instance
And finally, the custom permission I'm trying to use here:
class BelongsInWorkspace(BasePermission):
def has_permission(self, request, view):
return True
def has_object_permission(self, request, view, obj):
return obj.users.filter(pk=request.user).exists()
I would highly recommend django-guardian to handle this problem. Django-guardian allows for straightforward, object-level permission management.
To handle your problem, all you would need to do is add a Meta class to your workspace model, where you can create custom permissions.
class Workspace(models.Model):
name = models.CharField(max_length=100)
users = models.ManyToManyField(User, related_name='workspaces')
class Meta:
default_permissions = ('add', 'change', 'delete')
permissions = (
('access_workspace', 'Access workspace'),
)
Assign either a user or a group that permission, attached to a specific workspace.
assign_perm('access_workspace', user, workspace)
#This will return True if the user has been properly assigned the permission
user.has_perm('access_workspace', workspace)
Then to get all workspaces a user has access to, you would just need to call get_objects_for_user() in your view
queryset = get_objects_for_user(self.request.user, 'project.access_workspace')

Django-Guardian/Tastypie: Toggle object-level permissions from private to public

I am using django-guardian to implement object-level permissions in my tastypie api and I would like to be able to toggle an instance of a resource's permissions between private, and public.
For example, I have an Event resource. When a User creates an Event, the Event.creator is set to the User instance. I use a a post_save signal on the Event to give object-level permissions the User who created it - making it private to the creator.
I want a User to be able toggle the permissions level for the Event to allow all users to be able to access it - aka change the Event from private to public (and back again).
What is the best way to handle this? I'd like to stick with guardian if possible, but am not married to it. Here are some snippets of code to help illustrate:
models.py
class UserProfile(models.Model):
user = models.OneToOneField(User, primary_key=True)
events = models.ManyToManyField('myapp.Event', through="myapp.EventMembership", null=True, blank=True)
class Event(models.Model):
name = models.CharField(max_length=64)
creator = models.ForeignKey('myapp.UserProfile', related_name="creator")
resources.py
class EventResource(ModelResource):
class Meta:
queryset = Event.objects.all()
resource_name = 'events'
authentication = ApiKeyAuthentication()
authorization = GuardianAuthorization(
create_permission_code = "myapp.add_event",
view_permission_code = "myapp.view_event",
update_permission_code = "myapp.change_event",
delete_permission_code = "myapp.delete_event"
)
always_return_data = True
creator = fields.ForeignKey('myapp.UserProfile', 'creator', full=True)
def hydrate(self, bundle, **kwargs):
bundle.obj.creator = UserProfile.objects.get(user_id=bundle.request.user)
return bundle
signals.py
#receiver(post_save, sender=User)
def allow_user_to_create_events(sender, instance, **kwargs):
if not instance:
return None
if kwargs.get('created'):
ApiKey.objects.create(user=instance)
# give newly created user permissions to create events
add_permission(instance, "myapp.add_event", Event)
#receiver(post_save, sender=Event)
def allow_user_to_edit_events(sender, instance, *args, **kwargs):
if not instance or not isinstance(instance, Event):
return None
# assign_perm is a guardian shortcut
assign_perm("myapp.view_event", instance.creator.user, instance)
assign_perm("myapp.edit_event", instance.creator.user, instance)
assign_perm("myapp.delete_event", instance.creator.user, instance)
Thanks for your time and help!
Note: I am using the guardian-authorization in my tastypie resources. However, I think my question is more of a guardian question, and not so much about it's use with tastypie.
I don't know how guaridan-authorization works. But in your authorization class you class you can always do a lookup and return True if the object has been set to public.

Restrict access to a Class Based View based on the request user and view to be rendered

Here's the scene:
profiles/models.py
class UserProfile(models.Model):
user = models.OneToOneField(User, primary_key = True)
birthdate = models.DateTimeField(blank=True)
def __unicode__(self):
return unicode(self.user)
class SpecialProfile(models.Model):
user = models.OneToOneField(User, primary_key = True)
...
# additional fields here
def __unicode__(self):
return unicode(self.user)
class SpecialProfileURLs(models.Model):
profile = models.OneToOneField(SpecialProfile, primary_key = True)
... #some more URLs
homepage_url = models.URLField(blank = True)
def __unicode__(self):
return unicode(self.profile)
class SpecialProfileImages(models.Model):
profile = models.OneToOneField(SpecialProfile, primary_key = True)
img1 = models.ImageField(blank = True, upload_to='profiles/')
img2 = models.ImageField(blank = True, upload_to='profiles/')
img3 = models.ImageField(blank = True, upload_to='profiles/')
def __unicode__(self):
return unicode(self.profile)`
profiles/views.py
class PublicProfileView(DetailView):
template_name = "public_profile.html"
model = User
class PrivateProfileView(DetailView):
template_name = 'profile/profile2.html'
context_object_name = "profile"
model = User
pk_field = 'pk'
profiles/urls.py
urlpatterns = patterns("",
url(r'^$', 'mysite.views.home', name='home'), # give me nothing? just take me home!
url(r'^(?P<pk>\d+)/$', PrivateProfileView.as_view(), name="profile"),
url(r'^(?P<username>\w+)/$', ProfileRedirectView.as_view(), name="profile_redirect"),
url(r"^edit/(?P<profile_id>\w+)/$", EditProfileView.as_view(), name="profile_update_form"),
)
Here's the problem:
I want to be able to test whether the user giving the request is the same as the ID used to access a profile. I would intercept at a GET request and check, but django gets mad when I do that (probably because it's a DetailView). Is there a recommended/ non-hackish way to be sure only the user to whom this profile belongs can access it? If not, then the user sending the request should be redirected to the PublicProfileView.
It seems bad form to answer my first question but thankfully Alex helped me find exactly what I was looking for:
Per object permissions.
There is a package on GitHub, django-guardian which allows me to set specific permissions to any User or Group for a single model instance.
The documentation is quite thorough.
Your solution is a combination of some different things:
1) Use the PermissionsRequiredMixin from django-braces. This is an easy way to add Permission functionality to class-based views. Documentation on the PermissionsRequiredMixin can be found here.
2) Create your own permission. The Django documentation on that is here. Include this permission in your class based view.
Here's a related answer on SO that addresses the same problem in a function-based view. You can use it to help create your permission.

django admin add data with fixed value in some field

class Facilites(models.Model):
id = models.CharField(max_length=32, primary_key=True)
name = models.CharField(max_length=128)
class Objects(models.Model):
name = models.CharField(max_length=64)
facilityid = models.ForeignKey(Facilities)
class Admins(models.Model):
user = models.OneToOneField(User)
facilities = models.ManyToManyField(Facilities)
def create_user_profile(sender, instance, created, **kwargs):
if created:
Admins.objects.create(user=instance)
post_save.connect(create_user_profile, sender=User)
What i want is to have users (admins) only be able to add or modify "facilityid" in Objects to values specified in their Admins.facilities.
So if some user is named UserA and has facilities = ('FacA', 'FacB'), when he is adding a new object to DB, he shoudln't be able to add something like Object('Random object', 'FacC')
Also, he shouldn't be able to modify existing objects to facilities he doesn't belong to.
I have filtered the Objects with:
def queryset(self, request):
qs = super(ObjectsAdmin, self).queryset(request)
if request.user.is_superuser:
return qs
return qs.filter(facitityid__id__in = request.user.get_profile().facilities.all())
so users can only see the object that belong to their facilities. But i have no idea how to prevent them from adding/editing object out of their facilities.
edit:
found the answer here: https://stackoverflow.com/a/3048563/1421572
It turns out that ModelAdmin.formfield_for_foreignkey was the right answer in this situation: http://docs.djangoproject.com/en/dev/ref/contrib/admin/#django.contrib.admin.ModelAdmin.formfield_for_foreignkey
I would do this with either a pre-made facility list (i.e. You could create an integer field that is hooked to FACILITY_CHOICES for the user to select from.)
If only admins can do it then permissions sounds quite viable. You can also do form validation to check for errors against the db. Depending on how many facilities you have you may want a different approach.
You can do this same technique with a models.CharField as well. So perhaps assign a 3 letter facility code to each facility and require the entry to match one of the 3 letter strings. You could even have the list in a .txt file to read from. There are really so many ways to do this. I will provide an example of a pre-made facility list and accessing the facility a particular user belongs to from the api / template:
NYC_FACILITY = 0
LA_FACILITY = 1
ATL_FACILITY = 2
FACILITY_CHOICES = (
(NYC_FACILITY, 'NYC'),
(LA_FACILITY, 'LA'),
(ATL_FACILITY, 'ATL'),
class Facility(models.Model):
name = models.IntegerField(choices=FACILITY_CHOICES, default="NYC")
class Meta:
order_by = ['name']
verbose_name_plural = "facilities"
verbose_name = "facility"
def __unicode__(self):
return self.name
As far as viewing the facilities page that a particular user belongs to you will have a m2m one to one or FK relationship between the objects. If FK or m2m relationship then you will have access to additional methods of that model type. get_related However, I'm not going to use get_related in my example. Once you are in an instance you then have access to entry_set.
# models.py
from django.auth import User
class Person(User):
first_name = models.CharField(max_length=50)
last_name = models.CharField(max_length=50)
facility_loc = models.ForeignKey('Facility') # ForeignKey used assuming only one person can belong to a facility.
slug = models.SlugField(unique=True)
def get_absolute_url(self):
return "/%s/%s/" % self.facility_loc % self.slug
# views.py - TemplateView is automatically given a context variable called params which parses data from the URL. So, I'll leave the regex in the URLConf up to you.
class UserFacilityView(TemplateView):
model = Facility
template_name = "user_facility.html"
Now in your template you should be able to access facility_set from a User instance or user_set from a facility instance.