Implemented badger service endpoint

Also added manage.py command to create badger service accounts.
This commit is contained in:
Sybren A. Stüvel 2016-05-30 15:42:57 +02:00
parent 4aa44c42c8
commit 222d9efc89
7 changed files with 287 additions and 4 deletions

View File

@ -196,6 +196,7 @@ from modules import users
from modules import nodes from modules import nodes
from modules import latest from modules import latest
from modules import blender_cloud from modules import blender_cloud
from modules import service
app.register_blueprint(encoding, url_prefix='/encoding') app.register_blueprint(encoding, url_prefix='/encoding')
app.register_blueprint(blender_id, url_prefix='/blender_id') app.register_blueprint(blender_id, url_prefix='/blender_id')
@ -205,4 +206,5 @@ file_storage.setup_app(app, url_prefix='/storage')
latest.setup_app(app, url_prefix='/latest') latest.setup_app(app, url_prefix='/latest')
blender_cloud.setup_app(app, url_prefix='/bcloud') blender_cloud.setup_app(app, url_prefix='/bcloud')
users.setup_app(app, url_prefix='/users') users.setup_app(app, url_prefix='/users')
service.setup_app(app, url_prefix='/service')
nodes.setup_app(app) nodes.setup_app(app)

View File

@ -0,0 +1,102 @@
"""Service accounts."""
import logging
from bson import ObjectId
from flask import Blueprint, current_app, g, request
from werkzeug import exceptions as wz_exceptions
from application.utils import authorization
from application.modules import local_auth
blueprint = Blueprint('service', __name__)
log = logging.getLogger(__name__)
@blueprint.route('/badger', methods=['POST'])
@authorization.require_login(require_roles={u'service', u'badger'}, require_all=True)
def badger():
if request.mimetype != 'application/json':
raise wz_exceptions.BadRequest()
# Parse the request
args = request.json
action = args['action']
user_email = args['user_email']
role = args['role']
if action not in {'grant', 'revoke'}:
raise wz_exceptions.BadRequest('Action %r not supported' % action)
log.info('Service account %s %ss role %r to/from user %s',
g.current_user['user_id'], action, role, user_email)
users_coll = current_app.data.driver.db['users']
# Check that the user is allowed to grant this role.
srv_user = users_coll.find_one(g.current_user['user_id'],
projection={'service.badger': 1})
if srv_user is None:
log.error('badger(%s, %s, %s): current user %s not found -- how did they log in?',
action, user_email, role, g.current_user['user_id'])
return 'User not found', 403
allowed_roles = set(srv_user.get('service', {}).get('badger', []))
if role not in allowed_roles:
log.warning('badger(%s, %s, %s): service account not authorized to %s role %s',
action, user_email, role, action, role)
return 'Role not allowed', 403
# Fetch the user
db_user = users_coll.find_one({'email': user_email}, projection={'roles': 1})
if db_user is None:
log.warning('badger(%s, %s, %s): user not found', action, user_email, role)
return 'User not found', 404
# Apply the action
roles = set(db_user['roles'] or [])
if action == 'grant':
roles.add(role)
else:
roles.discard(role)
users_coll.update_one({'_id': db_user['_id']},
{'$set': {'roles': list(roles)}})
return '', 204
def create_service_account(email, roles, service):
"""Creates a service account with the given roles + the role 'service'.
:param email: email address associated with the account
:type email: str
:param roles: iterable of role names
:param service: dict of the 'service' key in the user.
:type service: dict
:return: tuple (user doc, token doc)
"""
from eve.methods.post import post_internal
# Create a user with the correct roles.
roles = list(set(roles).union({u'service'}))
user = {'username': email,
'groups': [],
'roles': roles,
'settings': {'email_communications': 0},
'auth': [],
'full_name': email,
'email': email,
'service': service}
result, _, _, status = post_internal('users', user)
if status != 201:
raise SystemExit('Error creating user {}: {}'.format(email, result))
user.update(result)
# Create an authentication token that won't expire for a long time.
token = local_auth.generate_and_store_token(user['_id'], days=36500, prefix='SRV')
return user, token
def setup_app(app, url_prefix):
app.register_blueprint(blueprint, url_prefix=url_prefix)

View File

@ -232,15 +232,26 @@ def merge_permissions(*args):
return effective return effective
def require_login(require_roles=set()): def require_login(require_roles=set(),
require_all=False):
"""Decorator that enforces users to authenticate. """Decorator that enforces users to authenticate.
Optionally only allows access to users with a certain role./ Optionally only allows access to users with a certain role.
:param require_roles: set of roles.
:param require_all:
When False (the default): if the user's roles have a
non-empty intersection with the given roles, access is granted.
When True: require the user to have all given roles before access is
granted.
""" """
if not isinstance(require_roles, set): if not isinstance(require_roles, set):
raise TypeError('require_roles param should be a set, but is a %r' % type(require_roles)) raise TypeError('require_roles param should be a set, but is a %r' % type(require_roles))
if require_all and not require_roles:
raise ValueError('require_login(require_all=True) cannot be used with empty require_roles.')
def decorator(func): def decorator(func):
@functools.wraps(func) @functools.wraps(func)
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
@ -253,7 +264,13 @@ def require_login(require_roles=set()):
log.debug('Unauthenticated acces to %s attempted.', func) log.debug('Unauthenticated acces to %s attempted.', func)
abort(403) abort(403)
if require_roles and not require_roles.intersection(set(current_user['roles'])): intersection = require_roles.intersection(set(current_user['roles']))
if require_all:
if intersection != require_roles:
log.warning('User %s does not have ALL required roles %s to access %s',
current_user['user_id'], require_roles, func)
abort(403)
elif require_roles and not intersection:
log.warning('User %s is authenticated, but does not have any required role %s to ' log.warning('User %s is authenticated, but does not have any required role %s to '
'access %s', current_user['user_id'], require_roles, func) 'access %s', current_user['user_id'], require_roles, func)
abort(403) abort(403)

View File

@ -781,5 +781,31 @@ def update_texture_nodes_maps():
nodes_collection.update({'_id': node['_id']}, node) nodes_collection.update({'_id': node['_id']}, node)
@manager.command
def create_badger_account(email, badges):
"""
Creates a new service account that can give badges (i.e. roles).
:param email: email address associated with the account
:param badges: single space-separated argument containing the roles
this account can assign and revoke.
"""
from application.modules import service
from application.utils import dumps
account, token = service.create_service_account(
email,
[u'badger'],
{'badger': badges.strip().split()}
)
print('Account created:')
print(dumps(account, indent=4, sort_keys=True))
print()
print('Access token: %s' % token['token'])
print(' expires on: %s' % token['expire_time'])
if __name__ == '__main__': if __name__ == '__main__':
manager.run() manager.run()

View File

@ -62,7 +62,7 @@ users_schema = {
}, },
'roles': { 'roles': {
'type': 'list', 'type': 'list',
'allowed': ["admin", "subscriber", "demo"], 'schema': {'type': 'string'}
}, },
'groups': { 'groups': {
'type': 'list', 'type': 'list',
@ -107,6 +107,15 @@ users_schema = {
'allowed': [0, 1] 'allowed': [0, 1]
} }
} }
},
'service': {
'type': 'dict',
'schema': {
'badger': {
'type': 'list',
'schema': {'type': 'string'}
}
}
} }
} }

View File

@ -497,3 +497,79 @@ class PermissionComputationTest(AbstractPillarTest):
u'methods': [u'GET']}], u'methods': [u'GET']}],
u'world': [u'GET']}, u'world': [u'GET']},
self.sort(compute_aggr_permissions('nodes', node, None))) self.sort(compute_aggr_permissions('nodes', node, None)))
class RequireRolesTest(AbstractPillarTest):
def test_no_roles_required(self):
from flask import g
from application.utils.authorization import require_login
called = [False]
@require_login()
def call_me():
called[0] = True
with self.app.test_request_context():
g.current_user = {'user_id': ObjectId(24*'a'),
'roles': [u'succubus']}
call_me()
self.assertTrue(called[0])
def test_some_roles_required(self):
from flask import g
from application.utils.authorization import require_login
called = [False]
@require_login(require_roles={u'admin'})
def call_me():
called[0] = True
with self.app.test_request_context():
g.current_user = {'user_id': ObjectId(24*'a'),
'roles': [u'succubus']}
self.assertRaises(Forbidden, call_me)
self.assertFalse(called[0])
with self.app.test_request_context():
g.current_user = {'user_id': ObjectId(24*'a'),
'roles': [u'admin']}
call_me()
self.assertTrue(called[0])
def test_all_roles_required(self):
from flask import g
from application.utils.authorization import require_login
called = [False]
@require_login(require_roles={u'service', u'badger'},
require_all=True)
def call_me():
called[0] = True
with self.app.test_request_context():
g.current_user = {'user_id': ObjectId(24*'a'),
'roles': [u'admin']}
self.assertRaises(Forbidden, call_me)
self.assertFalse(called[0])
with self.app.test_request_context():
g.current_user = {'user_id': ObjectId(24*'a'),
'roles': [u'service']}
self.assertRaises(Forbidden, call_me)
self.assertFalse(called[0])
with self.app.test_request_context():
g.current_user = {'user_id': ObjectId(24*'a'),
'roles': [u'badger']}
self.assertRaises(Forbidden, call_me)
self.assertFalse(called[0])
with self.app.test_request_context():
g.current_user = {'user_id': ObjectId(24*'a'),
'roles': [u'service', u'badger']}
call_me()
self.assertTrue(called[0])

View File

@ -0,0 +1,51 @@
"""Test badger service."""
from common_test_class import AbstractPillarTest, TEST_EMAIL_ADDRESS
class BadgerServiceTest(AbstractPillarTest):
def setUp(self, **kwargs):
AbstractPillarTest.setUp(self, **kwargs)
from application.modules import service
with self.app.test_request_context():
self.badger, token_doc = service.create_service_account(
'serviceaccount@example.com', [u'badger'], {u'badger': [u'succubus']}
)
self.badger_token = token_doc['token']
self.user_id = self.create_user()
self.user_email = TEST_EMAIL_ADDRESS
def _post(self, data):
from application.utils import dumps
return self.client.post('/service/badger',
data=dumps(data),
headers={'Authorization': self.make_header(self.badger_token),
'Content-Type': 'application/json'})
def test_grant_revoke_badge(self):
# Grant the badge
resp = self._post({'action': 'grant', 'user_email': self.user_email, 'role': 'succubus'})
self.assertEqual(204, resp.status_code)
with self.app.test_request_context():
user = self.app.data.driver.db['users'].find_one(self.user_id)
self.assertIn(u'succubus', user['roles'])
# Aaaahhhw it's gone again
resp = self._post({'action': 'revoke', 'user_email': self.user_email, 'role': 'succubus'})
self.assertEqual(204, resp.status_code)
with self.app.test_request_context():
user = self.app.data.driver.db['users'].find_one(self.user_id)
self.assertNotIn(u'succubus', user['roles'])
def test_grant_not_allowed_badge(self):
resp = self._post({'action': 'grant', 'user_email': self.user_email, 'role': 'admin'})
self.assertEqual(403, resp.status_code)
with self.app.test_request_context():
user = self.app.data.driver.db['users'].find_one(self.user_id)
self.assertNotIn(u'admin', user['roles'])