diff --git a/pillar/application/modules/blender_cloud/home_project.py b/pillar/application/modules/blender_cloud/home_project.py index 74bb8fe0..40579763 100644 --- a/pillar/application/modules/blender_cloud/home_project.py +++ b/pillar/application/modules/blender_cloud/home_project.py @@ -77,6 +77,7 @@ def create_home_project(user_id): @blueprint.route('/home-project') +@authorization.ab_testing(require_roles={u'homeproject'}) @authorization.require_login(require_roles={u'subscriber', u'demo'}) def home_project(): """Fetches the home project, creating it if necessary. diff --git a/pillar/application/utils/authorization.py b/pillar/application/utils/authorization.py index 90650135..bebf229d 100644 --- a/pillar/application/utils/authorization.py +++ b/pillar/application/utils/authorization.py @@ -273,6 +273,37 @@ def require_login(require_roles=set(), return decorator +def ab_testing(require_roles=set(), + require_all=False): + """Decorator that raises a 404 when the user doesn't match the roles.. + + :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): + if not user_matches_roles(require_roles, require_all): + abort(404) + + return func(*args, **kwargs) + + return wrapper + + return decorator + + def user_has_role(role, user=None): """Returns True iff the user is logged in and has the given role.""" diff --git a/tests/common_test_class.py b/tests/common_test_class.py index 00fabdc4..f09d7c56 100644 --- a/tests/common_test_class.py +++ b/tests/common_test_class.py @@ -141,7 +141,7 @@ class AbstractPillarTest(TestMinimal): return token_data - def badger(self, user_email, role, action, srv_token=None): + def badger(self, user_email, roles, action, srv_token=None): """Creates a service account, and uses it to grant or revoke a role to the user. To skip creation of the service account, pass a srv_token. @@ -150,21 +150,25 @@ class AbstractPillarTest(TestMinimal): :rtype: str """ + if isinstance(roles, str): + roles = set(roles) + # Create a service account if needed. if srv_token is None: from application.modules.service import create_service_account with self.app.test_request_context(): _, srv_token_doc = create_service_account('service@example.com', {'badger'}, - {'badger': [role]}) + {'badger': list(roles)}) srv_token = srv_token_doc['token'] - resp = self.client.post('/service/badger', - headers={'Authorization': self.make_header(srv_token), - 'Content-Type': 'application/json'}, - data=json.dumps({'action': action, - 'role': role, - 'user_email': user_email})) + for role in roles: + resp = self.client.post('/service/badger', + headers={'Authorization': self.make_header(srv_token), + 'Content-Type': 'application/json'}, + data=json.dumps({'action': action, + 'role': role, + 'user_email': user_email})) self.assertEqual(204, resp.status_code, resp.data) return srv_token diff --git a/tests/test_bcloud_home_project.py b/tests/test_bcloud_home_project.py index 2b64adc5..aae5c0d3 100644 --- a/tests/test_bcloud_home_project.py +++ b/tests/test_bcloud_home_project.py @@ -23,7 +23,7 @@ class HomeProjectTest(AbstractPillarTest): def _create_user_with_token(self, roles, token, user_id='cafef00df00df00df00df00d'): """Creates a user directly in MongoDB, so that it doesn't trigger any Eve hooks.""" - user_id = self.create_user(roles=roles, user_id=user_id) + user_id = self.create_user(roles=roles.union({u'homeproject'}), user_id=user_id) self.create_valid_auth_token(user_id, token) return user_id @@ -47,7 +47,9 @@ class HomeProjectTest(AbstractPillarTest): # Test availability at end-point resp = self.client.get(endpoint) - self.assertEqual(403, resp.status_code) + # While we're still AB-testing, unauthenticated users should get a 404. + # When that's over, it should result in a 403. + self.assertEqual(404, resp.status_code) resp = self.client.get(endpoint, headers={'Authorization': self.make_header('token')}) self.assertEqual(200, resp.status_code) @@ -63,8 +65,8 @@ class HomeProjectTest(AbstractPillarTest): resp = self.client.get('/users/me', headers={'Authorization': self.make_header('token')}) self.assertEqual(200, resp.status_code, resp) - # Grant subscriber role, and fetch the home project. - self.badger(TEST_EMAIL_ADDRESS, 'subscriber', 'grant') + # Grant subscriber and homeproject roles, and fetch the home project. + self.badger(TEST_EMAIL_ADDRESS, {'subscriber', 'homeproject'}, 'grant') resp = self.client.get('/bcloud/home-project', headers={'Authorization': self.make_header('token')}) @@ -73,6 +75,28 @@ class HomeProjectTest(AbstractPillarTest): json_proj = json.loads(resp.data) self.assertEqual('home', json_proj['category']) + @responses.activate + def test_home_project_ab_testing(self): + self.mock_blenderid_validate_happy() + resp = self.client.get('/users/me', headers={'Authorization': self.make_header('token')}) + self.assertEqual(200, resp.status_code, resp) + + # Grant subscriber and but NOT homeproject role, and fetch the home project. + self.badger(TEST_EMAIL_ADDRESS, {'subscriber'}, 'grant') + + resp = self.client.get('/bcloud/home-project', + headers={'Authorization': self.make_header('token')}) + self.assertEqual(404, resp.status_code) + + resp = self.client.get('/users/me', + headers={'Authorization': self.make_header('token')}) + self.assertEqual(200, resp.status_code) + me = json.loads(resp.data) + + with self.app.test_request_context(): + from application.modules.blender_cloud import home_project + self.assertFalse(home_project.has_home_project(me['_id'])) + @responses.activate def test_autocreate_home_project_with_demo_role(self): # Implicitly create user by token validation. @@ -80,8 +104,8 @@ class HomeProjectTest(AbstractPillarTest): resp = self.client.get('/users/me', headers={'Authorization': self.make_header('token')}) self.assertEqual(200, resp.status_code, resp) - # Grant demo role, which should allow creation of the home project. - self.badger(TEST_EMAIL_ADDRESS, 'demo', 'grant') + # Grant demo and homeproject role, which should allow creation of the home project. + self.badger(TEST_EMAIL_ADDRESS, {'demo', 'homeproject'}, 'grant') resp = self.client.get('/bcloud/home-project', headers={'Authorization': self.make_header('token')}) @@ -97,8 +121,8 @@ class HomeProjectTest(AbstractPillarTest): resp = self.client.get('/users/me', headers={'Authorization': self.make_header('token')}) self.assertEqual(200, resp.status_code, resp) - # Grant demo role, which should NOT allow creation fo the home project. - self.badger(TEST_EMAIL_ADDRESS, 'succubus', 'grant') + # Grant succubus role, which should NOT allow creation fo the home project. + self.badger(TEST_EMAIL_ADDRESS, {'succubus', 'homeproject'}, 'grant') resp = self.client.get('/bcloud/home-project', headers={'Authorization': self.make_header('token')}) @@ -135,7 +159,7 @@ class HomeProjectTest(AbstractPillarTest): self.assertEqual(200, resp.status_code, resp) # Grant subscriber role, and fetch the home project. - self.badger(TEST_EMAIL_ADDRESS, 'subscriber', 'grant') + self.badger(TEST_EMAIL_ADDRESS, {'subscriber', 'homeproject'}, 'grant') resp = self.client.get('/bcloud/home-project', query_string={'projection': json.dumps( @@ -160,7 +184,7 @@ class HomeProjectTest(AbstractPillarTest): self.assertEqual(200, resp.status_code, resp) # Grant subscriber role, and fetch the home project. - self.badger(TEST_EMAIL_ADDRESS, 'subscriber', 'grant') + self.badger(TEST_EMAIL_ADDRESS, {'subscriber', 'homeproject'}, 'grant') resp = self.client.get('/bcloud/home-project', headers={'Authorization': self.make_header('token')})