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():
|
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)]
|
||||||
|
@ -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}}}}}
|
||||||
|
@ -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.
|
||||||
|
@ -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,"
|
||||||
|
@ -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):
|
||||||
|
@ -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):
|
||||||
|
Loading…
Reference in New Issue
Block a user