27

I am writing an app that has multiple classes that function as Users (for example, a School Account and a Staff account). I'm trying to use Flask-Login to make this easy but I'm not quite sure how to make it, so that when a user logs in I can have my app check to see whether or not the username belongs to a School account or Staff account, and then log in appropriately.

I know how to figure out which type of account it belongs to (since all usernames must be unique). But after that I'm not sure how to tell the app that I want it to login that specific user.

Right now, I only have one universal login page. Is it easier if I make separate login pages for Staff accounts and School accounts? I'm using a MySQL database through Flask-SQLAlchemy.

Dale K
  • 25,246
  • 15
  • 42
  • 71
Ashu Goel
  • 323
  • 2
  • 4
  • 7
  • A way simplifying what you are attempting is to use roles, so that you don't have many different classes per user and use inheritance since you are using Flask-SQLAlchemy. – lv10 Apr 08 '13 at 14:13
  • The problem is I am using existing databases that are separate and have separate data – Ashu Goel May 08 '13 at 04:01

5 Answers5

51

You can define each User with a specific role. For example, user 'x' can be SCHOOL while user 'y' can be 'STAFF'.

class User(db.Model):

    __tablename__ = 'User'
    id = db.Column(db.Integer,primary_key=True)
    username = db.Column(db.String(80),unique=True)
    pwd_hash = db.Column(db.String(200))
    email = db.Column(db.String(256),unique=True)
    is_active = db.Column(db.Boolean,default=False)
    urole = db.Column(db.String(80))


    def __init__(self,username,pwd_hash,email,is_active,urole):
            self.username = username
            self.pwd_hash = pwd_hash
            self.email = email
            self.is_active = is_active
            self.urole = urole

    def get_id(self):
            return self.id
    def is_active(self):
            return self.is_active
    def activate_user(self):
            self.is_active = True         
    def get_username(self):
            return self.username
    def get_urole(self):
            return self.urole

Flask-login however does not have the concept of user roles yet and I wrote my own version of login_required decorator to override that. So you might want to use something like:

def login_required(role="ANY"):
    def wrapper(fn):
        @wraps(fn)
        def decorated_view(*args, **kwargs):

            if not current_user.is_authenticated():
               return current_app.login_manager.unauthorized()
            urole = current_app.login_manager.reload_user().get_urole()
            if ( (urole != role) and (role != "ANY")):
                return current_app.login_manager.unauthorized()      
            return fn(*args, **kwargs)
        return decorated_view
    return wrapper

Then, you can use this decorator on a view function like:

@app.route('/school/')
@login_required(role="SCHOOL")
def restricted_view_for_school():
    pass
codegeek
  • 32,236
  • 12
  • 63
  • 63
  • The problem is I have an existing database with separate SCHOOL and STAFF database that has data in it. Do you think this way would still be possible? Could I just create a generic user class that doesn't relate to the database but instead wraps a school or staff account? – Ashu Goel May 07 '13 at 22:58
  • 1
    I am not sure if I quite follow you but usually, you should have 1 generic User class/model which could usually correspond to 1 database table. I strongly advise you to use the concept of "roles" as I explained above. It works very well. – codegeek May 08 '13 at 14:00
  • 2
    This is the approach I took. Thank you very much for a clear, helpful answer. – tmthyjames Sep 11 '15 at 20:05
  • How do you recommend to override the default `@login_required` decorator when the blueprints are in a different module? 1. Import the custom decorator defined in your answer or somehow override `flask_login.login_required`? – Greg Aug 27 '20 at 11:31
12

I had to modify codegeek's code a bit to get it working for me, so I figured I'd drop it here in case it can help anyone else:

from functools import wraps

login_manager = LoginManager()

...

def login_required(role="ANY"):
    def wrapper(fn):
        @wraps(fn)
        def decorated_view(*args, **kwargs):
            if not current_user.is_authenticated():
              return login_manager.unauthorized()
            if ((current_user.role != role) and (role != "ANY")):
                return login_manager.unauthorized()
            return fn(*args, **kwargs)
        return decorated_view
    return wrapper
Patrick Yoder
  • 1,065
  • 4
  • 14
  • 19
Chockomonkey
  • 3,895
  • 7
  • 38
  • 55
  • 2
    This works but I need to remove `()` from `is_authenticated()`, as it's not a bool function. – stenlytw Jun 24 '19 at 02:22
  • How do you override the standard login_required decorator? That is, how do get your login_required to be executed instead of the standard flask_login.login_required? – Greg Aug 24 '20 at 11:49
  • I'm pretty sure simply defining it overwrites it. Or if you haven't imported the login_required function explicitly from flask_login, there will be no login_required to overwrite. – Chockomonkey Oct 08 '20 at 21:42
6

This is an example of what you could do. I don't have experience using Flask-SQLAlchemy, but the how shouldn't be much more different. The example below uses SQLAlchemy directly.

First you define a user class that inherits from Base so that it can be mapped by ORM (Declarative)

class User(Base):

    __tablename__ = 'user_table'
    id = Column(Integer, primary_key=True)
    email = Column(String(45), unique=True)
    name = Column(String(45))
    pwd = Column(String(8))
    user_role = Column(String(15))

    __mapper_args__ = {
        'polymorphic_identity': 'user_table',
        'polymorphic_on': user_role
    }

Once your parent class class is ready, set a different class for each of the roles that you want to have.

class SchoolAccount(User):
    __tablename__ = 'school_account'
    id = Column(Integer, ForeignKey('user_table.id'), primary_key=True)
    representative_name = Column(String(45))

    __mapper_args__ = {
        'polymorphic_identity': 'school_account'
    } 

Using Flask-Login you login the user and limit access based on roles.

Here is an example of a login system with two different roles. This is a nice tutorial for flask, flask-sqlalchemy, flask-login: http://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-v-user-logins

lv10
  • 1,469
  • 7
  • 25
  • 46
3

An option would be Flask User as mentioned by @lv10 answer. It implements an abstraction to User that handles a lot of things, one of them is Role-based authentication.

The code will remain basically the same as in @codegeek answer, but "urole" property on User class should be renamed to "roles" and role have to be an class with "name" property (you can copy-paste from documentation). You'll not need define login_required, as it's already implemented as roles_required. So, instead of @login_required(role='rolename') we have @roles_required('rolename').

I just started to use this API for security reasons and it's being an great experience. I highly recommend to anyone with problems with passwords, user authentication and user roles.

Daniel Mitre
  • 176
  • 5
  • 1
    thanks for your answer, i have a similar problem but i've been using two classes for two different user types, the problem is only one of them should have a relationship with other tables, so how can i get over this ? your help will be appreciated. – BHA Bilel May 20 '20 at 22:35
1

Following accepted solution (by @codegeek), in case if you want multiple roles tagged against each routes with users having one/multiple roles, you may try below --

login_manager = LoginManager()


def role_required(role=[]):  
    def wrapper(fn):
        @wraps(fn)
        def decorated_view(*args, **kwargs):
            if not current_user.is_authenticated:
                return login_manager.unauthorized()

            if all(x != role1 for role1 in role for x in current_user.role) and (all(role1 != "ANY" for role1 in role)):  # One user may have multiple roles
                
                return render_template("Unauthorized_Access.html")

            return fn(*args, **kwargs)

        return decorated_view

    return wrapper

where --

dummy user database --

users = {'User_1': {'role': ['school', 'staff']}, 'User_2': {'role': ['admin']},
'User_3': {'role': ['staff', 'admin']}}

User Class --

class User(UserMixin):
    role = None

User Loader (Callback to reload the user object)

@login_manager.user_loader
def user_loader(userid):
    user = User()
    user.id = userid
    user.role = users[userid]['role']
    return user
Blue Bird
  • 193
  • 3
  • 8