diff --git a/ceilometer/api/controllers/v2/events.py b/ceilometer/api/controllers/v2/events.py index b314dde0e4..1b93ac7c28 100644 --- a/ceilometer/api/controllers/v2/events.py +++ b/ceilometer/api/controllers/v2/events.py @@ -157,16 +157,19 @@ class Event(base.Base): ) -def _add_user_proj_filter(): - traits_filter = [] +def _build_rbac_query_filters(): + filters = {'t_filter': [], 'admin_proj': None} # Returns user_id, proj_id for non-admins user_id, proj_id = rbac.get_limited_to(pecan.request.headers) # If non-admin, filter events by user and project - if (user_id and proj_id): - traits_filter.append({"key": "project_id", "string": proj_id, - "op": "eq"}) - traits_filter.append({"key": "user_id", "string": user_id, "op": "eq"}) - return traits_filter + if user_id and proj_id: + filters['t_filter'].append({"key": "project_id", "string": proj_id, + "op": "eq"}) + filters['t_filter'].append({"key": "user_id", "string": user_id, + "op": "eq"}) + elif not user_id and not proj_id: + filters['admin_proj'] = pecan.request.headers.get('X-Project-Id') + return filters def _event_query_to_event_filter(q): @@ -176,7 +179,9 @@ def _event_query_to_event_filter(q): 'start_timestamp': None, 'end_timestamp': None } - traits_filter = _add_user_proj_filter() + filters = _build_rbac_query_filters() + traits_filter = filters['t_filter'] + admin_proj = filters['admin_proj'] for i in q: if not i.op: @@ -193,7 +198,8 @@ def _event_query_to_event_filter(q): traits_filter.append({"key": i.field, trait_type: i._get_value_as_type(), "op": i.op}) - return storage.EventFilter(traits_filter=traits_filter, **evt_model_filter) + return storage.EventFilter(traits_filter=traits_filter, + admin_proj=admin_proj, **evt_model_filter) class TraitsController(rest.RestController): @@ -279,8 +285,11 @@ class EventsController(rest.RestController): :param message_id: Message ID of the Event to be returned """ rbac.enforce("events:show", pecan.request) - t_filter = _add_user_proj_filter() + filters = _build_rbac_query_filters() + t_filter = filters['t_filter'] + admin_proj = filters['admin_proj'] event_filter = storage.EventFilter(traits_filter=t_filter, + admin_proj=admin_proj, message_id=message_id) events = [event for event in pecan.request.event_storage_conn.get_events(event_filter)] diff --git a/ceilometer/event/storage/impl_elasticsearch.py b/ceilometer/event/storage/impl_elasticsearch.py index 9a992ccc6a..ab7ee8670a 100644 --- a/ceilometer/event/storage/impl_elasticsearch.py +++ b/ceilometer/event/storage/impl_elasticsearch.py @@ -141,9 +141,10 @@ class Connection(base.Connection): q_args['doc_type'] = ev_filter.event_type if ev_filter.message_id: filters.append({'term': {'_id': ev_filter.message_id}}) - if ev_filter.traits_filter: + if ev_filter.traits_filter or ev_filter.admin_proj: trait_filters = [] - for t_filter in ev_filter.traits_filter: + or_cond = [] + for t_filter in ev_filter.traits_filter or []: value = None for val_type in ['integer', 'string', 'float', 'datetime']: if t_filter.get(val_type): @@ -164,9 +165,13 @@ class Connection(base.Connection): if t_filter.get('op') == 'ne': tf = {"not": tf} trait_filters.append(tf) + if ev_filter.admin_proj: + or_cond = [{'missing': {'field': 'project_id'}}, + {'term': {'project_id': ev_filter.admin_proj}}] filters.append( {'nested': {'path': 'traits', 'query': {'filtered': { - 'filter': {'bool': {'must': trait_filters}}}}}}) + 'filter': {'bool': {'must': trait_filters, + 'should': or_cond}}}}}}) q_args['body'] = {'query': {'filtered': {'filter': {'bool': {'must': filters}}}}} diff --git a/ceilometer/event/storage/impl_sqlalchemy.py b/ceilometer/event/storage/impl_sqlalchemy.py index a34831a56a..e0497b420e 100644 --- a/ceilometer/event/storage/impl_sqlalchemy.py +++ b/ceilometer/event/storage/impl_sqlalchemy.py @@ -265,6 +265,15 @@ class Connection(base.Connection): if trait_subq is not None: query = query.join(trait_subq, trait_subq.c.ev_id == models.Event.id) + if event_filter.admin_proj: + admin_q = session.query(models.TraitText).filter( + models.TraitText.key == 'project_id') + query = query.filter(sa.or_(~sa.exists().where( + models.Event.id == admin_q.subquery().c.event_id), + sa.and_( + models.TraitText.key == 'project_id', + models.TraitText.value == event_filter.admin_proj, + models.Event.id == models.TraitText.event_id))) if event_filter_conditions: query = query.filter(sa.and_(*event_filter_conditions)) @@ -274,7 +283,7 @@ class Connection(base.Connection): for (id_, generated, message_id, desc, raw) in query.add_columns( models.Event.generated, models.Event.message_id, - models.EventType.desc, models.Event.raw).all(): + models.EventType.desc, models.Event.raw).distinct().all(): event_list[id_] = api_models.Event(message_id, desc, generated, [], raw) # Query all traits related to events. diff --git a/ceilometer/storage/__init__.py b/ceilometer/storage/__init__.py index 7e15b0b337..5df4cec8dd 100644 --- a/ceilometer/storage/__init__.py +++ b/ceilometer/storage/__init__.py @@ -214,6 +214,7 @@ class EventFilter(object): :param end_timestamp: UTC end datetime (mandatory) :param event_type: the name of the event. None for all. :param message_id: the message_id of the event. None for all. + :param admin_proj: the project_id of admin role. None if non-admin user. :param traits_filter: the trait filter dicts, all of which are optional. This parameter is a list of dictionaries that specify trait values: @@ -228,12 +229,14 @@ class EventFilter(object): """ def __init__(self, start_timestamp=None, end_timestamp=None, - event_type=None, message_id=None, traits_filter=None): + event_type=None, message_id=None, traits_filter=None, + admin_proj=None): self.start_timestamp = utils.sanitize_timestamp(start_timestamp) self.end_timestamp = utils.sanitize_timestamp(end_timestamp) self.message_id = message_id self.event_type = event_type self.traits_filter = traits_filter or [] + self.admin_proj = admin_proj def __repr__(self): return ("<EventFilter(start_timestamp: %s," diff --git a/ceilometer/storage/mongo/utils.py b/ceilometer/storage/mongo/utils.py index 08fc7768a9..8cc588b4b3 100644 --- a/ceilometer/storage/mongo/utils.py +++ b/ceilometer/storage/mongo/utils.py @@ -88,18 +88,18 @@ def make_events_query_from_filter(event_filter): :param event_filter: storage.EventFilter object. """ - q = {} + query = {} + q_list = [] ts_range = make_timestamp_range(event_filter.start_timestamp, event_filter.end_timestamp) if ts_range: - q['timestamp'] = ts_range + q_list.append({'timestamp': ts_range}) if event_filter.event_type: - q['event_type'] = event_filter.event_type + q_list.append({'event_type': event_filter.event_type}) if event_filter.message_id: - q['_id'] = event_filter.message_id + q_list.append({'_id': event_filter.message_id}) if event_filter.traits_filter: - q.setdefault('traits') for trait_filter in event_filter.traits_filter: op = trait_filter.pop('op', 'eq') dict_query = {} @@ -116,14 +116,17 @@ def make_events_query_from_filter(event_filter): v if op == 'eq' else {OP_SIGN[op]: v}) dict_query = {'$elemMatch': dict_query} - if q['traits'] is None: - q['traits'] = dict_query - elif q.get('$and') is None: - q.setdefault('$and', [{'traits': q.pop('traits')}, - {'traits': dict_query}]) - else: - q['$and'].append({'traits': dict_query}) - return q + q_list.append({'traits': dict_query}) + if event_filter.admin_proj: + q_list.append({'$or': [ + {'traits': {'$not': {'$elemMatch': {'trait_name': 'project_id'}}}}, + {'traits': { + '$elemMatch': {'trait_name': 'project_id', + 'trait_value': event_filter.admin_proj}}}]}) + if q_list: + query = {'$and': q_list} + + return query def make_query_from_filter(sample_filter, require_meter=True): diff --git a/ceilometer/tests/functional/api/v2/test_event_scenarios.py b/ceilometer/tests/functional/api/v2/test_event_scenarios.py index aba01f0d7e..76ad707577 100644 --- a/ceilometer/tests/functional/api/v2/test_event_scenarios.py +++ b/ceilometer/tests/functional/api/v2/test_event_scenarios.py @@ -540,6 +540,51 @@ class AclRestrictedEventTestBase(v2.FunctionalTest, expect_errors=True) self.assertEqual(404, data.status_int) + @tests_db.run_with('sqlite', 'mysql', 'pgsql', 'mongodb', 'es', 'db2') + def test_admin_access(self): + a_headers = {"X-Roles": "admin", + "X-User-Id": self.admin_user_id, + "X-Project-Id": self.admin_proj_id} + data = self.get_json('/events', headers=a_headers) + self.assertEqual(2, len(data)) + self.assertEqual(set(['empty_ev', 'admin_ev']), + set(ev['event_type'] for ev in data)) + + @tests_db.run_with('sqlite', 'mysql', 'pgsql', 'mongodb', 'es', 'db2') + def test_admin_access_trait_filter(self): + a_headers = {"X-Roles": "admin", + "X-User-Id": self.admin_user_id, + "X-Project-Id": self.admin_proj_id} + data = self.get_json('/events', headers=a_headers, + q=[{'field': 'random', + 'value': 'blah', + 'type': 'string', + 'op': 'eq'}]) + self.assertEqual(1, len(data)) + self.assertEqual('empty_ev', data[0]['event_type']) + + @tests_db.run_with('sqlite', 'mysql', 'pgsql', 'mongodb', 'es', 'db2') + def test_admin_access_single(self): + a_headers = {"X-Roles": "admin", + "X-User-Id": self.admin_user_id, + "X-Project-Id": self.admin_proj_id} + data = self.get_json('/events/1', headers=a_headers) + self.assertEqual('empty_ev', data['event_type']) + data = self.get_json('/events/2', headers=a_headers) + self.assertEqual('admin_ev', data['event_type']) + + @tests_db.run_with('sqlite', 'mysql', 'pgsql', 'mongodb', 'es', 'db2') + def test_admin_access_trait_filter_no_access(self): + a_headers = {"X-Roles": "admin", + "X-User-Id": self.admin_user_id, + "X-Project-Id": self.admin_proj_id} + data = self.get_json('/events', headers=a_headers, + q=[{'field': 'user_id', + 'value': self.user_id, + 'type': 'string', + 'op': 'eq'}]) + self.assertEqual(0, len(data)) + class EventRestrictionTestBase(v2.FunctionalTest, tests_db.MixinTestsWithBackendScenarios):