Merge "restrict admin event access"

This commit is contained in:
Jenkins 2015-09-09 15:04:19 +00:00 committed by Gerrit Code Review
commit 4b1ead72f9
6 changed files with 102 additions and 28 deletions

View File

@ -157,16 +157,19 @@ class Event(base.Base):
) )
def _add_user_proj_filter(): def _build_rbac_query_filters():
traits_filter = [] filters = {'t_filter': [], 'admin_proj': None}
# Returns user_id, proj_id for non-admins # Returns user_id, proj_id for non-admins
user_id, proj_id = rbac.get_limited_to(pecan.request.headers) user_id, proj_id = rbac.get_limited_to(pecan.request.headers)
# If non-admin, filter events by user and project # If non-admin, filter events by user and project
if (user_id and proj_id): if user_id and proj_id:
traits_filter.append({"key": "project_id", "string": proj_id, filters['t_filter'].append({"key": "project_id", "string": proj_id,
"op": "eq"}) "op": "eq"})
traits_filter.append({"key": "user_id", "string": user_id, "op": "eq"}) filters['t_filter'].append({"key": "user_id", "string": user_id,
return traits_filter "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): def _event_query_to_event_filter(q):
@ -176,7 +179,9 @@ def _event_query_to_event_filter(q):
'start_timestamp': None, 'start_timestamp': None,
'end_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: for i in q:
if not i.op: if not i.op:
@ -193,7 +198,8 @@ def _event_query_to_event_filter(q):
traits_filter.append({"key": i.field, traits_filter.append({"key": i.field,
trait_type: i._get_value_as_type(), trait_type: i._get_value_as_type(),
"op": i.op}) "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): class TraitsController(rest.RestController):
@ -279,8 +285,11 @@ class EventsController(rest.RestController):
:param message_id: Message ID of the Event to be returned :param message_id: Message ID of the Event to be returned
""" """
rbac.enforce("events:show", pecan.request) 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, event_filter = storage.EventFilter(traits_filter=t_filter,
admin_proj=admin_proj,
message_id=message_id) message_id=message_id)
events = [event for event events = [event for event
in pecan.request.event_storage_conn.get_events(event_filter)] in pecan.request.event_storage_conn.get_events(event_filter)]

View File

@ -141,9 +141,10 @@ class Connection(base.Connection):
q_args['doc_type'] = ev_filter.event_type q_args['doc_type'] = ev_filter.event_type
if ev_filter.message_id: if ev_filter.message_id:
filters.append({'term': {'_id': 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 = [] trait_filters = []
for t_filter in ev_filter.traits_filter: or_cond = []
for t_filter in ev_filter.traits_filter or []:
value = None value = None
for val_type in ['integer', 'string', 'float', 'datetime']: for val_type in ['integer', 'string', 'float', 'datetime']:
if t_filter.get(val_type): if t_filter.get(val_type):
@ -164,9 +165,13 @@ class Connection(base.Connection):
if t_filter.get('op') == 'ne': if t_filter.get('op') == 'ne':
tf = {"not": tf} tf = {"not": tf}
trait_filters.append(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( filters.append(
{'nested': {'path': 'traits', 'query': {'filtered': { {'nested': {'path': 'traits', 'query': {'filtered': {
'filter': {'bool': {'must': trait_filters}}}}}}) 'filter': {'bool': {'must': trait_filters,
'should': or_cond}}}}}})
q_args['body'] = {'query': {'filtered': q_args['body'] = {'query': {'filtered':
{'filter': {'bool': {'must': filters}}}}} {'filter': {'bool': {'must': filters}}}}}

View File

@ -265,6 +265,15 @@ class Connection(base.Connection):
if trait_subq is not None: if trait_subq is not None:
query = query.join(trait_subq, query = query.join(trait_subq,
trait_subq.c.ev_id == models.Event.id) 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: if event_filter_conditions:
query = query.filter(sa.and_(*event_filter_conditions)) query = query.filter(sa.and_(*event_filter_conditions))
@ -274,7 +283,7 @@ class Connection(base.Connection):
for (id_, generated, message_id, for (id_, generated, message_id,
desc, raw) in query.add_columns( desc, raw) in query.add_columns(
models.Event.generated, models.Event.message_id, 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, event_list[id_] = api_models.Event(message_id, desc,
generated, [], raw) generated, [], raw)
# Query all traits related to events. # Query all traits related to events.

View File

@ -214,6 +214,7 @@ class EventFilter(object):
:param end_timestamp: UTC end datetime (mandatory) :param end_timestamp: UTC end datetime (mandatory)
:param event_type: the name of the event. None for all. :param event_type: the name of the event. None for all.
:param message_id: the message_id 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. :param traits_filter: the trait filter dicts, all of which are optional.
This parameter is a list of dictionaries that specify trait values: 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, 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.start_timestamp = utils.sanitize_timestamp(start_timestamp)
self.end_timestamp = utils.sanitize_timestamp(end_timestamp) self.end_timestamp = utils.sanitize_timestamp(end_timestamp)
self.message_id = message_id self.message_id = message_id
self.event_type = event_type self.event_type = event_type
self.traits_filter = traits_filter or [] self.traits_filter = traits_filter or []
self.admin_proj = admin_proj
def __repr__(self): def __repr__(self):
return ("<EventFilter(start_timestamp: %s," return ("<EventFilter(start_timestamp: %s,"

View File

@ -88,18 +88,18 @@ def make_events_query_from_filter(event_filter):
:param event_filter: storage.EventFilter object. :param event_filter: storage.EventFilter object.
""" """
q = {} query = {}
q_list = []
ts_range = make_timestamp_range(event_filter.start_timestamp, ts_range = make_timestamp_range(event_filter.start_timestamp,
event_filter.end_timestamp) event_filter.end_timestamp)
if ts_range: if ts_range:
q['timestamp'] = ts_range q_list.append({'timestamp': ts_range})
if event_filter.event_type: 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: if event_filter.message_id:
q['_id'] = event_filter.message_id q_list.append({'_id': event_filter.message_id})
if event_filter.traits_filter: if event_filter.traits_filter:
q.setdefault('traits')
for trait_filter in event_filter.traits_filter: for trait_filter in event_filter.traits_filter:
op = trait_filter.pop('op', 'eq') op = trait_filter.pop('op', 'eq')
dict_query = {} dict_query = {}
@ -116,14 +116,17 @@ def make_events_query_from_filter(event_filter):
v if op == 'eq' v if op == 'eq'
else {OP_SIGN[op]: v}) else {OP_SIGN[op]: v})
dict_query = {'$elemMatch': dict_query} dict_query = {'$elemMatch': dict_query}
if q['traits'] is None: q_list.append({'traits': dict_query})
q['traits'] = dict_query if event_filter.admin_proj:
elif q.get('$and') is None: q_list.append({'$or': [
q.setdefault('$and', [{'traits': q.pop('traits')}, {'traits': {'$not': {'$elemMatch': {'trait_name': 'project_id'}}}},
{'traits': dict_query}]) {'traits': {
else: '$elemMatch': {'trait_name': 'project_id',
q['$and'].append({'traits': dict_query}) 'trait_value': event_filter.admin_proj}}}]})
return q if q_list:
query = {'$and': q_list}
return query
def make_query_from_filter(sample_filter, require_meter=True): def make_query_from_filter(sample_filter, require_meter=True):

View File

@ -540,6 +540,51 @@ class AclRestrictedEventTestBase(v2.FunctionalTest,
expect_errors=True) expect_errors=True)
self.assertEqual(404, data.status_int) 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, class EventRestrictionTestBase(v2.FunctionalTest,
tests_db.MixinTestsWithBackendScenarios): tests_db.MixinTestsWithBackendScenarios):