Skip to content

Commit

Permalink
Role-based access control
Browse files Browse the repository at this point in the history
  • Loading branch information
tooxie committed Oct 19, 2014
1 parent 32f1bc7 commit fccc47f
Show file tree
Hide file tree
Showing 11 changed files with 114 additions and 17 deletions.
19 changes: 19 additions & 0 deletions docs/users.rst
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,22 @@ You then need to include that token with your every request:
.. code:: sh
curl http://127.0.0.1:9002/tracks?token=$AUTH_TOKEN
Role-based Access Control
=========================

The concept of Roles is very limited in Shiva. There are 3 possible roles:

* User
* Admin
* Shiva

The first 2 are assigned to users, the last one is only used by other Shiva
instances to communicate with each other. Please note that this functionality
is not yet implemented.

To create a normal user (i.e. either *User* or *Admin* roles) use the command
`shiva-admin user add`.

A role-authentication failure will result in a 401 Forbidden status code.
6 changes: 4 additions & 2 deletions shiva/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@

from docopt import docopt

from shiva.app import app
from shiva.auth import Roles
from shiva.models import db, User
from shiva.utils import get_logger
from shiva.app import app

log = get_logger()

Expand Down Expand Up @@ -137,8 +138,9 @@ def create_user_interactive(email=None, password='', is_public=False,


def mk_user(email, password, is_public, is_active, is_admin):
role = Roles._as_dict().get('ADMIN' if is_admin else 'USER')
user = User(email=email, password=password, is_public=is_public,
is_active=is_active, is_admin=is_admin)
is_active=is_active, role=role)

db.session.add(user)
db.session.commit()
Expand Down
5 changes: 3 additions & 2 deletions shiva/auth/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
from shiva.auth.resource import AuthResource, verify_credentials
from shiva.auth.const import Roles
from shiva.auth.resource import AuthResource, ACLMixin, verify_credentials

__all__ = [AuthResource, verify_credentials]
__all__ = [ACLMixin, AuthResource, Roles, verify_credentials]
19 changes: 19 additions & 0 deletions shiva/auth/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-


class Roles:
USER = '1'
ADMIN = '2'
SHIVA = '3'

@classmethod
def _as_tuple(cls):
return (cls.USER, cls.ADMIN, cls.SHIVA)

@classmethod
def _as_dict(cls):
return {
'USER': cls.USER,
'ADMIN': cls.ADMIN,
'SHIVA': cls.SHIVA,
}
52 changes: 50 additions & 2 deletions shiva/auth/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from flask import g, request
from flask.ext.restful import abort, Resource

from shiva.constants import HTTP
from shiva.models import User


Expand All @@ -18,7 +19,7 @@ def verify_credentials(app):
user = User.verify_auth_token(token)

if not user:
abort(401)
abort(HTTP.UNAUTHORIZED)

g.user = user

Expand All @@ -30,8 +31,55 @@ def post(self):

user = User.query.filter_by(email=email).first()
if not user or not user.verify_password(password):
abort(401)
abort(HTTP.UNAUTHORIZED)

return {
'token': user.generate_auth_token(),
}


class ACLMixin(object):
"""
This mixin enhances Resources with ACL capabilities. Every resource that
contains an `allow` attribute (which should be a list of roles, whose valid
values are defined in `shiva.auth.Roles`) will be authenticated against the
currently logged in user. If no `allow` attribute is present, the resource
is considered to be unrestricted.
This check will be done right before any attribute in the class is called.
In pseudo code, the check is as follows:
.. code::
if ALLOW_ANONYMOUS_ACCESS:
call()
else:
if self.allow:
if g.user.role in self.allow:
call()
else:
call()
abort()
"""

def __getattribute__(self, name):
attr = object.__getattribute__(self, name)
if hasattr(attr, '__call__'):
if hasattr(self, '_allowed') and self._allowed:
return attr

if app.config.get('ALLOW_ANONYMOUS_ACCESS', False):
self._allowed = True
return attr
else:
if hasattr(self, 'allow'):
if g.user.role in self.allow:
self._allowed = True
return attr
else:
self._allowed = True
return attr

abort(HTTP.FORBIDDEN)

return attr
6 changes: 4 additions & 2 deletions shiva/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@
from sqlalchemy.sql.expression import func
from slugify import slugify

from shiva.utils import MetadataManager
from shiva import dbtypes
from shiva.auth import Roles
from shiva.utils import MetadataManager

db = SQLAlchemy()

Expand Down Expand Up @@ -432,7 +433,8 @@ class User(db.Model):
# Should these attributes be in their own table?
is_public = db.Column(db.Boolean, nullable=False, default=False)
is_active = db.Column(db.Boolean, nullable=False, default=False)
is_admin = db.Column(db.Boolean, nullable=False, default=False)
role = db.Column(db.Enum(*Roles._as_tuple()), nullable=False,
default=Roles.USER)
creation_date = db.Column(db.DateTime, nullable=False)

def __init__(self, *args, **kwargs):
Expand Down
7 changes: 5 additions & 2 deletions shiva/resources/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from flask.ext.restful import abort, fields, marshal
from werkzeug.exceptions import NotFound

from shiva.auth import Roles
from shiva.constants import HTTP
from shiva.exceptions import (InvalidFileTypeError, IntegrityError,
ObjectExistsError)
Expand Down Expand Up @@ -529,8 +530,9 @@ def post(self, id=None):
return response, 201, headers

def create(self, display_name, email, password, is_active, is_admin):
role = Roles._as_dict().get('ADMIN' if is_admin else 'USER')
user = User(display_name=display_name, email=email, password=password,
is_active=is_active, is_admin=is_admin)
is_active=is_active, role=role)

db.session.add(user)
db.session.commit()
Expand Down Expand Up @@ -564,7 +566,8 @@ def update(self, user):
user.is_active = parse_bool(request.form.get('is_active'))

if 'is_admin' in request.form:
user.is_admin = parse_bool(request.form.get('is_admin'))
is_admin = parse_bool(request.form.get('is_admin'))
user.role = Roles._as_dict().get('ADMIN' if is_admin else 'USER')

return user

Expand Down
6 changes: 3 additions & 3 deletions tests/integration/admin-test.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
import tempfile
import unittest

from shiva import admin
from shiva import app as shiva
from shiva import admin, app as shiva
from shiva.auth import Roles


class AdminTestCase(unittest.TestCase):
Expand Down Expand Up @@ -40,7 +40,7 @@ def test_user_creation(self):

nose.ok_(user.pk is not None)
nose.eq_(user.is_active, True)
nose.eq_(user.is_admin, False)
nose.eq_(user.role, Roles.USER)

def test_user_activation(self):
params = self.get_payload()
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/auth-test.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def setUp(self):
shiva.db.create_all()

self.user = User(email='derp@mail.com', password='blink182',
is_public=False, is_active=True, is_admin=False)
is_public=False, is_active=True)
shiva.db.session.add(self.user)
shiva.db.session.commit()

Expand Down
3 changes: 2 additions & 1 deletion tests/integration/resource-users-test.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
from nose import tools as nose

from shiva.auth import Roles
from tests.integration.resource import ResourceTestCase


Expand Down Expand Up @@ -50,7 +51,7 @@ def get_payload(self):
'display_name': 'derpina',
'password': 'blink182',
'is_active': True,
'is_admin': False,
'role': Roles.USER,
}

# Unauthorized
Expand Down
6 changes: 4 additions & 2 deletions tests/integration/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from flask import json

from shiva import app as shiva
from shiva.auth import Roles
from shiva.converter import Converter
from shiva.models import Artist, Album, Track, User
from shiva.resources.upload import UploadHandler
Expand Down Expand Up @@ -57,7 +58,7 @@ def setUp(self):
hash_file=False, no_metadata=True)
self.album.artists.append(self.artist)
self.user = User(email='derp@mail.com', password='blink182',
is_public=True, is_active=True, is_admin=False)
is_public=True, is_active=True)

shiva.db.session.add(self.artist)
shiva.db.session.add(self.album)
Expand Down Expand Up @@ -88,8 +89,9 @@ def mk_user(self, is_public=True, is_active=True, is_admin=False):
email = str(random.random())
password = str(random.random())

role = Roles._as_dict().get('ADMIN' if is_admin else 'USER')
user = User(email=email, password=password, is_public=is_public,
is_active=is_active, is_admin=is_admin)
is_active=is_active, role=role)

self._db.session.add(user)
self._db.session.commit()
Expand Down

0 comments on commit fccc47f

Please sign in to comment.