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 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)
|
||||||
|
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
|
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)
|
||||||
|
@ -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()
|
||||||
|
@ -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'}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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])
|
||||||
|
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