Flask-Admin permissions for adding user with specific role - python

I use Flask-Admin and Flask-Security to implement admin panel for my Python application. Here are the models for User and for Role:
class Role(db.Model, RoleMixin):
id = db.Column(db.Integer(), primary_key=True)
name = db.Column(db.String(80), unique=True)
description = db.Column(db.String(255))
def __str__(self):
return self.name
class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(255), unique=True, index=True)
password = db.Column(db.String(255))
roles = db.relationship('Role', secondary=roles_users,
backref=db.backref('users', lazy='dynamic'))
def __str__(self):
return self.username
I created a sqla.ModelView for User available only for users with roles "admin" and "superadmin", so such users can create new users using this view. How can I make only users with role "superadmin" can create users with roles "superadmin" and "admin"? In other words I want to specify which of the variants are available for users with different roles: https://imgur.com/a/GdhUS2Y.

To do that you will have to customize a few things.
I would suggest the following:
add this method in your User model that checks if a user has a certain role:
def has_role(self, role):
# db is your database session.
query = db.query(Role).filter(Role.name == role).first()
if query:
if query.name in self.roles:
return True
return False
Now you have to customize the roles that are being displayed when a user tries to create a new user. To achieve that, you will need to change the definition of the roles field. To achieve that, you will need to override the flask-admin scaffold_form() method.
from flask_admin.contrib.sqla.fields import QuerySelectMultipleField
from flask_security import current_user
from flask_admin.form import Select2Widget
def scaffold_form(self):
form_class = super(YourUserAdminClass, self).scaffold_form()
role_filter = Role.name == Role.name if current_user.has_role('superadmin') else Role.name.notin_('superadmin', 'admin')
form_class.roles = QuerySelectMultipleField(
query_factory=lambda: self.session.query(Role).filter(role_filter),
allow_blank,
blank_text='Select Role',
widget=Select2Widget(multiple=False) # change this to True if you allow multiple roles
)
The code above will render the dropdown with the roles depending on the role that the current_user have (using the has_role() method in the filter

Although the answer by #shifloni provides a concise idea of what's need to be done, as per what I have observed, I think it's incomplete. It's because of the fact that
if current_user.has_role('superadmin') throws error during app initialization as current_user is available only when the code is executed in a request context. This is what worked for me:
class ViewForCreatingAdminAndOperators(sqla.ModelView):
def _run_view(self, fn, *args, **kwargs):
if current_user.has_role('admin'):
self.role_filter = Role.name.in_(['operator'])
else:
self.role_filter = Role.name.in_(['superadmin', 'admin', 'operator'])
return fn(self, *args, **kwargs)
def scaffold_form(self):
form = super(ViewForCreatingAdminUsers, self).scaffold_form()
form.roles = QuerySelectMultipleField(
query_factory=lambda: self.session.query(Role).filter(self.role_filter),
widget=Select2Widget(multiple=False) # change this to True if you allow multiple roles
)
return form
_run_view func is always called in a request context, so whenever we click to create a form, that func is called and we have the role_filter available.
P.S. Use case as per above code was that admin should be able to create only operator.

Related

How can I use flask_login with multiple user classes and different database entries?

If someone were to have a flask web app with several "types" of user, for example in a school setting there may be "teacher", "student" and "admin". Each user needs to have a way to log in to the app, but Flask-Login only provides one #login.user_loader. My question is similar to this question, about multiple user classes, but the difference is that each type of user does not have the same model structure as the others. For example, if one were to use flask-sqlalchemy and declared the following models:
class Teacher(db.Model, UserMixin):
id = db.Column(db.Integer,primary_key=True)
title = db.Column(db.String(32))
pwd_hash = db.Column(db.String(128))
first_name = db.Column(db.String(128))
last_name = db.Column(db.String(128))
students = db.relationship('Student',backref='teacher',lazy='dynamic') #because a teacher will teach multiple students
class Student(db.Model, UserMixin):
id = db.Column(db.Integer,primary_key=True)
username = db.Column(db.String(64),unique=True)
teacher_id = db.Column(db.ForeignKey('teacher.id'),db.Integer) #for the ease of things, the student is only ever taught by one teacher
class Admin(db.Model, UserMixin):
email = db.Column(db.String(128),unique=True)
name = db.Column(db.String(128))
The user_loader is normally used like this, with the User class here being a generic user class:
#login.user_loader
def load_user(id):
return User.query.get(int(id))
This means that you have to have just one User class that can be logged in. The normal approach would be to condense the Student, Teacher and Admin class into one User class, with a role field in the model which tells you what level of access they are. But this brings up multiple problems. For example, not all students have an email at their disposal yet the administrative account for the school needs one. Similarly, the relationship between students and teachers would fail because there is no model for the teachers to have students in their class etc.
So how can I implement a way of there being multiple user models with different attributes and model fields that can each be logged in separately with flask_login?
Your User needs only an id and a relationship to the tables which that user corresponds to. You can create this by creating a table for roles and roles-relationships:
class User(db.Model, UserMixin):
""""""
__tablename__ = "user"
# Core
id = db.Column(db.Integer,primary_key=True)
roles = db.relationship('Role', secondary=roles_users,
backref=db.backref('users', lazy='dynamic'))
class Role(db.Model, RoleMixin):
__tablename__ = "role"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=True)
roles_users = db.Table('roles_users',
db.Column('user_id', db.Integer(), db.ForeignKey('user.id')),
db.Column('role_id', db.Integer(), db.ForeignKey('role.id')))
You can check for a given role using the following:
class User(db.Model, UserMixin):
# all the args go here...
...
def has_role(self, role_name):
"""Does this user have this permission?"""
my_role = Role.query.filter_by(name=role_name).first()
if my_role in self.roles:
return True
else:
return False
Next, create a decorator which checks for a role
from flask import redirect
from flask_login import current_user
from functools import wraps
def require_role(role):
"""make sure user has this role"""
def decorator(func):
#wraps(func)
def wrapped_function(*args, **kwargs):
if not current_user.has_role(role):
return redirect("/")
else:
return func(*args, **kwargs)
return wrapped_function
return decorator
Step 3: Apply the decorator in operations:
#app.route("/")
#login_required
#require_role(role="teacher")
def teachers():
"""Only allow teachers"""
# Otherwise, proceed
return render_template('teachers.html')

Flask Admin edit able user change their data

I'm using Flask-Admin to manage my CRUD.
There are three roles in my app, which is superuser, operator and client.
In this app, operators must ask superuser to register their account, to change their data and others.
But for the client which is uncounted numbers, I want they can register their account or editable their account information by own.
For now, the client has can register by own, but now I want the client can editable their information individually without through superuser.
So far, I just can edit the account information by superuser, like this screenshot:
So for now, I want client can edit their name, email, password or other information by their own, but also separate their data with the other clients.
Here is the snippet of my model:
roles_users = db.Table(
'roles_users',
db.Column('user_id', db.Integer(), db.ForeignKey('user.id')),
db.Column('role_id', db.Integer(), db.ForeignKey('role.id'))
)
class Role(db.Model, RoleMixin):
id = db.Column(db.Integer(), primary_key=True)
name = db.Column(db.String(80), unique=True)
class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(255), unique=True)
password = db.Column(db.String(255))
roles = db.relationship('Role', secondary=roles_users,
backref=db.backref('users', lazy='dynamic'))
class Operator(User):
__tablename__ = 'operator'
id = db.Column(db.Integer(), primary_key=True)
user_id = db.Column(db.Integer(), db.ForeignKey('user.id'))
class Client(User):
__tablename__ = 'client'
id = db.Column(db.Integer(), primary_key=True)
user_id = db.Column(db.Integer(), db.ForeignKey('user.id'))
So, how to do that with Flask-Admin..?
Flask-Security comes with a built-in form and view for password change. I would recommend using that. https://pythonhosted.org/Flask-Security/customizing.html
to edit user info via Flask-Admin view, you can override these methods by doing the following. Don't forget to add 'client' as accepted role in your flask-admin User class.
The custom filter has to filter on current_user_id, so no other user profile can be editted.
def get_query(self)
if "superuser" in current_user.roles:
return self.session.query(self.model) # as original source code
else: # for all other roles
return self.session.query(self.model).filter(
< insert custom filter here> )
def get_count_query(self):
if "superuser" in current_user.roles:
return self.session.query(func.count('*')).select_from(self.model) # as original source code
else: # for all other roles
return self.session.query(func.count('*')).filter(
<insert custom filter here> )
An alternative solution would be so build a custom view (without using flask-admin) and call it /myprofile.

Assign default user role when user registers to website with flask-user

I'm trying to assign a default user role to newly registered accounts. I'm using the flask-user plugin(http://flask-user.readthedocs.io/en/latest/index.html). In the flask-user documentation they use this code to manually assign a user role to a new account:
if not User.query.filter(User.email == 'admin#example.com').first():
user = User(
email='admin#example.com',
email_confirmed_at=datetime.datetime.utcnow(),
password=user_manager.hash_password('Password1'),
)
user.roles.append(Role(name='Admin'))
user.roles.append(Role(name='Agent'))
db.session.add(user)
db.session.commit()
But how can this be done automatically when the user registers?
Use signals. user_registered signal is emitted after successful user registration. Its subscriber can do every necessary initialization:
from flask_user import user_registered
#user_registered.connect_via(app)
def _after_user_registered_hook(sender, user, user_invite, **extra):
role = Role.query.filter_by(name='Agent').one()
user.roles.append(role)
app.user_manager.db_adapter.commit()
The easiest way to assign a default role is to modify the UserRoles class, adding a default value to the role_id colum.
class UserRoles(db.Model):
__tablename__ = 'user_roles'
id = db.Column(
db.Integer(),
primary_key=True
)
user_id = db.Column(
db.Integer(),
db.ForeignKey('users.id', ondelete='CASCADE')
)
role_id = db.Column(
db.Integer(),
db.ForeignKey('roles.id',
ondelete='CASCADE'),
default=1
)
The original class from the docs here.
According to the newest flask-user docs you can use this. It helped me solve the problem. the import is .. important.
from flask_user.signals import user_registered
#user_registered.connect_via(app)
def _after_registration_hook(sender, user, **extra):
print('user is registered', sender, user)

flask admin: How to make a query according to the current user's id and role?

I used the classes below, allowing users to edit their own profile after registering and it works well. I achieve this using get_query and get_count_query.
However, if the current user is administrator, how can I customize it to let him/her view all users' profiles instead of just his/her own profile? Thanks in advance.
from flask_admin.contrib.sqla.view import ModelView, func
class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
first_name = db.Column(db.String(255))
last_name = db.Column(db.String(255))
email = db.Column(db.String(255), unique=True)
password = db.Column(db.String(255))
def __str__(self):
return self.email
class UserView(ModelView):
"""
Restrict only the current user can see his/her own profile
"""
def get_query(self):
return self.session.query(self.model).filter(self.model.id==current_user.id)
def get_count_query(self):
return self.session.query(func.count('*')).filter(self.model.id==current_user.id)
You can define another custom ModelView for administrator. For example:
class UserViewForAdmin(ModelView):
def is_accessible(self):
return current_user.has_role("admin")
def inaccessible_callback(self, name, **kwargs):
return redirect(url_for("security.login", next=request.url))
pass
admin = Admin(name="Flask-Admin Example")
admin.add_view(UserView(User, db.session, name="Profile")
admin.add_view(UserViewForAdmin(User, db.session, name="UserList", endpoint="users")
The is example assume you use Flask-Security to do user management.
Another option here is to check the user and handle accordingly in y our get_query or get_count_query such as:
def get_query(self):
if current_user.username == 'admin':
return self.session.query(self.model)
else:
return self.session.query(self.model).filter(
# your existing filter for user
)
Note: the above uses current_user from flask_security but can be adapted to other logic easily.

Entity Relational Model Design and Query Method

Current Design:
Users and Groups: Many-to-Many Table
Tools: SQLAlchemy 1.0.9, Python 2.7, Pyramid
The two questions I pose to you:
I am uncertain if I should have the group attribute under User or the user attribute under Group. This is a MANY-TO-MANY relationship; with a third table that relates the two different classes.
SQLAlchemy has a lot of great examples discussing .joins, but I'm finding the task is difficult with this DB design for querying based on the selected username. (see below)
I have designed a SQLAlchemy database and am trying to implement best practices for retrieving data from my table while eliminating redundancy. With that said, I also want this to be an effective design so that when I build a group_finder function using Pyramid's Authorization and Authentication System, so that I have no issue.
I am using a CRUD methodology. As of right now: To add a user to a Group, I update_group_add_user(...) to add a user or update_group_remove_users(...) to remove a user.
base.py
Commented out groups and added users to class Group
association_table = Table('group_user_link', Base.metadata,
Column('group_id', Integer, ForeignKey('groups.id')),
Column('user_id', Integer, ForeignKey('users.id')))
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
username = Column(String(15), nullable=False, unique=True)
email = Column(String(300))
password = Column(String(300), nullable=False)
#groups = relationship('Group', secondary='group_user_link', backref='users')
def __init__(self, username, password, firstname, lastname, email):
self.username = username
self.password = hashlib.sha224(password).hexdigest()
self.email = email
#self._groups = groups
def __repr__(self): # code
class Group(Base):
__tablename__ = 'groups'
id = Column(Integer, primary_key=True)
group_name = Column(String(50), unique=True)
users = relationship('User', secondary='group_user_link', backref='groups')
def __init__(self, group_name, user=None):
if user is None: user = []
self.group_name = group_name
self._user = user # to group_add_user and group_remove_user
def __repr__(self): # code
Query method (using CRUD):
This prints out ALL the relationships in tuples in a list. I want to only print out (and return) only the user being entered.
def retrieve_user_bygroup(self, username):
query= self.session.query(User, Group).join(association).\
filter(User.id == Group.id).\
order_by(User.id).all()
print "retrieve user by group:", query
return query
I discovered two similarly asked questions that clarified the use of backref, .join, and .query in SQLAlchemy. This helped clarify how the backref line in users = relationship('User', secondary='group_user_link', backref='groups') made groups accessible through User when later querying with a .join and .filter (shown at the bottom).
Discussing the purpose of backref:
backref means a 'poster' attribute will be added to UserPost. Actually
I'd probably call that 'user'. But the point is that 'relation()'
knows how to join between those two tables because of the previously
defined ForeignKey.
Discussing .join and .query:
.join [works] according [to] the relations, and yields 3-tuples. The arguments to
the query() call are essentially the select list in sqlalchemy.
I also found this Stacks answer to be helpful with using .any()
def retrieve_group_byuser(self, username):
# retrieves group by username
query = self.session.query(Group).\
filter(Group.users.any(User.username == username)).all()
print "retrieved by username:", query
return query
And I removed groups from User which now looks like this:
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
username = Column(String(15), nullable=False, unique=True)
email = Column(String(300))
password = Column(String(300), nullable=False)
def __init__(self, username, password, firstname, lastname, email):
self.username = username
self.password = hashlib.sha224(password).hexdigest()
self.email = email
def __repr__(self):
# code
I would say group should contain [say a set of] users in it. Not the other way round.
Now, you might have a need to know the group given the user, if you want to cache this mapping in memory (instead of getting it from DB) then I would recommend NOT to create a circular dependency between group and user classes. Instead promote the that dependency to a third class, which just defines the relation between group and users.
E.g.
class user:
def __init__(self, name):
self.name = name
class group:
def __init__(self, name):
self.name = name
class user_group_relation:
def get_users(self, grp_name):
# could get it from an internal map (like return g2u_mapping[grp_name]) or
# run a query on some DB table..
pass
def get_group(self, usr_name):
# could get it from an internal map or run a query on some DB table..
pass
# group to user mapping
u2g_mapping = {user('user1'): group('group1'), user('user2'): group('group1')}
g2u_mapping = {group('group1'): [user('user1'), user('user2')]}
if this is e.g. a representation of normal user authentication mechanism then only users have passwords, groups don't. Groups just represent a way to organizing things. Hence the password field (and any related functionality should remain in user and not promoted to base.

Categories

Resources