Implemented badger service endpoint
Also added manage.py command to create badger service accounts.
This commit is contained in:
parent
4aa44c42c8
commit
222d9efc89
@ -196,6 +196,7 @@ from modules import users
|
||||
from modules import nodes
|
||||
from modules import latest
|
||||
from modules import blender_cloud
|
||||
from modules import service
|
||||
|
||||
app.register_blueprint(encoding, url_prefix='/encoding')
|
||||
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')
|
||||
blender_cloud.setup_app(app, url_prefix='/bcloud')
|
||||
users.setup_app(app, url_prefix='/users')
|
||||
service.setup_app(app, url_prefix='/service')
|
||||
nodes.setup_app(app)
|
||||
|
102
pillar/application/modules/service.py
Normal file
102
pillar/application/modules/service.py
Normal 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)
|
@ -232,15 +232,26 @@ def merge_permissions(*args):
|
||||
return effective
|
||||
|
||||
|
||||
def require_login(require_roles=set()):
|
||||
def require_login(require_roles=set(),
|
||||
require_all=False):
|
||||
"""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):
|
||||
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):
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
@ -253,7 +264,13 @@ def require_login(require_roles=set()):
|
||||
log.debug('Unauthenticated acces to %s attempted.', func)
|
||||
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 '
|
||||
'access %s', current_user['user_id'], require_roles, func)
|
||||
abort(403)
|
||||
|
@ -781,5 +781,31 @@ def update_texture_nodes_maps():
|
||||
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__':
|
||||
manager.run()
|
||||
|
@ -62,7 +62,7 @@ users_schema = {
|
||||
},
|
||||
'roles': {
|
||||
'type': 'list',
|
||||
'allowed': ["admin", "subscriber", "demo"],
|
||||
'schema': {'type': 'string'}
|
||||
},
|
||||
'groups': {
|
||||
'type': 'list',
|
||||
@ -107,6 +107,15 @@ users_schema = {
|
||||
'allowed': [0, 1]
|
||||
}
|
||||
}
|
||||
},
|
||||
'service': {
|
||||
'type': 'dict',
|
||||
'schema': {
|
||||
'badger': {
|
||||
'type': 'list',
|
||||
'schema': {'type': 'string'}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -497,3 +497,79 @@ class PermissionComputationTest(AbstractPillarTest):
|
||||
u'methods': [u'GET']}],
|
||||
u'world': [u'GET']},
|
||||
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])
|
||||
|
51
tests/test_service_badger.py
Normal file
51
tests/test_service_badger.py
Normal 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'])
|
Loading…
x
Reference in New Issue
Block a user