From db3bf045a8a9bb75ad1641ac3e0a7e55fbfa0db6 Mon Sep 17 00:00:00 2001 From: Chenxiong Qi Date: Jul 17 2017 09:32:32 +0000 Subject: [PATCH 1/2] Protect resource by requiring login Signed-off-by: Chenxiong Qi --- diff --git a/conf/config.py b/conf/config.py index b958da5..f2a9bea 100644 --- a/conf/config.py +++ b/conf/config.py @@ -89,6 +89,8 @@ class DevConfiguration(BaseConfiguration): except: pass + # Disable login_required in development environment + LOGIN_DISABLED = True AUTH_BACKEND = 'noauth' AUTH_OPENIDC_USERINFO_URI = 'https://iddev.fedorainfracloud.org/openidc/UserInfo' diff --git a/odcs/__init__.py b/odcs/__init__.py index 8ac7ee6..0862f7a 100644 --- a/odcs/__init__.py +++ b/odcs/__init__.py @@ -25,6 +25,7 @@ from logging import getLogger from flask import Flask, jsonify from flask_sqlalchemy import SQLAlchemy +from flask_login import LoginManager from odcs.logger import init_logging from odcs.config import init_config @@ -40,10 +41,13 @@ conf = init_config(app) init_logging(conf) log = getLogger(__name__) +login_manager = LoginManager() +login_manager.init_app(app) + from odcs import views from odcs.auth import init_auth -init_auth(app, backend=conf.auth_backend) +init_auth(login_manager, conf.auth_backend) def json_error(status, error, message): response = jsonify( diff --git a/odcs/auth.py b/odcs/auth.py index a247376..1403ec4 100644 --- a/odcs/auth.py +++ b/odcs/auth.py @@ -29,7 +29,6 @@ from itertools import chain from flask import abort from flask import g -from flask import request from odcs import conf, log from odcs.models import User @@ -37,7 +36,7 @@ from odcs.models import commit_on_success @commit_on_success -def load_krb_user_from_request(): +def load_krb_user_from_request(request): """Load Kerberos user from current request REMOTE_USER needs to be set in environment variable, that is set by @@ -62,6 +61,7 @@ def load_krb_user_from_request(): g.groups = groups g.user = user + return user def query_ldap_groups(uid): @@ -82,7 +82,7 @@ def query_ldap_groups(uid): @commit_on_success -def load_openidc_user(): +def load_openidc_user(request): """Load FAS user from current request""" username = request.environ.get('REMOTE_USER') if not username: @@ -105,6 +105,7 @@ def load_openidc_user(): g.groups = user_info.get('groups', []) g.user = user + return user def validate_scopes(scope): @@ -132,7 +133,7 @@ def get_user_info(token): return r.json() -def init_auth(app, backend): +def init_auth(login_manager, backend): """Initialize authentication backend Enable and initialize authentication backend to work with frontend @@ -144,10 +145,11 @@ def init_auth(app, backend): return if backend == 'kerberos': global load_krb_user_from_request - load_krb_user_from_request = app.before_request(load_krb_user_from_request) + load_krb_user_from_request = login_manager.request_loader( + load_krb_user_from_request) elif backend == 'openidc': global load_openidc_user - load_openidc_user = app.before_request(load_openidc_user) + load_openidc_user = login_manager.request_loader(load_openidc_user) else: raise ValueError('Unknown backend name {0}.'.format(backend)) diff --git a/odcs/models.py b/odcs/models.py index 1c15f56..c11caf6 100644 --- a/odcs/models.py +++ b/odcs/models.py @@ -29,6 +29,7 @@ import os from datetime import datetime, timedelta from odcs import conf from sqlalchemy.orm import validates +from flask_login import UserMixin from odcs import db @@ -79,7 +80,7 @@ class ODCSBase(db.Model): __abstract__ = True -class User(ODCSBase): +class User(ODCSBase, UserMixin): """User information table""" __tablename__ = 'users' diff --git a/odcs/views.py b/odcs/views.py index 6b64649..434c0f0 100644 --- a/odcs/views.py +++ b/odcs/views.py @@ -26,6 +26,7 @@ import json from flask.views import MethodView from flask import request, jsonify +from flask_login import login_required from odcs import app, db, log, conf from odcs.errors import NotFound, BadRequest @@ -82,6 +83,7 @@ class ODCSAPI(MethodView): else: raise NotFound('No such compose found.') + @login_required def post(self): owner = "Unknown" # TODO @@ -168,6 +170,7 @@ class ODCSAPI(MethodView): return jsonify(compose.json()), 200 + @login_required def delete(self, id): compose = Compose.query.filter_by(id=id).first() if compose: diff --git a/requirements.txt b/requirements.txt index ace5a03..23c52d2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,6 +15,7 @@ Flask Flask-Migrate Flask-SQLAlchemy Flask-Script +Flask-Login==0.4.0 requests jinja2 pyldap diff --git a/tests/test_auth.py b/tests/test_auth.py index b129102..02f328d 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -57,7 +57,7 @@ class TestLoadKrbUserFromRequest(ModelsBaseTest): } with app.test_request_context(environ_base=environ_base): - load_krb_user_from_request() + load_krb_user_from_request(flask.request) expected_user = db.session.query(User).filter( User.username == 'newuser')[0] @@ -79,19 +79,17 @@ class TestLoadKrbUserFromRequest(ModelsBaseTest): } with app.test_request_context(environ_base=environ_base): - load_krb_user_from_request() + load_krb_user_from_request(flask.request) self.assertEqual(original_users_count, db.session.query(User.id).count()) self.assertEqual(self.user.id, flask.g.user.id) self.assertEqual(self.user.username, flask.g.user.username) self.assertEqual(['admins', 'devel'], sorted(flask.g.groups)) - @patch('odcs.auth.request') - @patch('odcs.auth.g') - def test_401_if_remote_user_not_present(self, g, request): - request.environ.get.return_value = None - - self.assertRaises(Unauthorized, load_krb_user_from_request) + def test_401_if_remote_user_not_present(self): + with app.test_request_context(): + self.assertRaises(Unauthorized, + load_krb_user_from_request, flask.request) class TestLoadOpenIDCUserFromRequest(ModelsBaseTest): @@ -119,7 +117,7 @@ class TestLoadOpenIDCUserFromRequest(ModelsBaseTest): } with app.test_request_context(environ_base=environ_base): - load_openidc_user() + load_openidc_user(flask.request) new_user = db.session.query(User).filter(User.username == 'new_user')[0] @@ -145,7 +143,7 @@ class TestLoadOpenIDCUserFromRequest(ModelsBaseTest): with app.test_request_context(environ_base=environ_base): original_users_count = db.session.query(User.id).count() - load_openidc_user() + load_openidc_user(flask.request) users_count = db.session.query(User.id).count() self.assertEqual(original_users_count, users_count) @@ -162,7 +160,7 @@ class TestLoadOpenIDCUserFromRequest(ModelsBaseTest): 'OIDC_CLAIM_scope': 'openid https://id.fedoraproject.org/scope/groups', } with app.test_request_context(environ_base=environ_base): - self.assertRaises(Unauthorized, load_openidc_user) + self.assertRaises(Unauthorized, load_openidc_user, flask.request) def test_401_if_access_token_not_present(self): environ_base = { @@ -172,7 +170,7 @@ class TestLoadOpenIDCUserFromRequest(ModelsBaseTest): 'OIDC_CLAIM_scope': 'openid https://id.fedoraproject.org/scope/groups', } with app.test_request_context(environ_base=environ_base): - self.assertRaises(Unauthorized, load_openidc_user) + self.assertRaises(Unauthorized, load_openidc_user, flask.request) def test_401_if_scope_not_present(self): environ_base = { @@ -182,7 +180,7 @@ class TestLoadOpenIDCUserFromRequest(ModelsBaseTest): # Missing OIDC_CLAIM_scope here } with app.test_request_context(environ_base=environ_base): - self.assertRaises(Unauthorized, load_openidc_user) + self.assertRaises(Unauthorized, load_openidc_user, flask.request) def test_401_if_required_scope_not_present_in_token_scope(self): environ_base = { @@ -198,7 +196,7 @@ class TestLoadOpenIDCUserFromRequest(ModelsBaseTest): self.assertRaisesRegexp( Unauthorized, 'Required OIDC scope new-compose not present.', - load_openidc_user) + load_openidc_user, flask.request) class TestQueryLdapGroups(unittest.TestCase): @@ -223,23 +221,22 @@ class TestQueryLdapGroups(unittest.TestCase): class TestInitAuth(unittest.TestCase): """Test init_auth""" + def setUp(self): + self.login_manager = Mock() + def test_select_kerberos_auth_backend(self): - app = Mock() - init_auth(app, 'kerberos') - app.before_request.assert_called_once_with(load_krb_user_from_request) + init_auth(self.login_manager, 'kerberos') + self.login_manager.request_loader.assert_called_once_with(load_krb_user_from_request) def test_select_openidc_auth_backend(self): - app = Mock() - init_auth(app, 'openidc') - app.before_request.assert_called_once_with(load_openidc_user) + init_auth(self.login_manager, 'openidc') + self.login_manager.request_loader.assert_called_once_with(load_openidc_user) def test_not_use_auth_backend(self): - app = Mock() - init_auth(app, 'noauth') - app.before_request.assert_not_called() + init_auth(self.login_manager, 'noauth') + self.login_manager.request_loader.assert_not_called() def test_error_if_select_an_unknown_backend(self): - app = Mock() - self.assertRaises(ValueError, init_auth, app, 'xxx') - self.assertRaises(ValueError, init_auth, app, '') - self.assertRaises(ValueError, init_auth, app, None) + self.assertRaises(ValueError, init_auth, self.login_manager, 'xxx') + self.assertRaises(ValueError, init_auth, self.login_manager, '') + self.assertRaises(ValueError, init_auth, self.login_manager, None) diff --git a/tests/test_views.py b/tests/test_views.py index 2bb73bb..f2cf1be 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -25,11 +25,16 @@ import unittest import json from freezegun import freeze_time -from odcs import db, app -from odcs.models import Compose, COMPOSE_STATES, COMPOSE_RESULTS +from odcs import db, app, login_manager +from odcs.models import Compose, COMPOSE_STATES, COMPOSE_RESULTS, User from odcs.pungi import PungiSourceType +@login_manager.user_loader +def user_loader(username): + return User(id=1, username=username) + + class TestViews(unittest.TestCase): maxDiff = None @@ -58,7 +63,14 @@ class TestViews(unittest.TestCase): db.drop_all() db.session.commit() + def login_user(self): + with self.client.session_transaction() as sess: + sess['user_id'] = 'tester' + sess['_fresh'] = True + def test_submit_build(self): + self.login_user() + rv = self.client.post('/odcs/1/composes/', data=json.dumps( {'source_type': 'module', 'source': 'testmodule-master'})) data = json.loads(rv.data.decode('utf8')) @@ -78,6 +90,8 @@ class TestViews(unittest.TestCase): self.assertEqual(c.state, COMPOSE_STATES["wait"]) def test_submit_build_nodeps(self): + self.login_user() + rv = self.client.post('/odcs/1/composes/', data=json.dumps( {'source_type': 'tag', 'source': 'f26', 'packages': ['ed'], 'flags': ['no_deps']})) @@ -93,6 +107,8 @@ class TestViews(unittest.TestCase): self.c1.state = COMPOSE_STATES["removed"] self.c1.reused_id = 1 db.session.commit() + + self.login_user() rv = self.client.post('/odcs/1/composes/', data=json.dumps({'id': 1})) data = json.loads(rv.data.decode('utf8')) @@ -108,6 +124,9 @@ class TestViews(unittest.TestCase): self.c1.state = COMPOSE_STATES["failed"] self.c1.reused_id = 1 db.session.commit() + + self.login_user() + rv = self.client.post('/odcs/1/composes/', data=json.dumps({'id': 1})) data = json.loads(rv.data.decode('utf8')) @@ -120,6 +139,8 @@ class TestViews(unittest.TestCase): self.assertEqual(c.reused_id, None) def test_submit_build_resurrection_no_removed(self): + self.login_user() + db.session.commit() rv = self.client.post('/odcs/1/composes/', data=json.dumps({'id': 1})) data = json.loads(rv.data.decode('utf8')) @@ -127,6 +148,8 @@ class TestViews(unittest.TestCase): self.assertEqual(data['message'], 'No expired or failed compose with id 1') def test_submit_build_resurrection_not_found(self): + self.login_user() + db.session.commit() rv = self.client.post('/odcs/1/composes/', data=json.dumps({'id': 100})) data = json.loads(rv.data.decode('utf8')) @@ -175,6 +198,8 @@ class TestViews(unittest.TestCase): self.assertEqual(len(evs), 1) def test_delete_compose(self): + self.login_user() + with freeze_time(self.initial_datetime) as frozen_datetime: c3 = Compose.create( db.session, "unknown", PungiSourceType.MODULE, "testmodule-master", @@ -202,6 +227,8 @@ class TestViews(unittest.TestCase): self.assertEqual(expired_compose.id, c3.id) def test_delete_not_allowed_states_compose(self): + self.login_user() + for state in COMPOSE_STATES.keys(): if state not in ['done', 'failed']: new_c = Compose.create( @@ -220,6 +247,8 @@ class TestViews(unittest.TestCase): self.assertEqual(data['error'], 'Bad Request') def test_delete_non_exist_compose(self): + self.login_user() + resp = self.client.delete("/odcs/1/composes/999999") data = json.loads(resp.data.decode('utf8')) From bfe56c842031746b43406c750f72d9c940011050 Mon Sep 17 00:00:00 2001 From: Chenxiong Qi Date: Jul 17 2017 13:43:44 +0000 Subject: [PATCH 2/2] Authorize request to POST and DELETE endpoints Authorization is based on groups got when user's request is authenticated. In development environment, it would be convenient to disable it by setting AUTHORIZE_DISABLED to True. Signed-off-by: Chenxiong Qi --- diff --git a/conf/config.py b/conf/config.py index f2a9bea..0ac48b2 100644 --- a/conf/config.py +++ b/conf/config.py @@ -91,6 +91,9 @@ class DevConfiguration(BaseConfiguration): # Disable login_required in development environment LOGIN_DISABLED = True + # Disable authorize in development environment + AUTHORIZE_DISABLED = True + AUTH_BACKEND = 'noauth' AUTH_OPENIDC_USERINFO_URI = 'https://iddev.fedorainfracloud.org/openidc/UserInfo' diff --git a/odcs/auth.py b/odcs/auth.py index 1403ec4..bb5b14b 100644 --- a/odcs/auth.py +++ b/odcs/auth.py @@ -24,6 +24,7 @@ import requests import ldap +import flask from itertools import chain @@ -161,4 +162,4 @@ def user_in_allowed_groups(): returned. :rtype: bool """ - return bool(set(g.groups) & set(conf.allowed_groups)) + return bool(set(flask.g.groups) & set(conf.allowed_groups)) diff --git a/odcs/config.py b/odcs/config.py index 503625b..a965942 100644 --- a/odcs/config.py +++ b/odcs/config.py @@ -179,6 +179,10 @@ class Config(object): 'type': list, 'default': [], 'desc': 'Required scopes for submitting request to run new compose.'}, + 'authorize_disabled': { + 'type': bool, + 'default': False, + 'desc': 'Disable group based authorization.'}, } def __init__(self, conf_section_obj): diff --git a/odcs/views.py b/odcs/views.py index 434c0f0..10fb61d 100644 --- a/odcs/views.py +++ b/odcs/views.py @@ -24,8 +24,10 @@ import datetime import json +import flask + from flask.views import MethodView -from flask import request, jsonify +from flask import request, jsonify, abort from flask_login import login_required from odcs import app, db, log, conf @@ -33,6 +35,19 @@ from odcs.errors import NotFound, BadRequest from odcs.models import Compose, COMPOSE_RESULTS, COMPOSE_FLAGS, COMPOSE_STATES from odcs.pungi import PungiSourceType from odcs.api_utils import pagination_metadata, filter_composes +from odcs.auth import user_in_allowed_groups as _user_in_allowed_groups + + +def user_in_allowed_groups(func): + """Only allow user who is in allowed groups to call endpoint""" + def _decorator(*args, **kwargs): + if conf.authorize_disabled: + return func(*args, **kwargs) + if _user_in_allowed_groups(): + return func(*args, **kwargs) + abort(401, 'User {0} is not in allowed groups.'.format( + flask.g.user.username)) + return _decorator api_v1 = { @@ -84,6 +99,7 @@ class ODCSAPI(MethodView): raise NotFound('No such compose found.') @login_required + @user_in_allowed_groups def post(self): owner = "Unknown" # TODO @@ -171,6 +187,7 @@ class ODCSAPI(MethodView): return jsonify(compose.json()), 200 @login_required + @user_in_allowed_groups def delete(self, id): compose = Compose.query.filter_by(id=id).first() if compose: diff --git a/tests/test_auth.py b/tests/test_auth.py index 02f328d..fb527f9 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -240,3 +240,34 @@ class TestInitAuth(unittest.TestCase): self.assertRaises(ValueError, init_auth, self.login_manager, 'xxx') self.assertRaises(ValueError, init_auth, self.login_manager, '') self.assertRaises(ValueError, init_auth, self.login_manager, None) + + +class TestUserInAllowedGroups(unittest.TestCase): + """Test user_in_allowed_groups""" + + @patch.object(odcs.auth.conf, 'allowed_groups', new=[]) + def test_not_allow_when_allowed_groups_is_empty(self): + with app.test_request_context(): + flask.g.groups = [] + self.assertFalse(odcs.auth.user_in_allowed_groups()) + + flask.g.groups = ['testers'] + self.assertFalse(odcs.auth.user_in_allowed_groups()) + + @patch.object(odcs.auth.conf, 'allowed_groups', new=['admins', 'testers']) + def test_not_allow_when_not_in_allowed_groups(self): + with app.test_request_context(): + flask.g.groups = ['common'] + self.assertFalse(odcs.auth.user_in_allowed_groups()) + + @patch.object(odcs.auth.conf, 'allowed_groups', new=['admins', 'testers']) + def test_allow_when_in_allowed_groups(self): + with app.test_request_context(): + flask.g.groups = ['common', 'testers'] + self.assertTrue(odcs.auth.user_in_allowed_groups()) + + flask.g.groups = ['common', 'testers', 'admins'] + self.assertTrue(odcs.auth.user_in_allowed_groups()) + + flask.g.groups = ['testers', 'admins'] + self.assertTrue(odcs.auth.user_in_allowed_groups()) diff --git a/tests/test_views.py b/tests/test_views.py index f2cf1be..683839e 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -23,11 +23,19 @@ import datetime import unittest import json + +import flask + from freezegun import freeze_time +from mock import Mock, patch +from werkzeug.exceptions import Unauthorized + +import odcs.auth from odcs import db, app, login_manager from odcs.models import Compose, COMPOSE_STATES, COMPOSE_RESULTS, User from odcs.pungi import PungiSourceType +from odcs.views import user_in_allowed_groups @login_manager.user_loader @@ -39,6 +47,11 @@ class TestViews(unittest.TestCase): maxDiff = None def setUp(self): + self.patch_allowed_groups = patch.object(odcs.auth.conf, + 'allowed_groups', + new=['tester']) + self.patch_allowed_groups.start() + self.client = app.test_client() db.session.remove() db.drop_all() @@ -63,6 +76,8 @@ class TestViews(unittest.TestCase): db.drop_all() db.session.commit() + self.patch_allowed_groups.stop() + def login_user(self): with self.client.session_transaction() as sess: sess['user_id'] = 'tester' @@ -71,9 +86,12 @@ class TestViews(unittest.TestCase): def test_submit_build(self): self.login_user() - rv = self.client.post('/odcs/1/composes/', data=json.dumps( - {'source_type': 'module', 'source': 'testmodule-master'})) - data = json.loads(rv.data.decode('utf8')) + with app.test_request_context(): + flask.g.groups = ['tester'] + + rv = self.client.post('/odcs/1/composes/', data=json.dumps( + {'source_type': 'module', 'source': 'testmodule-master'})) + data = json.loads(rv.data.decode('utf8')) expected_json = {'source_type': 2, 'state': 0, 'time_done': None, 'state_name': 'wait', 'source': u'testmodule-master', @@ -92,10 +110,13 @@ class TestViews(unittest.TestCase): def test_submit_build_nodeps(self): self.login_user() - rv = self.client.post('/odcs/1/composes/', data=json.dumps( - {'source_type': 'tag', 'source': 'f26', 'packages': ['ed'], - 'flags': ['no_deps']})) - data = json.loads(rv.data.decode('utf8')) + with app.test_request_context(): + flask.g.groups = ['tester'] + + rv = self.client.post('/odcs/1/composes/', data=json.dumps( + {'source_type': 'tag', 'source': 'f26', 'packages': ['ed'], + 'flags': ['no_deps']})) + data = json.loads(rv.data.decode('utf8')) self.assertEqual(data['flags'], ['no_deps']) @@ -109,8 +130,12 @@ class TestViews(unittest.TestCase): db.session.commit() self.login_user() - rv = self.client.post('/odcs/1/composes/', data=json.dumps({'id': 1})) - data = json.loads(rv.data.decode('utf8')) + + with app.test_request_context(): + flask.g.groups = ['tester'] + + rv = self.client.post('/odcs/1/composes/', data=json.dumps({'id': 1})) + data = json.loads(rv.data.decode('utf8')) self.assertEqual(data['id'], 3) self.assertEqual(data['state_name'], 'wait') @@ -127,8 +152,11 @@ class TestViews(unittest.TestCase): self.login_user() - rv = self.client.post('/odcs/1/composes/', data=json.dumps({'id': 1})) - data = json.loads(rv.data.decode('utf8')) + with app.test_request_context(): + flask.g.groups = ['tester'] + + rv = self.client.post('/odcs/1/composes/', data=json.dumps({'id': 1})) + data = json.loads(rv.data.decode('utf8')) self.assertEqual(data['id'], 3) self.assertEqual(data['state_name'], 'wait') @@ -139,20 +167,26 @@ class TestViews(unittest.TestCase): self.assertEqual(c.reused_id, None) def test_submit_build_resurrection_no_removed(self): + db.session.commit() self.login_user() - db.session.commit() - rv = self.client.post('/odcs/1/composes/', data=json.dumps({'id': 1})) - data = json.loads(rv.data.decode('utf8')) + with app.test_request_context(): + flask.g.groups = ['tester'] + + rv = self.client.post('/odcs/1/composes/', data=json.dumps({'id': 1})) + data = json.loads(rv.data.decode('utf8')) self.assertEqual(data['message'], 'No expired or failed compose with id 1') def test_submit_build_resurrection_not_found(self): self.login_user() - db.session.commit() - rv = self.client.post('/odcs/1/composes/', data=json.dumps({'id': 100})) - data = json.loads(rv.data.decode('utf8')) + + with app.test_request_context(): + flask.g.groups = ['tester'] + + rv = self.client.post('/odcs/1/composes/', data=json.dumps({'id': 100})) + data = json.loads(rv.data.decode('utf8')) self.assertEqual(data['message'], 'No expired or failed compose with id 100') @@ -210,11 +244,14 @@ class TestViews(unittest.TestCase): self.assertEqual(len(Compose.composes_to_expire()), 0) - resp = self.client.delete("/odcs/1/composes/%s" % c3.id) + with app.test_request_context(): + flask.g.groups = ['tester'] + + resp = self.client.delete("/odcs/1/composes/%s" % c3.id) + data = json.loads(resp.data.decode('utf8')) self.assertEqual(resp.status, '202 ACCEPTED') - data = json.loads(resp.data.decode('utf8')) self.assertEqual(data['status'], 202) self.assertEqual(data['message'], "The delete request for compose (id=%s) has been accepted and will be processed by backend later." % c3.id) @@ -238,8 +275,12 @@ class TestViews(unittest.TestCase): db.session.add(new_c) db.session.commit() - resp = self.client.delete("/odcs/1/composes/%s" % new_c.id) - data = json.loads(resp.data.decode('utf8')) + with app.test_request_context(): + flask.g.groups = ['tester'] + + resp = self.client.delete("/odcs/1/composes/%s" % new_c.id) + data = json.loads(resp.data.decode('utf8')) + self.assertEqual(resp.status, '400 BAD REQUEST') self.assertEqual(data['status'], 400) self.assertRegexpMatches(data['message'], @@ -249,10 +290,62 @@ class TestViews(unittest.TestCase): def test_delete_non_exist_compose(self): self.login_user() - resp = self.client.delete("/odcs/1/composes/999999") - data = json.loads(resp.data.decode('utf8')) + with app.test_request_context(): + flask.g.groups = ['tester'] + + resp = self.client.delete("/odcs/1/composes/999999") + data = json.loads(resp.data.decode('utf8')) self.assertEqual(resp.status, '404 NOT FOUND') self.assertEqual(data['status'], 404) self.assertEqual(data['message'], "No such compose found.") self.assertEqual(data['error'], 'Not Found') + + +class TestUserInAllowedGroupsDecorator(unittest.TestCase): + """Test decorator user_in_allowed_groups""" + + def setUp(self): + self.mock_func = Mock() + self.decorated_func = user_in_allowed_groups(self.mock_func) + + self.patch_allowed_groups = patch.object(odcs.auth.conf, + 'allowed_groups', + new=['testers']) + self.patch_allowed_groups.start() + + def tearDown(self): + self.patch_allowed_groups.stop() + + def test_401_if_not_in_allowed_groups(self): + with app.test_request_context(): + flask.g.groups = ['another_group'] + flask.g.user = User(id=1, username='tester') + + self.assertRaises(Unauthorized, self.decorated_func, 1, 2, 3) + self.mock_func.assert_not_called() + + def test_authorized_if_in_allowed_groups(self): + with app.test_request_context(): + flask.g.groups = ['testers'] + flask.g.user = User(id=1, username='tester') + + self.decorated_func(1, 2, 3) + self.mock_func.assert_called_once_with(1, 2, 3) + + @patch.object(odcs.views.conf, 'authorize_disabled', new=True) + def test_no_authorize_when_disable_authorize(self): + with app.test_request_context(): + flask.g.groups = ['testers'] + flask.g.user = User(id=1, username='tester') + + self.decorated_func(1, 2, 3) + self.mock_func.assert_called_once_with(1, 2, 3) + + with app.test_request_context(): + flask.g.groups = ['another_groups'] + flask.g.user = User(id=1, username='tester') + + self.decorated_func(1, 2, 3) + self.assertEqual(2, self.mock_func.call_count) + self.mock_func.assert_called_with(1, 2, 3)