From 0ba1ec0da91dc3c9630d020af945d7f15a0e52cf Mon Sep 17 00:00:00 2001 From: Julien Danjou Date: Thu, 27 Dec 2012 18:51:48 +0100 Subject: [PATCH] Implement user-api This implements the blueprint user-api for version 1 of the API. The ACL checks now don't return access denied on non-admin users. All requests without a in the URL limit their scope to the tenant contained into X-Tenant-Id. All request with a in the URL returns 404 if the specified is different than the one from X-Tenant-Id. Change-Id: If1ec55fa491ea5de30036ce7ed75d0f28e925457 Signed-off-by: Julien Danjou --- ceilometer/api/v1/acl.py | 9 +- ceilometer/api/v1/blueprint.py | 140 ++++++++++++++++------- ceilometer/storage/impl_mongodb.py | 2 +- ceilometer/tests/api.py | 18 +-- tests/api/v1/test_acl.py | 63 ---------- tests/api/v1/test_list_events.py | 64 +++++++++++ tests/api/v1/test_list_meters.py | 75 ++++++++++++ tests/api/v1/test_list_projects.py | 62 ++++------ tests/api/v1/test_list_resources.py | 97 ++++++++++++++++ tests/api/v1/test_list_users.py | 63 ++++------ tests/api/v1/test_max_project_volume.py | 13 +++ tests/api/v1/test_max_resource_volume.py | 12 ++ tests/api/v1/test_sum_project_volume.py | 13 +++ tests/api/v1/test_sum_resource_volume.py | 13 +++ 14 files changed, 448 insertions(+), 196 deletions(-) delete mode 100644 tests/api/v1/test_acl.py diff --git a/ceilometer/api/v1/acl.py b/ceilometer/api/v1/acl.py index 555e5afd4..8fab9878d 100644 --- a/ceilometer/api/v1/acl.py +++ b/ceilometer/api/v1/acl.py @@ -17,7 +17,6 @@ # under the License. """Set up the ACL to acces the API server.""" -import flask from ceilometer import policy import keystoneclient.middleware.auth_token as auth_token @@ -37,14 +36,12 @@ def install(app, conf): app.wsgi_app = auth_token.AuthProtocol(app.wsgi_app, conf=conf, ) - app.before_request(check) return app -def check(): - """Check application access.""" - headers = flask.request.headers +def get_limited_to_project(headers): + """Return the tenant the request should be limited to.""" if not policy.check_is_admin(headers.get('X-Roles', "").split(","), headers.get('X-Tenant-Id'), headers.get('X-Tenant-Name')): - return "Access denied", 401 + return headers.get('X-Tenant-Id') diff --git a/ceilometer/api/v1/blueprint.py b/ceilometer/api/v1/blueprint.py index ea67032ce..503c67a32 100644 --- a/ceilometer/api/v1/blueprint.py +++ b/ceilometer/api/v1/blueprint.py @@ -85,6 +85,8 @@ from ceilometer.openstack.common import timeutils from ceilometer import storage +from ceilometer.api.v1 import acl + LOG = log.getLogger(__name__) @@ -105,6 +107,13 @@ def _get_metaquery(args): for (k, v) in args.iteritems() if k.startswith('metadata.')) + +def check_authorized_project(project): + authorized_project = acl.get_limited_to_project(flask.request.headers) + if authorized_project and authorized_project != project: + flask.abort(404) + + ## APIs for working with meters. @@ -114,7 +123,9 @@ def list_meters_all(): :param metadata. match on the metadata within the resource. (optional) """ rq = flask.request - meters = rq.storage_conn.get_meters(metaquery=_get_metaquery(rq.args)) + meters = rq.storage_conn.get_meters( + project=acl.get_limited_to_project(rq.headers), + metaquery=_get_metaquery(rq.args)) return flask.jsonify(meters=list(meters)) @@ -126,8 +137,10 @@ def list_meters_by_resource(resource): :param metadata. match on the metadata within the resource. (optional) """ rq = flask.request - meters = rq.storage_conn.get_meters(resource=resource, - metaquery=_get_metaquery(rq.args)) + meters = rq.storage_conn.get_meters( + resource=resource, + project=acl.get_limited_to_project(rq.headers), + metaquery=_get_metaquery(rq.args)) return flask.jsonify(meters=list(meters)) @@ -139,8 +152,10 @@ def list_meters_by_user(user): :param metadata. match on the metadata within the resource. (optional) """ rq = flask.request - meters = rq.storage_conn.get_meters(user=user, - metaquery=_get_metaquery(rq.args)) + meters = rq.storage_conn.get_meters( + user=user, + project=acl.get_limited_to_project(rq.headers), + metaquery=_get_metaquery(rq.args)) return flask.jsonify(meters=list(meters)) @@ -151,9 +166,12 @@ def list_meters_by_project(project): :param project: The ID of the owning project. :param metadata. match on the metadata within the resource. (optional) """ + check_authorized_project(project) + rq = flask.request - meters = rq.storage_conn.get_meters(project=project, - metaquery=_get_metaquery(rq.args)) + meters = rq.storage_conn.get_meters( + project=project, + metaquery=_get_metaquery(rq.args)) return flask.jsonify(meters=list(meters)) @@ -165,8 +183,10 @@ def list_meters_by_source(source): :param metadata. match on the metadata within the resource. (optional) """ rq = flask.request - meters = rq.storage_conn.get_meters(source=source, - metaquery=_get_metaquery(rq.args)) + meters = rq.storage_conn.get_meters( + source=source, + project=acl.get_limited_to_project(rq.headers), + metaquery=_get_metaquery(rq.args)) return flask.jsonify(meters=list(meters)) @@ -201,6 +221,7 @@ def list_resources_by_project(project): :type end_timestamp: ISO date in UTC :param metadata. match on the metadata within the resource. (optional) """ + check_authorized_project(project) return _list_resources(project=project) @@ -216,7 +237,8 @@ def list_all_resources(): :type end_timestamp: ISO date in UTC :param metadata. match on the metadata within the resource. (optional) """ - return _list_resources() + return _list_resources( + project=acl.get_limited_to_project(flask.request.headers)) @blueprint.route('/sources/') @@ -242,7 +264,10 @@ def list_resources_by_source(source): :type end_timestamp: ISO date in UTC :param metadata. match on the metadata within the resource. (optional) """ - return _list_resources(source=source) + return _list_resources( + source=source, + project=acl.get_limited_to_project(flask.request.headers), + ) @blueprint.route('/users//resources') @@ -258,7 +283,10 @@ def list_resources_by_user(user): :type end_timestamp: ISO date in UTC :param metadata. match on the metadata within the resource. (optional) """ - return _list_resources(user=user) + return _list_resources( + user=user, + project=acl.get_limited_to_project(flask.request.headers), + ) ## APIs for working with users. @@ -267,7 +295,13 @@ def list_resources_by_user(user): def _list_users(source=None): """Return a list of user names. """ - users = flask.request.storage_conn.get_users(source=source) + # TODO(jd) it might be better to return the real list of users that are + # belonging to the project, but that's not provided by the storage + # drivers for now + if acl.get_limited_to_project(flask.request.headers): + users = [flask.request.headers.get('X-User-id')] + else: + users = flask.request.storage_conn.get_users(source=source) return flask.jsonify(users=list(users)) @@ -294,7 +328,18 @@ def list_users_by_source(source): def _list_projects(source=None): """Return a list of project names. """ - projects = flask.request.storage_conn.get_projects(source=source) + project = acl.get_limited_to_project(flask.request.headers) + if project: + if source: + if project in flask.request.storage_conn.get_projects( + source=source): + projects = [project] + else: + projects = [] + else: + projects = [project] + else: + projects = flask.request.storage_conn.get_projects(source=source) return flask.jsonify(projects=list(projects)) @@ -334,7 +379,7 @@ def _list_events(meter, start=q_ts['start_timestamp'], end=q_ts['end_timestamp'], metaquery=_get_metaquery(flask.request.args), - ) + ) events = list(flask.request.storage_conn.get_raw_events(f)) jsonified = flask.jsonify(events=events) if request_wants_html(): @@ -361,6 +406,7 @@ def list_events_by_project(project, meter): (optional) :type end_timestamp: ISO date in UTC """ + check_authorized_project(project) return _list_events(project=project, meter=meter, ) @@ -379,9 +425,11 @@ def list_events_by_resource(resource, meter): (optional) :type end_timestamp: ISO date in UTC """ - return _list_events(resource=resource, - meter=meter, - ) + return _list_events( + resource=resource, + meter=meter, + project=acl.get_limited_to_project(flask.request.headers), + ) @blueprint.route('/sources//meters/') @@ -397,9 +445,11 @@ def list_events_by_source(source, meter): (optional) :type end_timestamp: ISO date in UTC """ - return _list_events(source=source, - meter=meter, - ) + return _list_events( + source=source, + meter=meter, + project=acl.get_limited_to_project(flask.request.headers), + ) @blueprint.route('/users//meters/') @@ -415,9 +465,11 @@ def list_events_by_user(user, meter): (optional) :type end_timestamp: ISO date in UTC """ - return _list_events(user=user, - meter=meter, - ) + return _list_events( + user=user, + meter=meter, + project=acl.get_limited_to_project(flask.request.headers), + ) ## APIs for working with meter calculations. @@ -475,11 +527,13 @@ def compute_duration_by_resource(resource, meter): # Query the database for the interval of timestamps # within the desired range. - f = storage.EventFilter(meter=meter, - resource=resource, - start=q_ts['query_start'], - end=q_ts['query_end'], - ) + f = storage.EventFilter( + meter=meter, + project=acl.get_limited_to_project(flask.request.headers), + resource=resource, + start=q_ts['query_start'], + end=q_ts['query_end'], + ) min_ts, max_ts = flask.request.storage_conn.get_event_interval(f) # "Clamp" the timestamps we return to the original time @@ -533,11 +587,13 @@ def compute_max_resource_volume(resource, meter): q_ts = _get_query_timestamps(flask.request.args) # Query the database for the max volume - f = storage.EventFilter(meter=meter, - resource=resource, - start=q_ts['query_start'], - end=q_ts['query_end'], - ) + f = storage.EventFilter( + meter=meter, + project=acl.get_limited_to_project(flask.request.headers), + resource=resource, + start=q_ts['query_start'], + end=q_ts['query_end'], + ) # TODO(sberler): do we want to return an error if the resource # does not exist? results = list(flask.request.storage_conn.get_volume_max(f)) @@ -564,11 +620,13 @@ def compute_resource_volume_sum(resource, meter): q_ts = _get_query_timestamps(flask.request.args) # Query the database for the max volume - f = storage.EventFilter(meter=meter, - resource=resource, - start=q_ts['query_start'], - end=q_ts['query_end'], - ) + f = storage.EventFilter( + meter=meter, + project=acl.get_limited_to_project(flask.request.headers), + resource=resource, + start=q_ts['query_start'], + end=q_ts['query_end'], + ) # TODO(sberler): do we want to return an error if the resource # does not exist? results = list(flask.request.storage_conn.get_volume_sum(f)) @@ -592,6 +650,8 @@ def compute_project_volume_max(project, meter): :param search_offset: Number of minutes before and after start and end timestamps to query. """ + check_authorized_project(project) + q_ts = _get_query_timestamps(flask.request.args) f = storage.EventFilter(meter=meter, @@ -624,6 +684,8 @@ def compute_project_volume_sum(project, meter): :param search_offset: Number of minutes before and after start and end timestamps to query. """ + check_authorized_project(project) + q_ts = _get_query_timestamps(flask.request.args) f = storage.EventFilter(meter=meter, diff --git a/ceilometer/storage/impl_mongodb.py b/ceilometer/storage/impl_mongodb.py index 4bc4df32e..24aacff03 100644 --- a/ceilometer/storage/impl_mongodb.py +++ b/ceilometer/storage/impl_mongodb.py @@ -97,7 +97,7 @@ def make_query_from_filter(event_filter, require_meter=True): if event_filter.user: q['user_id'] = event_filter.user - elif event_filter.project: + if event_filter.project: q['project_id'] = event_filter.project if event_filter.meter: diff --git a/ceilometer/tests/api.py b/ceilometer/tests/api.py index 1b0b870ef..2060988c2 100644 --- a/ceilometer/tests/api.py +++ b/ceilometer/tests/api.py @@ -52,18 +52,20 @@ class TestBase(db_test_base.TestBase): def attach_storage_connection(): flask.request.storage_conn = self.conn - def get(self, path, **kwds): + def get(self, path, headers=None, **kwds): if kwds: query = path + '?' + urllib.urlencode(kwds) else: query = path - rv = self.test_app.get(query) - try: - data = json.loads(rv.data) - except ValueError: - print 'RAW DATA:', rv - raise - return data + rv = self.test_app.get(query, headers=headers) + if rv.status_code == 200 and rv.content_type == 'application/json': + try: + data = json.loads(rv.data) + except ValueError: + print 'RAW DATA:', rv + raise + return data + return rv class FunctionalTest(unittest.TestCase): diff --git a/tests/api/v1/test_acl.py b/tests/api/v1/test_acl.py deleted file mode 100644 index 66c3a3a75..000000000 --- a/tests/api/v1/test_acl.py +++ /dev/null @@ -1,63 +0,0 @@ -# -*- encoding: utf-8 -*- -# -# Copyright © 2012 New Dream Network, LLC (DreamHost) -# -# Author: Julien Danjou -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -"""Test ACL.""" - -from ceilometer.tests import api as tests_api -from ceilometer.api.v1 import acl - - -class TestAPIACL(tests_api.TestBase): - - def setUp(self): - super(TestAPIACL, self).setUp() - acl.install(self.app, {}) - - def test_non_authenticated(self): - with self.app.test_request_context('/'): - self.app.preprocess_request() - self.assertEqual(self.test_app.get().status_code, 401) - - def test_authenticated_wrong_role(self): - with self.app.test_request_context('/', headers={ - "X-Roles": "Member", - "X-Tenant-Name": "foobar", - "X-Tenant-Id": "bc23a9d531064583ace8f67dad60f6bb", - }): - self.app.preprocess_request() - self.assertEqual(self.test_app.get().status_code, 401) - - # FIXME(dhellmann): This test is not properly looking at the tenant - # info. The status code returned is the expected value, but it - # is not clear why. - # - # def test_authenticated_wrong_tenant(self): - # with self.app.test_request_context('/', headers={ - # "X-Roles": "admin", - # "X-Tenant-Name": "foobar", - # "X-Tenant-Id": "bc23a9d531064583ace8f67dad60f6bb", - # }): - # self.app.preprocess_request() - # self.assertEqual(self.test_app.get().status_code, 401) - - def test_authenticated(self): - with self.app.test_request_context('/', headers={ - "X-Roles": "admin", - "X-Tenant-Name": "admin", - "X-Tenant-Id": "bc23a9d531064583ace8f67dad60f6bb", - }): - self.assertEqual(self.app.preprocess_request(), None) diff --git a/tests/api/v1/test_list_events.py b/tests/api/v1/test_list_events.py index ed1372857..bff8b9fde 100644 --- a/tests/api/v1/test_list_events.py +++ b/tests/api/v1/test_list_events.py @@ -3,6 +3,7 @@ # Copyright © 2012 New Dream Network, LLC (DreamHost) # # Author: Doug Hellmann +# Julien Danjou # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain @@ -85,6 +86,18 @@ class TestListEvents(tests_api.TestBase): data = self.get('/projects/project1/meters/instance') self.assertEquals(2, len(data['events'])) + def test_by_project_non_admin(self): + data = self.get('/projects/project1/meters/instance', + headers={"X-Roles": "Member", + "X-Tenant-Id": "project1"}) + self.assertEquals(2, len(data['events'])) + + def test_by_project_wrong_tenant(self): + resp = self.get('/projects/project1/meters/instance', + headers={"X-Roles": "Member", + "X-Tenant-Id": "this-is-my-project"}) + self.assertEquals(404, resp.status_code) + def test_by_project_with_timestamps(self): data = self.get('/projects/project1/meters/instance', start_timestamp=datetime.datetime(2012, 7, 2, 10, 42)) @@ -98,6 +111,18 @@ class TestListEvents(tests_api.TestBase): data = self.get('/resources/resource-id/meters/instance') self.assertEquals(2, len(data['events'])) + def test_by_resource_non_admin(self): + data = self.get('/resources/resource-id-alternate/meters/instance', + headers={"X-Roles": "Member", + "X-Tenant-Id": "project2"}) + self.assertEquals(1, len(data['events'])) + + def test_by_resource_some_tenant(self): + data = self.get('/resources/resource-id/meters/instance', + headers={"X-Roles": "Member", + "X-Tenant-Id": "project2"}) + self.assertEquals(0, len(data['events'])) + def test_empty_source(self): data = self.get('/sources/no-such-source/meters/instance') self.assertEquals({'events': []}, data) @@ -106,6 +131,12 @@ class TestListEvents(tests_api.TestBase): data = self.get('/sources/source1/meters/instance') self.assertEquals(3, len(data['events'])) + def test_by_source_non_admin(self): + data = self.get('/sources/source1/meters/instance', + headers={"X-Roles": "Member", + "X-Tenant-Id": "project2"}) + self.assertEquals(1, len(data['events'])) + def test_by_source_with_timestamps(self): data = self.get('/sources/source1/meters/instance', end_timestamp=datetime.datetime(2012, 7, 2, 10, 42)) @@ -119,6 +150,18 @@ class TestListEvents(tests_api.TestBase): data = self.get('/users/user-id/meters/instance') self.assertEquals(2, len(data['events'])) + def test_by_user_non_admin(self): + data = self.get('/users/user-id/meters/instance', + headers={"X-Roles": "Member", + "X-Tenant-Id": "project1"}) + self.assertEquals(2, len(data['events'])) + + def test_by_user_wrong_tenant(self): + data = self.get('/users/user-id/meters/instance', + headers={"X-Roles": "Member", + "X-Tenant-Id": "project2"}) + self.assertEquals(0, len(data['events'])) + def test_by_user_with_timestamps(self): data = self.get('/users/user-id/meters/instance', start_timestamp=datetime.datetime(2012, 7, 2, 10, 41), @@ -130,12 +173,33 @@ class TestListEvents(tests_api.TestBase): data = self.get('%s?metadata.tag=self.counter2' % q) self.assertEquals(1, len(data['events'])) + def test_metaquery1_wrong_tenant(self): + q = '/sources/source1/meters/instance' + data = self.get('%s?metadata.tag=self.counter2' % q, + headers={"X-Roles": "Member", + "X-Tenant-Id": "project1"}) + self.assertEquals(0, len(data['events'])) + def test_metaquery2(self): q = '/sources/source1/meters/instance' data = self.get('%s?metadata.tag=self.counter' % q) self.assertEquals(2, len(data['events'])) + def test_metaquery2_non_admin(self): + q = '/sources/source1/meters/instance' + data = self.get('%s?metadata.tag=self.counter' % q, + headers={"X-Roles": "Member", + "X-Tenant-Id": "project1"}) + self.assertEquals(2, len(data['events'])) + def test_metaquery3(self): q = '/sources/source1/meters/instance' data = self.get('%s?metadata.display_name=test-server' % q) self.assertEquals(3, len(data['events'])) + + def test_metaquery3_with_project(self): + q = '/sources/source1/meters/instance' + data = self.get('%s?metadata.display_name=test-server' % q, + headers={"X-Roles": "Member", + "X-Tenant-Id": "project2"}) + self.assertEquals(1, len(data['events'])) diff --git a/tests/api/v1/test_list_meters.py b/tests/api/v1/test_list_meters.py index 27f7c3022..99a1f3ead 100644 --- a/tests/api/v1/test_list_meters.py +++ b/tests/api/v1/test_list_meters.py @@ -3,6 +3,7 @@ # Copyright 2012 Red Hat, Inc. # # Author: Angus Salkeld +# Julien Danjou # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain @@ -115,6 +116,18 @@ class TestListMeters(tests_api.TestBase): set(['meter.test', 'meter.mine'])) + def test_list_meters_non_admin(self): + data = self.get('/meters', + headers={"X-Roles": "Member", + "X-Tenant-Id": "project-id"}) + self.assertEquals(2, len(data['meters'])) + self.assertEquals(set(r['resource_id'] for r in data['meters']), + set(['resource-id', + 'resource-id2'])) + self.assertEquals(set(r['name'] for r in data['meters']), + set(['meter.test', + 'meter.mine'])) + def test_with_resource(self): data = self.get('/resources/resource-id/meters') ids = set(r['name'] for r in data['meters']) @@ -128,6 +141,14 @@ class TestListMeters(tests_api.TestBase): 'resource-id3', 'resource-id4']), ids) + def test_with_source_non_admin(self): + data = self.get('/sources/test_list_resources/meters', + headers={"X-Roles": "Member", + "X-Tenant-Id": "project-id2"}) + ids = set(r['resource_id'] for r in data['meters']) + self.assertEquals(set(['resource-id3', + 'resource-id4']), ids) + def test_with_source_non_existent(self): data = self.get('/sources/test_list_resources_dont_exist/meters') self.assertEquals(data['meters'], []) @@ -141,6 +162,23 @@ class TestListMeters(tests_api.TestBase): rids = set(r['resource_id'] for r in data['meters']) self.assertEquals(set(['resource-id', 'resource-id2']), rids) + def test_with_user_non_admin(self): + data = self.get('/users/user-id/meters', + headers={"X-Roles": "Member", + "X-Tenant-Id": "project-id"}) + nids = set(r['name'] for r in data['meters']) + self.assertEquals(set(['meter.mine', 'meter.test']), nids) + + rids = set(r['resource_id'] for r in data['meters']) + self.assertEquals(set(['resource-id', 'resource-id2']), rids) + + def test_with_user_wrong_tenant(self): + data = self.get('/users/user-id/meters', + headers={"X-Roles": "Member", + "X-Tenant-Id": "project666"}) + + self.assertEquals(data['meters'], []) + def test_with_user_non_existent(self): data = self.get('/users/user-id-foobar123/meters') self.assertEquals(data['meters'], []) @@ -150,6 +188,19 @@ class TestListMeters(tests_api.TestBase): ids = set(r['resource_id'] for r in data['meters']) self.assertEquals(set(['resource-id3', 'resource-id4']), ids) + def test_with_project_non_admin(self): + data = self.get('/projects/project-id2/meters', + headers={"X-Roles": "Member", + "X-Tenant-Id": "project-id2"}) + ids = set(r['resource_id'] for r in data['meters']) + self.assertEquals(set(['resource-id3', 'resource-id4']), ids) + + def test_with_project_wrong_tenant(self): + data = self.get('/projects/project-id2/meters', + headers={"X-Roles": "Member", + "X-Tenant-Id": "project-id"}) + self.assertEqual(data.status_code, 404) + def test_with_project_non_existent(self): data = self.get('/projects/jd-was-here/meters') self.assertEquals(data['meters'], []) @@ -158,6 +209,30 @@ class TestListMeters(tests_api.TestBase): data = self.get('/meters?metadata.tag=self.counter') self.assertEquals(1, len(data['meters'])) + def test_metaquery1_non_admin(self): + data = self.get('/meters?metadata.tag=self.counter', + headers={"X-Roles": "Member", + "X-Tenant-Id": "project-id"}) + self.assertEquals(1, len(data['meters'])) + + def test_metaquery1_wrong_tenant(self): + data = self.get('/meters?metadata.tag=self.counter', + headers={"X-Roles": "Member", + "X-Tenant-Id": "project-666"}) + self.assertEquals(0, len(data['meters'])) + def test_metaquery2(self): data = self.get('/meters?metadata.tag=four.counter') self.assertEquals(1, len(data['meters'])) + + def test_metaquery2_non_admin(self): + data = self.get('/meters?metadata.tag=four.counter', + headers={"X-Roles": "Member", + "X-Tenant-Id": "project-id2"}) + self.assertEquals(1, len(data['meters'])) + + def test_metaquery2_non_admin(self): + data = self.get('/meters?metadata.tag=four.counter', + headers={"X-Roles": "Member", + "X-Tenant-Id": "project-666"}) + self.assertEquals(0, len(data['meters'])) diff --git a/tests/api/v1/test_list_projects.py b/tests/api/v1/test_list_projects.py index 91ee9a952..c09393f2a 100644 --- a/tests/api/v1/test_list_projects.py +++ b/tests/api/v1/test_list_projects.py @@ -3,6 +3,7 @@ # Copyright © 2012 New Dream Network, LLC (DreamHost) # # Author: Doug Hellmann +# Julien Danjou # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain @@ -30,13 +31,17 @@ from ceilometer.tests import api as tests_api LOG = logging.getLogger(__name__) -class TestListProjects(tests_api.TestBase): +class TestListEmptyProjects(tests_api.TestBase): def test_empty(self): data = self.get('/projects') self.assertEquals({'projects': []}, data) - def test_projects(self): + +class TestListProjects(tests_api.TestBase): + + def setUp(self): + super(TestListProjects, self).setUp() counter1 = counter.Counter( 'instance', 'cumulative', @@ -73,45 +78,22 @@ class TestListProjects(tests_api.TestBase): ) self.conn.record_metering_data(msg2) + def test_projects(self): data = self.get('/projects') self.assertEquals(['project-id', 'project-id2'], data['projects']) - def test_with_source(self): - counter1 = counter.Counter( - 'instance', - 'cumulative', - 1, - 'user-id', - 'project-id', - 'resource-id', - timestamp=datetime.datetime(2012, 7, 2, 10, 40), - resource_metadata={'display_name': 'test-server', - 'tag': 'self.counter', - } - ) - msg = meter.meter_message_from_counter(counter1, - cfg.CONF.metering_secret, - 'test_list_users', - ) - self.conn.record_metering_data(msg) - - counter2 = counter.Counter( - 'instance', - 'cumulative', - 1, - 'user-id2', - 'project-id2', - 'resource-id-alternate', - timestamp=datetime.datetime(2012, 7, 2, 10, 41), - resource_metadata={'display_name': 'test-server', - 'tag': 'self.counter2', - } - ) - msg2 = meter.meter_message_from_counter(counter2, - cfg.CONF.metering_secret, - 'not-test', - ) - self.conn.record_metering_data(msg2) - - data = self.get('/sources/test_list_users/projects') + def test_projects_non_admin(self): + data = self.get('/projects', + headers={"X-Roles": "Member", + "X-Tenant-Id": "project-id"}) self.assertEquals(['project-id'], data['projects']) + + def test_with_source(self): + data = self.get('/sources/test_list_users/projects') + self.assertEquals(['project-id2'], data['projects']) + + def test_with_source_non_admin(self): + data = self.get('/sources/test_list_users/projects', + headers={"X-Roles": "Member", + "X-Tenant-Id": "project-id2"}) + self.assertEquals(['project-id2'], data['projects']) diff --git a/tests/api/v1/test_list_resources.py b/tests/api/v1/test_list_resources.py index 6964669a5..b623dda43 100644 --- a/tests/api/v1/test_list_resources.py +++ b/tests/api/v1/test_list_resources.py @@ -3,6 +3,7 @@ # Copyright © 2012 New Dream Network, LLC (DreamHost) # # Author: Doug Hellmann +# Julien Danjou # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain @@ -100,6 +101,15 @@ class TestListResources(tests_api.TestBase): 'resource-id-alternate', 'resource-id2'])) + def test_list_resources_non_admin(self): + data = self.get('/resources', + headers={"X-Roles": "Member", + "X-Tenant-Id": "project-id"}) + self.assertEquals(2, len(data['resources'])) + self.assertEquals(set(r['resource_id'] for r in data['resources']), + set(['resource-id', + 'resource-id-alternate'])) + def test_list_resources_with_timestamps(self): data = self.get('/resources', start_timestamp=datetime.datetime( @@ -110,6 +120,17 @@ class TestListResources(tests_api.TestBase): set(['resource-id-alternate', 'resource-id2'])) + def test_list_resources_with_timestamps_non_admin(self): + data = self.get('/resources', + start_timestamp=datetime.datetime( + 2012, 7, 2, 10, 41).isoformat(), + end_timestamp=datetime.datetime( + 2012, 7, 2, 10, 43).isoformat(), + headers={"X-Roles": "Member", + "X-Tenant-Id": "project-id"}) + self.assertEquals(set(r['resource_id'] for r in data['resources']), + set(['resource-id-alternate'])) + def test_with_source(self): data = self.get('/sources/test_list_resources/resources') ids = set(r['resource_id'] for r in data['resources']) @@ -117,6 +138,14 @@ class TestListResources(tests_api.TestBase): 'resource-id2', 'resource-id-alternate']), ids) + def test_with_source_non_admin(self): + data = self.get('/sources/test_list_resources/resources', + headers={"X-Roles": "Member", + "X-Tenant-Id": "project-id"}) + ids = set(r['resource_id'] for r in data['resources']) + self.assertEquals(set(['resource-id', + 'resource-id-alternate']), ids) + def test_with_source_with_timestamps(self): data = self.get('/sources/test_list_resources/resources', start_timestamp=datetime.datetime( @@ -127,6 +156,17 @@ class TestListResources(tests_api.TestBase): self.assertEquals(set(['resource-id2', 'resource-id-alternate']), ids) + def test_with_source_with_timestamps_non_admin(self): + data = self.get('/sources/test_list_resources/resources', + headers={"X-Roles": "Member", + "X-Tenant-Id": "project-id"}, + start_timestamp=datetime.datetime( + 2012, 7, 2, 10, 41).isoformat(), + end_timestamp=datetime.datetime( + 2012, 7, 2, 10, 43).isoformat()) + ids = set(r['resource_id'] for r in data['resources']) + self.assertEquals(set(['resource-id-alternate']), ids) + def test_with_source_non_existent(self): data = self.get('/sources/test_list_resources_dont_exist/resources') self.assertEquals(data['resources'], []) @@ -136,6 +176,20 @@ class TestListResources(tests_api.TestBase): ids = set(r['resource_id'] for r in data['resources']) self.assertEquals(set(['resource-id', 'resource-id-alternate']), ids) + def test_with_user_non_admin(self): + data = self.get('/users/user-id/resources', + headers={"X-Roles": "Member", + "X-Tenant-Id": "project-id"}) + ids = set(r['resource_id'] for r in data['resources']) + self.assertEquals(set(['resource-id', 'resource-id-alternate']), ids) + + def test_with_user_wrong_tenant(self): + data = self.get('/users/user-id/resources', + headers={"X-Roles": "Member", + "X-Tenant-Id": "project-jd"}) + ids = set(r['resource_id'] for r in data['resources']) + self.assertEquals(set(), ids) + def test_with_user_with_timestamps(self): data = self.get('/users/user-id/resources', start_timestamp=datetime.datetime( @@ -145,6 +199,17 @@ class TestListResources(tests_api.TestBase): ids = set(r['resource_id'] for r in data['resources']) self.assertEquals(set(), ids) + def test_with_user_with_timestamps_non_admin(self): + data = self.get('/users/user-id/resources', + headers={"X-Roles": "Member", + "X-Tenant-Id": "project-id"}, + start_timestamp=datetime.datetime( + 2012, 7, 2, 10, 42).isoformat(), + end_timestamp=datetime.datetime( + 2012, 7, 2, 10, 42).isoformat()) + ids = set(r['resource_id'] for r in data['resources']) + self.assertEquals(set(), ids) + def test_with_user_non_existent(self): data = self.get('/users/user-id-foobar123/resources') self.assertEquals(data['resources'], []) @@ -154,6 +219,13 @@ class TestListResources(tests_api.TestBase): ids = set(r['resource_id'] for r in data['resources']) self.assertEquals(set(['resource-id', 'resource-id-alternate']), ids) + def test_with_project_non_admin(self): + data = self.get('/projects/project-id/resources', + headers={"X-Roles": "Member", + "X-Tenant-Id": "project-id"}) + ids = set(r['resource_id'] for r in data['resources']) + self.assertEquals(set(['resource-id', 'resource-id-alternate']), ids) + def test_with_project_with_timestamp(self): data = self.get('/projects/project-id/resources', start_timestamp=datetime.datetime( @@ -163,6 +235,17 @@ class TestListResources(tests_api.TestBase): ids = set(r['resource_id'] for r in data['resources']) self.assertEquals(set(['resource-id']), ids) + def test_with_project_with_timestamp_non_admin(self): + data = self.get('/projects/project-id/resources', + headers={"X-Roles": "Member", + "X-Tenant-Id": "project-id"}, + start_timestamp=datetime.datetime( + 2012, 7, 2, 10, 40).isoformat(), + end_timestamp=datetime.datetime( + 2012, 7, 2, 10, 41).isoformat()) + ids = set(r['resource_id'] for r in data['resources']) + self.assertEquals(set(['resource-id']), ids) + def test_with_project_non_existent(self): data = self.get('/projects/jd-was-here/resources') self.assertEquals(data['resources'], []) @@ -172,7 +255,21 @@ class TestListResources(tests_api.TestBase): data = self.get('%s?metadata.display_name=test-server' % q) self.assertEquals(3, len(data['resources'])) + def test_metaquery1_non_admin(self): + q = '/sources/test_list_resources/resources' + data = self.get('%s?metadata.display_name=test-server' % q, + headers={"X-Roles": "Member", + "X-Tenant-Id": "project-id"}) + self.assertEquals(2, len(data['resources'])) + def test_metaquery2(self): q = '/sources/test_list_resources/resources' data = self.get('%s?metadata.tag=self.counter4' % q) self.assertEquals(1, len(data['resources'])) + + def test_metaquery2_non_admin(self): + q = '/sources/test_list_resources/resources' + data = self.get('%s?metadata.tag=self.counter4' % q, + headers={"X-Roles": "Member", + "X-Tenant-Id": "project-id"}) + self.assertEquals(1, len(data['resources'])) diff --git a/tests/api/v1/test_list_users.py b/tests/api/v1/test_list_users.py index 70a83de7c..49676b2c5 100644 --- a/tests/api/v1/test_list_users.py +++ b/tests/api/v1/test_list_users.py @@ -3,6 +3,7 @@ # Copyright © 2012 New Dream Network, LLC (DreamHost) # # Author: Doug Hellmann +# Julien Danjou # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain @@ -30,53 +31,18 @@ from ceilometer.tests import api as tests_api LOG = logging.getLogger(__name__) -class TestListUsers(tests_api.TestBase): +class TestListEmptyUsers(tests_api.TestBase): def test_empty(self): data = self.get('/users') self.assertEquals({'users': []}, data) - def test_users(self): - counter1 = counter.Counter( - 'instance', - 'cumulative', - 1, - 'user-id', - 'project-id', - 'resource-id', - timestamp=datetime.datetime(2012, 7, 2, 10, 40), - resource_metadata={'display_name': 'test-server', - 'tag': 'self.counter', - } - ) - msg = meter.meter_message_from_counter(counter1, - cfg.CONF.metering_secret, - 'test_list_users', - ) - self.conn.record_metering_data(msg) - counter2 = counter.Counter( - 'instance', - 'cumulative', - 1, - 'user-id2', - 'project-id', - 'resource-id-alternate', - timestamp=datetime.datetime(2012, 7, 2, 10, 41), - resource_metadata={'display_name': 'test-server', - 'tag': 'self.counter2', - } - ) - msg2 = meter.meter_message_from_counter(counter2, - cfg.CONF.metering_secret, - 'test_list_users', - ) - self.conn.record_metering_data(msg2) +class TestListUsers(tests_api.TestBase): - data = self.get('/users') - self.assertEquals(['user-id', 'user-id2'], data['users']) + def setUp(self): + super(TestListUsers, self).setUp() - def test_with_source(self): counter1 = counter.Counter( 'instance', 'cumulative', @@ -113,5 +79,24 @@ class TestListUsers(tests_api.TestBase): ) self.conn.record_metering_data(msg2) + def test_users(self): + data = self.get('/users') + self.assertEquals(['user-id', 'user-id2'], data['users']) + + def test_users_non_admin(self): + data = self.get('/users', + headers={"X-Roles": "Member", + "X-User-Id": "user-id", + "X-Tenant-Id": "project-id"}) + self.assertEquals(['user-id'], data['users']) + + def test_with_source(self): data = self.get('/sources/test_list_users/users') self.assertEquals(['user-id'], data['users']) + + def test_with_source_non_admin(self): + data = self.get('/sources/test_list_users/users', + headers={"X-Roles": "Member", + "X-User-Id": "user-id", + "X-Tenant-Id": "project-id"}) + self.assertEquals(['user-id'], data['users']) diff --git a/tests/api/v1/test_max_project_volume.py b/tests/api/v1/test_max_project_volume.py index 3b97932fa..9b55cf234 100644 --- a/tests/api/v1/test_max_project_volume.py +++ b/tests/api/v1/test_max_project_volume.py @@ -3,6 +3,7 @@ # Copyright © 2012 New Dream Network, LLC (DreamHost) # # Author: Steven Berler +# Julien Danjou # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain @@ -60,6 +61,18 @@ class TestMaxProjectVolume(tests_api.TestBase): expected = {'volume': 7} assert data == expected + def test_no_time_bounds_non_admin(self): + data = self.get('/projects/project1/meters/volume.size/volume/max', + headers={"X-Roles": "Member", + "X-Tenant-Id": "project1"}) + self.assertEqual(data, {'volume': 7}) + + def test_no_time_bounds_wrong_tenant(self): + resp = self.get('/projects/project1/meters/volume.size/volume/max', + headers={"X-Roles": "Member", + "X-Tenant-Id": "?"}) + self.assertEqual(resp.status_code, 404) + def test_start_timestamp(self): data = self.get('/projects/project1/meters/volume.size/volume/max', start_timestamp='2012-09-25T11:30:00') diff --git a/tests/api/v1/test_max_resource_volume.py b/tests/api/v1/test_max_resource_volume.py index 5fb2ae85f..9032cdebb 100644 --- a/tests/api/v1/test_max_resource_volume.py +++ b/tests/api/v1/test_max_resource_volume.py @@ -60,6 +60,18 @@ class TestMaxResourceVolume(tests_api.TestBase): expected = {'volume': 7} assert data == expected + def test_no_time_bounds_non_admin(self): + data = self.get('/resources/resource-id/meters/volume.size/volume/max', + headers={"X-Roles": "Member", + "X-Tenant-Id": "project1"}) + self.assertEqual(data, {'volume': 7}) + + def test_no_time_bounds_wrong_tenant(self): + data = self.get('/resources/resource-id/meters/volume.size/volume/max', + headers={"X-Roles": "Member", + "X-Tenant-Id": "??"}) + self.assertEqual(data, {'volume': None}) + def test_start_timestamp(self): data = self.get('/resources/resource-id/meters/volume.size/volume/max', start_timestamp='2012-09-25T11:30:00') diff --git a/tests/api/v1/test_sum_project_volume.py b/tests/api/v1/test_sum_project_volume.py index 92e201796..c6e95d5bd 100644 --- a/tests/api/v1/test_sum_project_volume.py +++ b/tests/api/v1/test_sum_project_volume.py @@ -3,6 +3,7 @@ # Copyright © 2012 New Dream Network, LLC (DreamHost) # # Author: Steven Berler +# Julien Danjou # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain @@ -60,6 +61,18 @@ class TestSumProjectVolume(tests_api.TestBase): expected = {'volume': 5 + 6 + 7} assert data == expected + def test_no_time_bounds_non_admin(self): + data = self.get('/projects/project1/meters/volume.size/volume/sum', + headers={"X-Roles": "Member", + "X-Tenant-Id": "project1"}) + self.assertEqual(data, {'volume': 5 + 6 + 7}) + + def test_no_time_bounds_wrong_tenant(self): + resp = self.get('/projects/project1/meters/volume.size/volume/sum', + headers={"X-Roles": "Member", + "X-Tenant-Id": "???"}) + self.assertEqual(resp.status_code, 404) + def test_start_timestamp(self): data = self.get('/projects/project1/meters/volume.size/volume/sum', start_timestamp='2012-09-25T11:30:00') diff --git a/tests/api/v1/test_sum_resource_volume.py b/tests/api/v1/test_sum_resource_volume.py index 7f2082fd6..647d73fc5 100644 --- a/tests/api/v1/test_sum_resource_volume.py +++ b/tests/api/v1/test_sum_resource_volume.py @@ -3,6 +3,7 @@ # Copyright © 2012 New Dream Network, LLC (DreamHost) # # Author: Doug Hellmann +# Julien Danjou # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain @@ -60,6 +61,18 @@ class TestSumResourceVolume(tests_api.TestBase): expected = {'volume': 5 + 6 + 7} assert data == expected + def test_no_time_bounds_non_admin(self): + data = self.get('/resources/resource-id/meters/volume.size/volume/sum', + headers={"X-Roles": "Member", + "X-Tenant-Id": "project1"}) + self.assertEqual(data, {'volume': 5 + 6 + 7}) + + def test_no_time_bounds_wrong_tenant(self): + data = self.get('/resources/resource-id/meters/volume.size/volume/sum', + headers={"X-Roles": "Member", + "X-Tenant-Id": "?"}) + self.assertEqual(data, {'volume': None}) + def test_start_timestamp(self): data = self.get('/resources/resource-id/meters/volume.size/volume/sum', start_timestamp='2012-09-25T11:30:00')