├── .gitignore ├── main.py ├── readme.md └── templates └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | .idea/ 3 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | # Example of combining Flask-Security and Flask-Admin. 2 | # by Steve Saporta 3 | # April 15, 2014 4 | # 5 | # Uses Flask-Security to control access to the application, with "admin" and "end-user" roles. 6 | # Uses Flask-Admin to provide an admin UI for the lists of users and roles. 7 | # SQLAlchemy ORM, Flask-Mail and WTForms are used in supporting roles, as well. 8 | 9 | from flask import Flask, render_template 10 | from flask.ext.sqlalchemy import SQLAlchemy 11 | from flask.ext.security import current_user, login_required, RoleMixin, Security, \ 12 | SQLAlchemyUserDatastore, UserMixin, utils 13 | from flask_mail import Mail 14 | from flask.ext.admin import Admin 15 | from flask.ext.admin.contrib import sqla 16 | 17 | from wtforms.fields import PasswordField 18 | 19 | # Initialize Flask and set some config values 20 | app = Flask(__name__) 21 | app.config['DEBUG']=True 22 | # Replace this with your own secret key 23 | app.config['SECRET_KEY'] = 'super-secret' 24 | # The database must exist (although it's fine if it's empty) before you attempt to access any page of the app 25 | # in your browser. 26 | # I used a PostgreSQL database, but you could use another type of database, including an in-memory SQLite database. 27 | # You'll need to connect as a user with sufficient privileges to create tables and read and write to them. 28 | # Replace this with your own database connection string. 29 | #xxxxx 30 | app.config['SQLALCHEMY_DATABASE_URI'] = 'postgresql://postgres:xxxxxxxx@localhost/flask_example' 31 | 32 | # Set config values for Flask-Security. 33 | # We're using PBKDF2 with salt. 34 | app.config['SECURITY_PASSWORD_HASH'] = 'pbkdf2_sha512' 35 | # Replace this with your own salt. 36 | app.config['SECURITY_PASSWORD_SALT'] = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' 37 | 38 | # Flask-Security optionally sends email notification to users upon registration, password reset, etc. 39 | # It uses Flask-Mail behind the scenes. 40 | # Set mail-related config values. 41 | # Replace this with your own "from" address 42 | app.config['SECURITY_EMAIL_SENDER'] = 'no-reply@example.com' 43 | # Replace the next five lines with your own SMTP server settings 44 | app.config['MAIL_SERVER'] = 'email-smtp.us-west-2.amazonaws.com' 45 | app.config['MAIL_PORT'] = 465 46 | app.config['MAIL_USE_SSL'] = True 47 | app.config['MAIL_USERNAME'] = 'xxxxxxxxxxxxxxxxxxxx' 48 | app.config['MAIL_PASSWORD'] = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' 49 | 50 | # Initialize Flask-Mail and SQLAlchemy 51 | mail = Mail(app) 52 | db = SQLAlchemy(app) 53 | 54 | # Create a table to support a many-to-many relationship between Users and Roles 55 | roles_users = db.Table( 56 | 'roles_users', 57 | db.Column('user_id', db.Integer(), db.ForeignKey('user.id')), 58 | db.Column('role_id', db.Integer(), db.ForeignKey('role.id')) 59 | ) 60 | 61 | 62 | # Role class 63 | class Role(db.Model, RoleMixin): 64 | 65 | # Our Role has three fields, ID, name and description 66 | id = db.Column(db.Integer(), primary_key=True) 67 | name = db.Column(db.String(80), unique=True) 68 | description = db.Column(db.String(255)) 69 | 70 | # __str__ is required by Flask-Admin, so we can have human-readable values for the Role when editing a User. 71 | # If we were using Python 2.7, this would be __unicode__ instead. 72 | def __str__(self): 73 | return self.name 74 | 75 | # __hash__ is required to avoid the exception TypeError: unhashable type: 'Role' when saving a User 76 | def __hash__(self): 77 | return hash(self.name) 78 | 79 | 80 | # User class 81 | class User(db.Model, UserMixin): 82 | 83 | # Our User has six fields: ID, email, password, active, confirmed_at and roles. The roles field represents a 84 | # many-to-many relationship using the roles_users table. Each user may have no role, one role, or multiple roles. 85 | id = db.Column(db.Integer, primary_key=True) 86 | email = db.Column(db.String(255), unique=True) 87 | password = db.Column(db.String(255)) 88 | active = db.Column(db.Boolean()) 89 | confirmed_at = db.Column(db.DateTime()) 90 | roles = db.relationship( 91 | 'Role', 92 | secondary=roles_users, 93 | backref=db.backref('users', lazy='dynamic') 94 | ) 95 | 96 | 97 | # Initialize the SQLAlchemy data store and Flask-Security. 98 | user_datastore = SQLAlchemyUserDatastore(db, User, Role) 99 | security = Security(app, user_datastore) 100 | 101 | 102 | # Executes before the first request is processed. 103 | @app.before_first_request 104 | def before_first_request(): 105 | 106 | # Create any database tables that don't exist yet. 107 | db.create_all() 108 | 109 | # Create the Roles "admin" and "end-user" -- unless they already exist 110 | user_datastore.find_or_create_role(name='admin', description='Administrator') 111 | user_datastore.find_or_create_role(name='end-user', description='End user') 112 | 113 | # Create two Users for testing purposes -- unless they already exists. 114 | # In each case, use Flask-Security utility function to encrypt the password. 115 | encrypted_password = utils.encrypt_password('password') 116 | if not user_datastore.get_user('someone@example.com'): 117 | user_datastore.create_user(email='someone@example.com', password=encrypted_password) 118 | if not user_datastore.get_user('admin@example.com'): 119 | user_datastore.create_user(email='admin@example.com', password=encrypted_password) 120 | 121 | # Commit any database changes; the User and Roles must exist before we can add a Role to the User 122 | db.session.commit() 123 | 124 | # Give one User has the "end-user" role, while the other has the "admin" role. (This will have no effect if the 125 | # Users already have these Roles.) Again, commit any database changes. 126 | user_datastore.add_role_to_user('someone@example.com', 'end-user') 127 | user_datastore.add_role_to_user('admin@example.com', 'admin') 128 | db.session.commit() 129 | 130 | 131 | # Displays the home page. 132 | @app.route('/') 133 | # Users must be authenticated to view the home page, but they don't have to have any particular role. 134 | # Flask-Security will display a login form if the user isn't already authenticated. 135 | @login_required 136 | def index(): 137 | return render_template('index.html') 138 | 139 | 140 | # Customized User model for SQL-Admin 141 | class UserAdmin(sqla.ModelView): 142 | 143 | # Don't display the password on the list of Users 144 | column_exclude_list = ('password',) 145 | 146 | # Don't include the standard password field when creating or editing a User (but see below) 147 | form_excluded_columns = ('password',) 148 | 149 | # Automatically display human-readable names for the current and available Roles when creating or editing a User 150 | column_auto_select_related = True 151 | 152 | # Prevent administration of Users unless the currently logged-in user has the "admin" role 153 | def is_accessible(self): 154 | return current_user.has_role('admin') 155 | 156 | # On the form for creating or editing a User, don't display a field corresponding to the model's password field. 157 | # There are two reasons for this. First, we want to encrypt the password before storing in the database. Second, 158 | # we want to use a password field (with the input masked) rather than a regular text field. 159 | def scaffold_form(self): 160 | 161 | # Start with the standard form as provided by Flask-Admin. We've already told Flask-Admin to exclude the 162 | # password field from this form. 163 | form_class = super(UserAdmin, self).scaffold_form() 164 | 165 | # Add a password field, naming it "password2" and labeling it "New Password". 166 | form_class.password2 = PasswordField('New Password') 167 | return form_class 168 | 169 | # This callback executes when the user saves changes to a newly-created or edited User -- before the changes are 170 | # committed to the database. 171 | def on_model_change(self, form, model, is_created): 172 | 173 | # If the password field isn't blank... 174 | if len(model.password2): 175 | 176 | # ... then encrypt the new password prior to storing it in the database. If the password field is blank, 177 | # the existing password in the database will be retained. 178 | model.password = utils.encrypt_password(model.password2) 179 | 180 | 181 | # Customized Role model for SQL-Admin 182 | class RoleAdmin(sqla.ModelView): 183 | 184 | # Prevent administration of Roles unless the currently logged-in user has the "admin" role 185 | def is_accessible(self): 186 | return current_user.has_role('admin') 187 | 188 | # Initialize Flask-Admin 189 | admin = Admin(app) 190 | 191 | # Add Flask-Admin views for Users and Roles 192 | admin.add_view(UserAdmin(User, db.session)) 193 | admin.add_view(RoleAdmin(Role, db.session)) 194 | 195 | 196 | # If running locally, listen on all IP addresses, port 8080 197 | if __name__ == '__main__': 198 | app.run( 199 | host='0.0.0.0', 200 | port=int('8080'), 201 | debug=app.config['DEBUG'] 202 | ) 203 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | Example of combining Flask-Security and Flask-Admin 2 | by Steve Saporta 3 | April 15, 2014 4 | 5 | Flask-Security provides a convenient way to add authentication and authorization to a Flask web app. Flask-Admin provides a convenient way to perform CRUD operations on database tables. This example combines Flask-Security and Flask-Admin so that authorized administrators can maintain the lists of users and roles that control access to the app. 6 | 7 | You could easily generalize this app so that administrators could manage all sorts of database tables, not just users and roles. 8 | 9 | This app also provides a basic example of the use of several underlying technologies that support Flask-Security and Flask-Admin. 10 | 11 | Here are some helpful links: 12 | - Flask: http://flask.pocoo.org/ 13 | - Flask-Admin: https://flask-admin.readthedocs.org/en/latest/ 14 | - Flask-Security: https://pythonhosted.org/Flask-Security/ 15 | - Flask-Mail, which helps send email messages generated by Flask-Security: http://wtforms.simplecodes.com/docs/0.6.1/index.html 16 | - SQLAlchemy, which supports SQL database access: http://wtforms.simplecodes.com/docs/0.6.1/index.html 17 | - In particular, SQLAlchemy ORM is SQLAlchemy's object relational manager, used by Flask-Admin: http://docs.sqlalchemy.org/en/rel_0_9/orm/tutorial.html 18 | - WTForms, which renders forms used by Flask-Admin: http://wtforms.simplecodes.com/docs/0.6.1/index.html 19 | - I used a PostgreSQL database: http://www.postgresql.org/ 20 | - However, you might find an in-memory SQLite database convenient: https://sqlite.org/ 21 | - A nod to this StackOverflow article regarding an issue with the \_\_hash\_\_ function in Flask-Admin: http://stackoverflow.com/questions/19530368/sqlalchemy-typeerror-unhashable-type-creating-instance-sqlalchemy 22 | - And to mrjoes for this article about encrypting the password form field in Flask-Admin: http://stackoverflow.com/questions/19530368/sqlalchemy-typeerror-unhashable-type-creating-instance-sqlalchemy 23 | 24 | To run this app, you'll need to install: 25 | - Python 3.3 26 | - Flask 27 | - Flask-Admin 28 | - Flask-Security 29 | - SQLAlchemy 30 | - A database system (PostgreSQL, SQLite, or another database system of your choice) 31 | - An empty database named "flask_example" 32 | 33 | I personally ran it on Windows 7 with a PostgreSQL database, but you should be able to use the operating system and database system of your choice. 34 | 35 | Comments throughout main.py explain what's going on. 36 | 37 | Once you have the app running, you can view it in your browser (e.g. http://localhost:8080). 38 | 39 | Some things to notice: 40 | - When you first visit the app's home page, you'll be prompted to log in, thanks to 41 | Flask-Security. 42 | - If you log in with username=someone@example.com and password=password, you'll have the 43 | "end-user" role. 44 | - If you log in with username=admin@example.com and password=password, you'll have the "admin" 45 | role. 46 | - Either role is permitted to access the home page. 47 | - Either role is permitted to access the /admin page. However, unless you have the "admin" 48 | role, you won't see the tabs for administration of users and roles on this page. 49 | - Only the admin role is permitted to access sub-pages of /admin page such as 50 | /admin/userview. Otherwise, you'll get a "forbidden" response. 51 | - Note that, when editing a user, the names of roles are automatically populated thanks to 52 | Flask-Admin. 53 | - You can add and edit users and roles. The resulting users will be able to log in (unless you 54 | set active=false) and, if they have the "admin" role, will be able to perform administration. 55 | 56 | Enjoy! -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |