Merge "restrict admin event access"
This commit is contained in:
commit
4b1ead72f9
@ -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,
|
||||
if user_id and proj_id:
|
||||
filters['t_filter'].append({"key": "project_id", "string": proj_id,
|
||||
"op": "eq"})
|
||||
traits_filter.append({"key": "user_id", "string": user_id, "op": "eq"})
|
||||
return traits_filter
|
||||
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)]
|
||||
|
@ -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}}}}}
|
||||
|
@ -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.
|
||||
|
@ -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,"
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
Loading…
Reference in New Issue
Block a user