Flask-Principal Documentation

repository·main·Indexed 19 days ago

https://github.com/pallets-eco/flask-principal

Identity 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.

Tokens
5.2K
Snippets
16
Records
19
Agent score
66%

What's inside Flask-Principal

  1. Overview of Flask-Principal

    main
    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.
  2. Combine permissions using bitwise operators

    main

    For complex authorization logic, you can combine Permission objects 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')
  3. Core concepts of Flask-Principal

    main

    Flask-Principal uses a loose framework based on signals to manage access control through four main components:

    1. 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.
    2. 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.
    3. Permission: A set of requirements (Needs). Access is granted if the requirements are met.
    4. IdentityContext: The mechanism used to check an Identity against a Permission. It can be used as a decorator or a context manager.
  4. Protect access to resources using decorators or context managers

    main

    Once 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')
  5. Implement a User Information provider using `identity-loaded`

    main

    User information providers should connect to the identity-loaded signal. This signal is triggered when an identity is loaded, allowing you to attach additional data (like roles or specific resource permissions) to the identity.provides set.

    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))
  6. Implement granular resource protection

    main

    For fine-grained access (e.g., 'only the author can edit this post'), create custom Need objects and a Permission class that encapsulates the logic for a specific resource ID. You then populate the identity.provides set during the identity_loaded phase.

    1. Define a Need: Use a namedtuple or similar to represent the resource and action.
    2. Define a Permission: A class that takes a resource ID and creates the corresponding Need.
    3. Populate Identity: In the identity_loaded handler, add the user's specific resource permissions to identity.provides.
    4. 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
  7. Implement an Authentication provider using `identity-changed`

    main

    Authentication providers should trigger the identity-changed signal to notify Flask-Principal that a user's authentication status has changed. This allows the system to update the current Identity.

    • On Login: Send an Identity object containing the user's ID.
    • On Logout: Send an AnonymousIdentity object 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())
  8. Handle identity changes with signals

    main

    Flask-Principal provides two main signals for managing the identity lifecycle:

    1. 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.
    2. 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))
  9. Define identity loaders and savers

    main

    Flask-Principal uses a chain of loaders and savers to manage identities across requests.

    • Identity Loaders: Use the @principals.identity_loader decorator to define functions that attempt to find an identity (e.g., from a cookie, header, or database). The first loader that returns a non-None Identity object wins.
    • Identity Savers: Use the @principals.identity_saver decorator 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
  10. Initialize Flask-Principal with the Principal extension

    main

    To use Flask-Principal, create an instance of the Principal class. You can pass your Flask app directly during initialization or use init_app(app) later. By default, it uses Flask sessions to store and retrieve identities. You can also configure it to skip static routes using skip_static=True.

    from flask import Flask
    from flask_principal import Principal
    
    app = Flask(__name__)
    principals = Principal(app)
    # or
    # principals = Principal()
    # principals.init_app(app)
  11. Create custom permissions by subclassing BasePermission

    main

    If your authorization logic depends on factors other than standard Needs, you can implement a custom permission by subclassing BasePermission and overriding the allows(self, identity) method. The allows method should return True if the identity is permitted or False otherwise. 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 False
  12. Combine permissions using AndPermission and OrPermission

    main

    In addition to bitwise operators, you can use the explicit constructor classes AndPermission and OrPermission to 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)