Flask-Principal Documentation
repository·main·Indexed 19 days ago
https://github.com/pallets-eco/flask-principalIdentity management extension for Flask applications (version 0.4.0). It provides a signal-based framework for handling user identities and access control using core concepts such as Identities, Needs, Permissions, and IdentityContext. The library supports protecting resources via decorators or context managers, granular resource protection, and complex authorization logic using bitwise operators or explicit AndPermission and OrPermission classes.
What's inside Flask-Principal
- Flask-Principal is an extension designed for identity management within Flask applications. It provides tools to handle identities and permissions, allowing developers to manage user roles and access control within the Flask ecosystem.
Combine permissions using bitwise operators
mainFor complex authorization logic, you can combine
Permissionobjects using bitwise operators. This allows you to create new permission objects that represent logical combinations of existing needs.|(OR): Requires at least one of the permissions to be met.&(AND): Requires all permissions to be met.~(NOT): Negates the permission (the identity must NOT have this permission).
blog_admin = Permission(RoleNeed('blog_admin')) blog_poster = Permission(RoleNeed('blog_poster')) blog_reviewer = Permission(RoleNeed('blog_reviewer')) under_probation = Permission(RoleNeed('under_probation')) # Requires (blog_poster OR blog_reviewer) AND NOT under_probation prize_permission = ((blog_poster | blog_reviewer) & ~under_probation) @app.route('/blog/prizes') @prize_permission.require() def prize_redeem(): return render_template('prize_redeem.html')Core concepts of Flask-Principal
mainFlask-Principal uses a loose framework based on signals to manage access control through four main components:
- Identity: Represents the user (the 'avatar' in the system). It is loaded per request (e.g., from a session) and contains the set of access rights (Needs) the user possesses.
- Need: The smallest unit of access control. It represents a specific requirement, such as a role (
('role', 'admin')) or a specific action on an object (('article', 'edit', 46)). Needs can be simple tuples or custom objects. - Permission: A set of requirements (Needs). Access is granted if the requirements are met.
- IdentityContext: The mechanism used to check an Identity against a Permission. It can be used as a decorator or a context manager.
Protect access to resources using decorators or context managers
mainOnce you have defined a
Permission, you can protect views or specific blocks of code using the.require()method. This method can be used as a decorator for Flask routes or as a context manager within a function.from flask import Flask, Response from flask_principal import Principal, Permission, RoleNeed app = Flask(__name__) principals = Principal(app) # Define a permission requiring the 'admin' role admin_permission = Permission(RoleNeed('admin')) # Option 1: Protect a view with a decorator @app.route('/admin') @admin_permission.require() def do_admin_index(): return Response('Only if you are an admin') # Option 2: Protect a block of code with a context manager @app.route('/articles') def do_articles(): with admin_permission.require(): return Response('Only if you are admin')from flask import Flask, Response from flask_principal import Principal, Permission, RoleNeed app = Flask(__name__) # load the extension principals = Principal(app) # Create a permission with a single Need, in this case a RoleNeed. admin_permission = Permission(RoleNeed('admin')) # protect a view with a principal for that need @app.route('/admin') @admin_permission.require() def do_admin_index(): return Response('Only if you are an admin') # this time protect with a context manager @app.route('/articles') def do_articles(): with admin_permission.require(): return Response('Only if you are admin')Implement a User Information provider using `identity-loaded`
mainUser information providers should connect to the
identity-loadedsignal. This signal is triggered when an identity is loaded, allowing you to attach additional data (like roles or specific resource permissions) to theidentity.providesset.from flask_principal import identity_loaded, RoleNeed, UserNeed @identity_loaded.connect_via(app) def on_identity_loaded(sender, identity): # Attach the user object to the identity identity.user = current_user # Add specific Needs to the identity's 'provides' set if hasattr(current_user, 'id'): identity.provides.add(UserNeed(current_user.id)) if hasattr(current_user, 'roles'): for role in current_user.roles: identity.provides.add(RoleNeed(role.name))@identity_loaded.connect_via(app) def on_identity_loaded(sender, identity): # Set the identity user object identity.user = current_user # Add the UserNeed to the identity if hasattr(current_user, 'id'): identity.provides.add(UserNeed(current_user.id)) # Assuming the User model has a list of roles, update the # identity with the roles that the user provides if hasattr(current_user, 'roles'): for role in current_user.roles: identity.provides.add(RoleNeed(role.name))Implement granular resource protection
mainFor fine-grained access (e.g., 'only the author can edit this post'), create custom
Needobjects and aPermissionclass that encapsulates the logic for a specific resource ID. You then populate theidentity.providesset during theidentity_loadedphase.- Define a Need: Use a
namedtupleor similar to represent the resource and action. - Define a Permission: A class that takes a resource ID and creates the corresponding
Need. - Populate Identity: In the
identity_loadedhandler, add the user's specific resource permissions toidentity.provides. - Check Permission: Use
permission.can()in your view logic.
from collections import namedtuple from flask_principal import identity_loaded, Permission, RoleNeed, UserNeed # 1. Define the Need BlogPostNeed = namedtuple('blog_post', ['method', 'value']) EditBlogPostNeed = partial(BlogPostNeed, 'edit') # 2. Define the Permission class class EditBlogPostPermission(Permission): def __init__(self, post_id): need = EditBlogPostNeed(str(post_id)) super().__init__(need) # 3. Populate Identity with user's specific post permissions @identity_loaded.connect_via(app) def on_identity_loaded(sender, identity): if hasattr(current_user, 'posts'): for post in current_user.posts: identity.provides.add(EditBlogPostNeed(str(post.id))) # 4. Use in a view @app.route('/posts/<post_id>', methods=['PUT', 'PATCH']) def edit_post(post_id): permission = EditBlogPostPermission(post_id) if permission.can(): return render_template('edit_post.html') abort(403)@app.route('/posts/<post_id>', methods=['PUT', 'PATCH']) def edit_post(post_id): permission = EditBlogPostPermission(post_id) if permission.can(): # Save the edits ... return render_template('edit_post.html') abort(403) # HTTP Forbidden- Define a Need: Use a
Implement an Authentication provider using `identity-changed`
mainAuthentication providers should trigger the
identity-changedsignal to notify Flask-Principal that a user's authentication status has changed. This allows the system to update the currentIdentity.- On Login: Send an
Identityobject containing the user's ID. - On Logout: Send an
AnonymousIdentityobject and manually clear any Flask-Principal related keys from the session.
from flask_principal import Identity, AnonymousIdentity, identity_changed # On successful login identity_changed.send(current_app._get_current_object(), identity=Identity(user.id)) # On logout for key in ('identity.name', 'identity.auth_type'): session.pop(key, None) identity_changed.send(current_app._get_current_object(), identity=AnonymousIdentity())# Tell Flask-Principal the identity changed identity_changed.send(current_app._get_current_object(), identity=Identity(user.id)) # ... later in logout ... # Remove session keys set by Flask-Principal for key in ('identity.name', 'identity.auth_type'): session.pop(key, None) # Tell Flask-Principal the user is anonymous identity_changed.send(current_app._get_current_object(), identity=AnonymousIdentity())- On Login: Send an
Handle identity changes with signals
mainFlask-Principal provides two main signals for managing the identity lifecycle:
identity-changed: Sent when an identity is explicitly set (e.g., during login). Authentication providers should trigger this signal to ensure the identity is saved to the session.identity-loaded: Sent when an identity is initialized for a request. Use this signal to populate the identity with roles, permissions, or additional user data from your database.
from flask_principal import identity_changed, identity_loaded, RoleNeed # Triggering a change (e.g., in a login view) identity_changed.send(app, identity=Identity(username)) # Populating identity data after it is loaded @identity_loaded.connect def on_identity_loaded(sender, identity): user = db.get_user(identity.id) for role in user.roles: identity.provides.add(RoleNeed(role.name))Define identity loaders and savers
mainFlask-Principal uses a chain of loaders and savers to manage identities across requests.
- Identity Loaders: Use the
@principals.identity_loaderdecorator to define functions that attempt to find an identity (e.g., from a cookie, header, or database). The first loader that returns a non-NoneIdentityobject wins. - Identity Savers: Use the
@principals.identity_saverdecorator to define functions that persist an identity when it is changed (e.g., saving to a custom cookie or database).
@principals.identity_loader def load_identity_from_custom_source(): # logic to find identity return Identity('user_id') @principals.identity_saver def save_identity_to_custom_source(identity): # logic to persist identity pass- Identity Loaders: Use the
Initialize Flask-Principal with the Principal extension
mainTo use Flask-Principal, create an instance of the
Principalclass. You can pass your Flaskappdirectly during initialization or useinit_app(app)later. By default, it uses Flask sessions to store and retrieve identities. You can also configure it to skip static routes usingskip_static=True.from flask import Flask from flask_principal import Principal app = Flask(__name__) principals = Principal(app) # or # principals = Principal() # principals.init_app(app)Create custom permissions by subclassing BasePermission
mainIf your authorization logic depends on factors other than standard Needs, you can implement a custom permission by subclassing
BasePermissionand overriding theallows(self, identity)method. Theallowsmethod should returnTrueif the identity is permitted orFalseotherwise. Custom permissions can be combined with other permissions using bitwise operators.from flask.ext.principal import BasePermission class CustomPermission(BasePermission): def allows(self, identity): # Implement custom logic here # e.g., check a database, a session variable, or external API return FalseCombine permissions using AndPermission and OrPermission
mainIn addition to bitwise operators, you can use the explicit constructor classes
AndPermissionandOrPermissionto combine multiple permission objects.from flask.ext.principal import AndPermission, OrPermission # Requires all permissions in the list allperms = AndPermission(blog_poster, blog_reviewer, blog_admin) # Requires any one of the permissions in the list anyperms = OrPermission(blog_poster, blog_reviewer, blog_admin)