Metadata-Version: 2.5
Name: Flask-Authorize
Version: 0.2.8
Summary: Flask plugin for content authorization and access control
Project-URL: Homepage, https://github.com/bprinty/Flask-Authorize
Author-email: Blake Printy <bprinty@gmail.com>
License: MIT
License-File: LICENSE
Keywords: Flask-Authorize,acl,authorization,authz,flask,group,permissions,rbac,role,user
Classifier: Environment :: Web Environment
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Natural Language :: English
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.9
Requires-Dist: flask
Requires-Dist: flask-sqlalchemy
Requires-Dist: six
Requires-Dist: sqlalchemy
Description-Content-Type: text/markdown

[![Build status](https://github.com/bprinty/Flask-Authorize/actions/workflows/ci.yml/badge.svg)](https://github.com/bprinty/Flask-Authorize/actions/workflows/ci.yml)
[![Code coverage](https://codecov.io/gh/bprinty/Flask-Authorize/branch/master/graph/badge.svg)](https://codecov.io/gh/bprinty/Flask-Authorize)
[![Maintenance yes](https://img.shields.io/badge/Maintained%3F-yes-green.svg)](https://github.com/bprinty/Flask-Authorize/graphs/commit-activity)
[![GitHub license](https://img.shields.io/github/license/bprinty/Flask-Authorize.svg)](https://github.com/bprinty/Flask-Authorize/blob/master/LICENSE)
[![Documentation Status](https://readthedocs.org/projects/flask-authorize/badge/?version=latest)](http://flask-authorize.readthedocs.io/?badge=latest)


# Flask-Authorize

Flask-Authorize is a Flask extension designed to simplify the process of incorporating Access Control Lists (ACLs) and Role-Based Access Control (RBAC) into applications housing sensitive data, allowing developers to focus on the actual code for their application instead of logic for enforcing permissions. It uses a unix-like permissions scheme for enforcing access permissions on existing content, and also provides mechanisms for globally enforcing permissions throughout an application.


## Installation

To install the latest stable release via pip, run:

```bash
$ pip install Flask-Authorize
```

Or via [uv](https://docs.astral.sh/uv/), run:

```bash
$ uv add Flask-Authorize
```

To install the bleeding-edge version of the project (not recommended):

```bash
$ git clone http://github.com/bprinty/Flask-Authorize.git
$ cd Flask-Authorize
$ uv sync --all-groups
```


## Usage

Below details a minimal example showcasing how to use the extension. First, to set up the flask application with extensions:

```python
from flask import Flask
from flask_login import LoginManager
from flask_sqlalchemy import SQLAlchemy
from flask_authorize import Authorize

app = Flask(__name__)
app.config.from_object(Config)
db = SQLAlchemy(app)
login = LoginManager(app)
authorize = Authorize(app)
```

Defining database models:

```python
from flask_authorize import RestrictionsMixin, AllowancesMixin
from flask_authorize import PermissionsMixin


# mapping tables
UserGroup = db.Table(
    'user_group', db.Model.metadata,
    db.Column('user_id', db.Integer, db.ForeignKey('users.id')),
    db.Column('group_id', db.Integer, db.ForeignKey('groups.id'))
)


UserRole = db.Table(
    'user_role', db.Model.metadata,
    db.Column('user_id', db.Integer, db.ForeignKey('users.id')),
    db.Column('role_id', db.Integer, db.ForeignKey('roles.id'))
)


# models
class User(db.Model):
    __tablename__ = 'users'

    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(255), nullable=False, unique=True)

    # `roles` and `groups` are reserved words that *must* be defined
    # on the `User` model to use group- or role-based authorization.
    roles = db.relationship('Role', secondary=UserRole)
    groups = db.relationship('Group', secondary=UserGroup)


class Group(db.Model, RestrictionsMixin):
    __tablename__ = 'groups'

    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(255), nullable=False, unique=True)


class Role(db.Model, AllowancesMixin):
    __tablename__ = 'roles'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(255), nullable=False, unique=True)


class Article(db.Model, PermissionsMixin):
    __tablename__ = 'articles'
    __permissions__ = dict(
        owner=['read', 'update', 'delete', 'revoke'],
        group=['read', 'update'],
        other=['read']
    )

    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(255), index=True, nullable=False)
```

Defining endpoint actions:

```python
from flask import jsonify
from werkzeug import NotFound, Unauthorized

@app.route('/articles', methods=['POST'])
@login.logged_in
@authorize.create(Article)
def article():
    article = Article(**request.json)
    db.session.add(article)
    db.session.commit()
    return jsonify(msg='Created Article'), 200

@app.route('/articles/<int:ident>', methods=['GET', 'PUT', 'DELETE'])
@login.logged_in
def single_article(ident):
    article = db.session.query(Article).filter_by(id=ident).first()
    if not article:
        raise NotFound

    if request.method == 'GET':

        # check if the current user is authorized to read the article
        if not authorize.read(article):
            raise Unauthorized

        return jsonify(id=article.id, name=article.name), 200

    elif request.method == 'PUT':

        # check if the current user is authorized to update to the article
        if not authorize.update(article):
            raise Unauthorized

        for key, value in request.json.items():
            setattr(article, key, value)
        db.session.commit()

        return jsonify(id=article.id, name=article.name), 200

    elif request.method == 'DELETE':

        # check if the current user is associated with the 'admin' role
        if not authorize.delete(article) or \
           not authorize.has_role('admin'):
            raise Unauthorized

        db.session.delete(article)
        db.session.commit()

    return

@app.route('/articles/<int:ident>/revoke', methods=['POST'])
@login.logged_in
def revoke_article(ident):
    article = db.session.query(Article).filter_by(id=ident).first()
    if not article:
        raise NotFound

    # check if the current user can revoke the article
    if not authorize.revoke(article):
        raise Unauthorized

    article.revoked = True
    db.session.commit()

    return
```

Additionally, if you've configured your application to dispatch request processing to API functions, you can use the `authorize` extension object as a decorator:

```python
@authorize.create(Article)
def create_article(name):
    article = Article(**request.json)
    db.session.add(article)
    db.session.commit()
    return article

@authorize.read
def read_article(article):
    return article

@authorize.update
def update_article(article, **kwargs):
    for key, value in request.json.items():
        setattr(article, key, value)
        db.session.commit()
    return article

@authorize.delete
def delete_article(article):
    db.session.delete(article)
    return

@authorize.revoke
def revoke_article(article):
    article.revoke = True
    db.session.commit()
    return

@authorize.has_role('admin')
def get_admin_articles():
    pass
```

Using the extension as a decorator goes a long way in removing boilerplate associated with permissions checking. Additionally, using the `authorize` extension object as a decorator will implicitly check the current user's access to each argument or keyword argument to the function. For example, if your method takes two `Article` objects and merges them into one, you can add permissions for both operations like so:

```python
@authorize.read
@authorize.create(Article)
def merge_articles(article1, article2):
    new_article = Article(name=article1.name + article.2.name)
    db.session.add(new_article)
    db.session.delete(article1, article2)
    db.session.commit()
    return new_article
```

This function will ensure that the current user has read access to both articles and also create permissions on the **Article** model itself. If the authorization criteria aren't satisfied, an `Unauthorized` error will be thrown.

Finally, the `authorize` operator is also available in Jinja templates:

```html
<!-- button for creating new article -->
{% if authorize.create('articles') %}
    <button>Create Article</button>
{% endif %}

<!-- display article feed -->
{% for article in articles %}

    <!-- show article if user has read access -->
    {% if authorize.read(article) %}
        <h1>{{ article.name }}</h1>

        <!-- add edit button for users who can update-->
        {% if authorize.update(article) %}
            <button>Update Article</button>
        {% endif %}

        <!-- add delete button for administrators -->
        {% if authorize.in_group('admins') %}
            <button>Delete Article</button>
        {% endif %}

    {% endif %}
{% endfor %}
```


## Documentation

For more detailed documentation, see the [Docs](https://Flask-Authorize.readthedocs.io/en/latest/).


## Questions/Feedback

File an issue in the [GitHub issue tracker](https://github.com/bprinty/Flask-Authorize/issues).
