Giter VIP home page Giter VIP logo

flask-authorize's Introduction

Build status Code coverage Maintenance yes GitHub license Documentation Status

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:

$ pip install Flask-Authorize

Alternatively with easy_install, run:

$ easy_install Flask-Authorize

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

$ git clone http://github.com/bprinty/Flask-Authorize.git
$ cd Flask-Authorize
$ python setup.py install

Usage

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

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:

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:

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:

@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:

@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:

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

Questions/Feedback

File an issue in the GitHub issue tracker.

flask-authorize's People

Contributors

bprinty avatar htdat148 avatar lanmaster53 avatar mmarihart avatar quichef avatar tordne avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

flask-authorize's Issues

ImportError: No module named 'six'

Traceback (most recent call last):
  File "manage.py", line 1, in <module>
    from app import create_app
  File "/app.py", line 5, in <module>
    from extensions import db, bootstrap, login_manager, authorize
  File "/extensions.py", line 4, in <module>
    from flask_authorize import Authorize
  File "/Documents/VirtualEnvs/xyzy/lib/python3.4/site-packages/flask_authorize/__init__.py", line 11, in <module>
    from .mixins import RestrictionsMixin           ## noqa
  File "/Documents/VirtualEnvs/xyzy/lib/python3.4/site-packages/flask_authorize/mixins.py", line 10, in <module>
    import six
ImportError: No module named 'six'

with Flask-Authorize I can achieve a roles and level permission for each service?

Hi , with Flask-Authorize I can achieve something like this:

ex:
I have implemented a normal role system like this:

ROLES = {
'user': 1,
'moderator': 2,
'admin': 3
}

But I want to achieve something like this for the following, for example

In my project I have the functionalities of:

- Posts 
-Comments 
-Chat

and users:

pepe: is administrator 3
Luis: is moderator 2

Mario: is a normal user 1
Tony: is a normal user 1
timy: is a normal user 1
jose: is a normal user 1

I want to have control of the permissions of each functionality.

pepe:  "can create/modify/delete Posts and Comments and use chat"

Luis:  "can create/modify Posts and Comments and use chat"

Mario: "can create/modify/delete your own Posts, but don't can use chat"

Tony: "can 'only see' all Posts and Comments, but can use chat"

timy: "can see posts but can't see or create comments, and don't can use chat"

jose: "can't see posts or comments, but can use chat"

Give me your ideas on how I could achieve something like that ... thanks!

Usage with factory pattern

Hi,

Maybe before I being its nice to mention that I am new to flask and python, this is my first issue ever, so I am definitely a noob, feedback is welcome.

I am just opening this issue to ask about support of the factory pattern: https://flask.palletsprojects.com/en/1.1.x/patterns/appfactories/

I am using this template https://github.com/cookiecutter-flask/cookiecutter-flask and trying to integrate this package into it.

In extensions.py I have added

from flask_authorize import Authorize
authorize = Authorize(None)

view.py

from app_name.extensions import authorize

app.py

from contentclub.extensions import authorize
authorize.Authorize(app)

I followed this patter because it was what they did for other items similar to this, e.g. sqlalchemy, flask_login etc.

But I am getting :( RuntimeError: Working outside of application context.

I know what it means generally speaking but in this case I am not sure what it is exactly or how I can fix it. It is my hypothesis that the Authorize(None) in extensions.py is actually going through initialising the None when there actually is not an app yet passed. So I have tried going with authorize = None but then I got AttributeError: 'NoneType' object has no attribute 'has_role' in the view, so I think the view is trying to access that object before app.py got done. Also All of this might (is probably) completely wrong though so pointers would be appreciated.

P.S. I have done my best to check the docs thoroughly, I would suggest if a solution for this is present there it is highlighted and if not added. Unless its something started that everyone else would know how to do.

Thank you very much!

Add python 3.10 3.11 3.12 for tests

Hey there,

i'm happily using Flask Authorize and thought it could be an idea to update the pythoon versions used for the CI 😄

Opened the PR accordingly, it also contains Actions version update #67

Hope its ok, and wish you a great end of the year period

Role-based allowance checks are broken, causing a fail-open condition.

I am using the AllowancesMixin on the Role model to do role-based content access control. I'm not using any groups and no content level permissions are defined at the model level.

After getting the general stuff set up (mixins added to models, tables and relationships built, a couple sample authz checks in various views, etc.), I tested things using the authorize.read(model_instance) approach without establishing any roles or setting any allowances for my test user. The documentation claims that the default result is to fail safe and refuse access if a user is not explicitly authorized. However, my test user could access everything without any roles assigned. I went a step further and added a role that didn't have any allowances on any tables ({}) and had the same result. I went one more step further and added an empty set of allowances to a valid table ({'table':[]}) and still had the same result. I then added a single allowance that did not match the action I was trying to perform ({'table':['delete']}) and still had the same result. No restrictions were actually enforced regardless of the configuration.

I traced it down in the code and no matter what you do, using this technique always fails at

if not hasattr(arg, 'permissions'):
and never gets to the allowances checks that happen several lines later on plugin.py#361. Using the authorize.has_role and authorize.create(Model) techniques both work just fine because they happen before line 357. I tried to figure out where the disconnect is, but I don't know the code base well enough to find it quickly. Thought you might be able to track it down before I will. However, it looks like the permissions check is a bit premature as they aren't required for the allowances check and will always return False if not using model-specific content access controls.

Wildcard allowances don't permit all as the documentation states it should.

Setting the default __allowances__ value to * for roles doesn't result in access to everything as the documentation states. Rather, it results in the following error because the user_is_allowed method never checks check for the * value, or the default_allowances method fails to convert the * to something useful.

  File "/usr/local/lib/python3.7/site-packages/flask_authorize/plugin.py", line 208, in user_is_allowed
    allowances.extend(cred.allowances.get(key, default))
AttributeError: 'str' object has no attribute 'get'

Add support for including custom permission types

In the Article example, an application might want to enforce specific authorization to a user for redact-ing a published Article. In that scheme, you'd want to be able to set up your classes and permissions like so:

# article model
class Article(db.Model, PermissionsMixin):
    __permissions__ = dict(
        user=['read', 'update', 'delete', 'revoke'],
        group=['read', 'update']
        other=['read']
    )

# api method
@authorize.revoke
def revoke_article(article):
    pass

Configuration for this could happen on the fly, or it could happen explicitly with the plugin. That's a design decision that needs to be had.

Detect the use of Restrictions/Allowances mixins and provide default fail-safe for users that aren't tied specifically to a role or group.

Originally proposed by @lanmaster53.

Even with the fail-safe option, a failure to provide a role to a user still results in full access to everything:

  1. Someone adds a user.
  2. Forgets to assign a role.
  3. New user is now a super admin.

This should probably also come with a Flask configuration option (something like AUTHORIZE_FAIL_SAFE = True). Documentation will also be important for highlighting the danger of not using this type of fail-safe.

See #31 for additional context.

Flask-SQLAlchemy Dependency needed?

Hi, title more or less already states my question.

More details:
I refactored my flask application to use the SQLAlchemy package instead of Flask-SQLAlchemy, due to certain requirements.
After removing Flask-SQLAlchemy manually, everything regarding the authorization functionality still seems to work and my tests finish successfully.

I am not using all the features of Flask-Authorize though. Therefore, the question if Flask-SQLAlchemy is still required came up?

Thanks.

RestrictionMixin does not work as expected

When _restrictions_ property model is set, there is no default value recorded in the database field that corresponds to the values specified in the class that inherits RestricionMixin. For example in the docs you can see something like:

class Group(db.Model, RestrictionMixin): __restrictions__ = { 'articles': ['update', 'delete'] }

I have seen that the default_restrictions function is not executed. Has anyone tested this functionality?

Default allowance are failing

Hey there,

I think the piece of code :

    role = RoleFactory.create(
        name='unallowed',
        allowances=default_allowances()
    )

do not work because of the default_allowance method, and is causing quite fails in the unit tests.
It seems that, in that method , especially in the

mixins.py
def gather_models():
...
    db = current_app.extensions['sqlalchemy'].db
...

It seems to be failing, as db attribute seems not to exist.

Hope i'm not missing something, i'm trying a bit to have a look, though not super super good in sqlalchemy here.
I suppose the way the code is trying to fetch all the models to define default allowances needs to be changed

Cheers

Unexpected Anonymous user behavior

The behavior for anonymous users (user = None) seems inconsistent/incorrect to me. I am using the package flask-jwt-extended for handling user authentication, so when a user is not logged in, None is returned for the current user. This is in contrast to flask-login where you get an Anonymous User object. I recently switched between the two, and saw no problem when using flask-login.

In an app I am creating, users can be logged in or not logged in (public user). Some entries in our DB are public and should be viewable by everyone - all logged in users, and users who are not logged in (public). To me, this is the equivalent of the permission for other being set to read along with AUTHORIZE_ALLOW_ANONYMOUS_ACTIONS being set to True.

I would expect that users who are not logged in experience the same permissions for an item that users who are not the owner or in the group would get (ie other permissions). However, if I allow anonymous actions, non-logged in users are able to see all entries, even those where other is set to no permissions (other=['']), which they should not be able to read. I haven't tested, but from looking at the code, AUTHORIZE_ALLOW_ANONYMOUS_ACTIONS will allow all actions for users who are not logged in to do any action (delete, create, update, etc).

Relevant code portion from flask_authorize/plugin.py, lines 336-338

# don't allow anything for anonymous users
        if user is None:
            return current_app.config['AUTHORIZE_ALLOW_ANONYMOUS_ACTIONS']

In the permission check, if 'authorize_allow_anonymous_actions' is True, using authorize.action will always return True (which seems like unwanted behavior to me). This is also inconsistent with what will come out of the DB if you have user = None and do a sqlalchemy query to your db, for example for entries which you are allowed to read. As in, I can do authorize.read(article) and get True on an sqlalchemy article object, but do Article.query.filter(Article.authorized('read')) and not get the article.

I patched this on my own package to get the desired behavior (https://github.com/janash/seamm_dashboard/blob/jwt-authorize/app/authorize_patch.py). I've tried applying this in the package as well, but it causes many tests to fail. Wondering if you see this behavior as intended.

If this is unclear, let me know if you need me to come up with a minimal example.

Thanks!

fix/new group add

Environment:


Name: Flask-Authorize
Version: 0.2.6
            Name: Flask-Authorize
            Version: 0.2.6
            
            Name: Flask
            Version: 2.1.2
            
            Name: Flask-SQLAlchemy
            Version: 2.5.1
            
            Name: six
            Version: 1.16.0
            
            Name: SQLAlchemy
            Version: 1.4.37


Python version: Python 3.8.10

I tried to create Group by using usual way like:

group_admin = Group(name='admin')
group_admin.set_restrictions(can_delete=True)
db.session.add(group_admin)
db.session.commit()

but i got following error:


    restrictions = self.restrictions.copy()
AttributeError: 'NoneType' object has no attribute 'copy'

i think here you are trying to copy restriction variable.. i updated it to __restrictions__ --> restrictions in mixins.py file and it worked fine.

Please guide if this behavior is right.

type object 'Model' has no attribute '_decl_class_registry'

I'm having an issue with the PermissionsMixin model when sqlalchemy is upgraded to version 1.4.x (1.3 works fine). To get this error, install flask-authorize in an environment with sqlalchemy > 1.3 and run pytest. You will see

flask_authorize/mixins.py:196: in default_restrictions
    gather_models()
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

    def gather_models():
        """
        Inspect sqlalchemy models from current context and set global
        dictionary to be used in url conversion.
        """
        global MODELS

        from flask import current_app
        if 'sqlalchemy' not in current_app.extensions:
            return
        check = current_app.config['AUTHORIZE_IGNORE_PROPERTY']

        # inspect current models and add to map
        db = current_app.extensions['sqlalchemy'].db
>       for cls in db.Model._decl_class_registry.values():
E       sqlalchemy.exc.StatementError: (builtins.AttributeError) type object 'Model' has no attribute '_decl_class_registry'
E       [SQL: INSERT INTO groups (id, name, "desc", restrictions) VALUES (?, ?, ?, ?)]
E       [parameters: [{'name': 'readers', 'id': 100, 'desc': None}]]

flask_authorize/mixins.py:101: StatementError

Notably - type object 'Model' has no attribute '_decl_class_registry'

Here's a direct link to the line

for cls in db.Model._decl_class_registry.values():

There must have been a change to sqlalchemy models, but I haven't figured it out yet.

Jinja support

Hi, it looks great, exactly what I was looking for.

Do you plan to support Jinja?

question about adding special permission for single user/group

Hi, I commented last month about the anonymous user behavior - thanks for the quick response on that.

I am now trying to implement more features in my app, and I'm having some trouble. For example, let's say I have a resource which has permissions

__permissions__ = dict(
        owner=['read', 'update', 'delete', 'revoke'],
        group=['read', 'update'],
        other=[]
    )

In other words, this resource is not public. The owner has all permissions, while the group can read or update. I would like to give a specific user (or other group) read permission as well. Is there a built in way to do this using this package?

user_id can only be an Integer

Hi,

So my second issue I've come across with the user_id's is the fact that you expect us to use an Integer.

Initially I started off using normal Integers as Id's, but it creates a very obvious security risk, that in every project user_id 1 will probably be the admin/root user.

I've been reading through several projects, and the most secure ones use UUID as id's for their tables.
easily done by
from sqlalchemy.dialects.postgresql import UUID
import uuid

and in the users table
id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)

I also have not really looked into the code of flask-authorize, but it would be a nice idea, to adopt a more secure approach and give a choice in between Integer or uuid4, which will solve a major security issue of easily guessing user ID's

Need to be able to specify the names of specific tables

Right now the tables must be named "users" and the classes must be named "Group". In our application, we don't follow this standard, we use "GroupModel" to specify classes and we go with singular table names: e.g. "user". I don't want to have to migrate tables.

Thus, i propose to add decorators to be able to set the SQLAlchemy classes that should be used, rather than hardcoding the strings for the tables/class names into the plugin. Once the decorator is used, it can refer to the tablename attribute on the class to get the table name. One decorator should exist for groups, users, and roles. If not provided, default to something sane.

Using authorize.has_role(‘<role>’) as decorator did not raised Unauthorized exception

Unauthorize exception is never raised even though the user didn't have admin role if I use authorize.has_role as decorator like this (documentation says Unauthorize exception should be raised):

@login_required
@authorize.has_role('admin')
def view():
    pass

But, Unauthorize exception is raised properly if user didn't have admin role if I use explicitly like this:

@login_required
def view():
    if not authorize.has_role('admin'):
        raise Unauthorized
    pass

I use Flask-Login and only need a role-based control, so I don't set ACL nor group.

This is my model:

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)

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'))
)

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

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

    roles = db.relationship('Role', secondary=UserRole)

    def get_id(self):
        return self.name

VARCHAR requires a length on dialect mysql

I've set up my application to use MySQL as the database, but when I try to make my Group model extend AllowancesMixin, I get the following error:

Traceback (most recent call last):
  File "/usr/lib64/python3.7/site-packages/sqlalchemy/sql/compiler.py", line 2895, in visit_create_table
    create_column, first_pk=column.primary_key and not first_pk
  File "/usr/lib64/python3.7/site-packages/sqlalchemy/sql/compiler.py", line 350, in process
    return obj._compiler_dispatch(self, **kwargs)
  File "/usr/lib64/python3.7/site-packages/sqlalchemy/sql/visitors.py", line 95, in _compiler_dispatch
    return meth(self, **kw)
  File "/usr/lib64/python3.7/site-packages/sqlalchemy/sql/compiler.py", line 2928, in visit_create_column
    text = self.get_column_specification(column, first_pk=first_pk)
  File "/usr/lib64/python3.7/site-packages/sqlalchemy/dialects/mysql/base.py", line 1562, in get_column_specification
    column.type, type_expression=column
  File "/usr/lib64/python3.7/site-packages/sqlalchemy/sql/compiler.py", line 400, in process
    return type_._compiler_dispatch(self, **kw)
  File "/usr/lib64/python3.7/site-packages/sqlalchemy/sql/visitors.py", line 95, in _compiler_dispatch
    return meth(self, **kw)
  File "/usr/lib64/python3.7/site-packages/sqlalchemy/sql/compiler.py", line 3438, in visit_type_decorator
    return self.process(type_.type_engine(self.dialect), **kw)
  File "/usr/lib64/python3.7/site-packages/sqlalchemy/sql/compiler.py", line 400, in process
    return type_._compiler_dispatch(self, **kw)
  File "/usr/lib64/python3.7/site-packages/sqlalchemy/sql/visitors.py", line 95, in _compiler_dispatch
    return meth(self, **kw)
  File "/usr/lib64/python3.7/site-packages/sqlalchemy/sql/compiler.py", line 3416, in visit_string
    return self.visit_VARCHAR(type_, **kw)
  File "/usr/lib64/python3.7/site-packages/sqlalchemy/dialects/mysql/base.py", line 2035, in visit_VARCHAR
    "VARCHAR requires a length on dialect %s" % self.dialect.name
sqlalchemy.exc.CompileError: VARCHAR requires a length on dialect mysql

It seems that manually editing the mixins.py file from this package and setting the length of the String on the JSON type manually fixes the problem.

Question on maintainers

Hey hey,

Just wondering if this extension is still maintained, we're willing to use, eventually to contribute, wanted to know if thats possible :)

ALl the best,

Suggestion /Sugestao

It would be interesting if there was a group relationship with roles.

group

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)
roles = db.relationship('Role', secondary=GroupRole)

GroupRole = db.Table(
'group_role', db.Model.metadata,
db.Column('group_id', db.Integer, db.ForeignKey('groups.id')),
db.Column('role_id', db.Integer, db.ForeignKey('roles.id'))
)

and modify the has_role method to also look for permissions in groups.

def user_has_role_custon(user, roles):
"""
Check if the specified user has one of the specified roles or
if any of the groups to which the user belongs have the roles.
"""

global STRICT
if not hasattr(user, 'roles') and not hasattr(user, 'groups'):
    return False

# Check roles in groups
if hasattr(user, 'groups'):
    for group in user.groups:
        if hasattr(group, 'roles'):
            for role in group.roles:
                if hasattr(role, 'name'):
                    role_name = role.name
                elif not STRICT:
                    role_name = str(role)
                else:
                    pass
                    # raise AssertionError('Role model has no name property for checking membership.')
                if role_name in roles:
                    return True
# Check user's roles
for role in user.roles:
    if hasattr(role, 'name'):
        role_name = role.name
    elif not STRICT:
        role_name = str(role)
    else:
        raise AssertionError('Role model has no name property for checking membership.')
    if role_name in roles:
        return True

return False

This way, it will look for whether the Group or User has a certain permission.

Ex.
@login_required
@authorize.has_role('can_edit_post')
def edit_post():
pass.

Can't use the same function name in different classes

Using the same function name across different classes results into a buggy behaviour (afaik).
First, the decorator would be called for one of the functions only; and second, the authorization are summed with each other.

Example:

class Invoice:
    @authorize.in_group("sales")
    def post(self):
        pass

class Payment:
    @authorize.in_group("accountants")
    def post(self):
        pass

This results into having the variable AUTHORIZE_CACHE to contain only one 'post' key, while its Authorizer object contains ["sales", "accountants"] in the in_group attribute.
Also, when Invoice.post is called the decorator is called but when Payment.post is called the decorator isn't called.

As it was for #40, using authorize inside the function works properly.

ImportError: cannot import name 'GroupRestrictionsMixin'

Traceback (most recent call last):
File "manage.py", line 5, in
from models import User
File "/xyzy/models.py", line 3, in
from flask_authorize import GroupRestrictionsMixin, RoleRestrictionsMixin
ImportError: cannot import name 'GroupRestrictionsMixin'

Owner ID always null in DB

I am following the documentation. Despite being logged in, I print the user ID to the console, before saving the record in the DB the owner ID is always stored as null.

image

I have copied and pasted the user code from the documentation. I am using the latest version of flask-authorize.

I created a simple app and stored the code on github if it helps to look there.

My 'store' resource is:

class NewStore(Resource):
    @classmethod
    @login_required
    @authorize.create(StoreModel)    
    def post(cls):
        store_json = request.get_json()
        store = store_schema.load(store_json)

        print(current_user.id)

        store.save_to_db()
        return {"message": gettext("STORE_CREATED")}, 201

The 'store' model is:

from datetime import datetime
from typing import List
from flask_authorize import PermissionsMixin

from db import db


class StoreModel(db.Model, PermissionsMixin):
    __tablename__ = "stores"
    __permissions__ = dict(
        owner=['create','read', 'update', 'delete', 'revoke'],
        group=['read', 'update'],
        other=['read']
    )

    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(80), nullable=False)
    description = db.Column(db.String(255))
    created_on = db.Column(db.DateTime, server_default=db.func.now())
    updated_on = db.Column(db.DateTime, onupdate=datetime.now())


    @classmethod
    def find_by_id(cls, _id: int) -> "StoreModel":
        return cls.query.filter_by(id=_id).first()

    @classmethod
    def find_all(cls) -> List["StoreModel"]:
        return cls.query.all()

    def save_to_db(self) -> None:
        db.session.add(self)
        db.session.commit()

    def delete_from_db(self) -> None:
        db.session.delete(self)
        db.session.commit()

I plan eventually on expanding this to test the various possible ways of using flask-authorize.

Thanks for your help, it looks like a great plugin....if I can get it to work! :-)

Unable to add a second user_id relationship

Hi,

I've tried to use both your packages flask-authorize and flask-continuum. I abandoned flask_continuum, because apparently even the maintainer of continuum abandoned it himself.
So to version control my tables I started to use the SQLAlchemy idea of version history SQLAlchemy version history
The only issue is that they don't add a user_id

I found other projects which used this version history, but they added their own "created_by_user" ForeignKey

On other tables where I didn't use any permission mixins, it works fine and I can add 2 user IDs with adding them using *foreign_keys=[user_id]
i.e.
user_id = db.Column(UUID(as_uuid=True), db.ForeignKey("users.id"))
user = db.relationship("User", uselist=False, backref="users", foreign_keys=[user_id])
edit_user_id = db.Column(UUID(as_uuid=True), db.ForeignKey("users.id"))
edit_user = db.relationship("User", foreign_keys=[edit_user_id])

On tables I did use the permissions mixins, it will give the following error

qlalchemy.exc.AmbiguousForeignKeysError: Could not determine join condition between parent/child tables on relationship App.owner - there are multiple foreign key paths linking the tables. Specify the 'foreign_keys' argument, providing a list of those columns which should be counted as containing a foreign key reference to the parent table.

I'm not sure how to solve this as I haven't really gone through your code, perhaps you have an idea?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.