statistics: allow filtering by users OU (#49670)

This commit is contained in:
Valentin Deniaud 2021-02-25 15:33:19 +01:00
parent c27792ecbe
commit dd3ed19adf
5 changed files with 97 additions and 27 deletions

View File

@ -1127,7 +1127,9 @@ class StatisticsSerializer(serializers.Serializer):
time_interval = serializers.ChoiceField(choices=TIME_INTERVAL_CHOICES, default='month')
service = ServiceOUField(child=serializers.SlugField(max_length=256), required=False)
ou = serializers.SlugField(required=False, allow_blank=False, max_length=256)
services_ou = serializers.SlugField(required=False, allow_blank=False, max_length=256)
users_ou = serializers.SlugField(required=False, allow_blank=False, max_length=256)
ou = serializers.SlugField(required=False, allow_blank=False, max_length=256) # legacy
start = serializers.DateTimeField(required=False, input_formats=['iso-8601', '%Y-%m-%d'])
end = serializers.DateTimeField(required=False, input_formats=['iso-8601', '%Y-%m-%d'])
@ -1173,8 +1175,14 @@ class StatisticsAPI(ViewSet):
for action in self.get_extra_actions():
url = self.reverse_action(action.url_name)
filters = common_filters.copy()
if 'ou' in action.filters:
filters.append({'id': 'ou', 'label': _('Organizational Unit'), 'options': ous})
if 'services_ou' in action.filters:
filters.append(
{'id': 'services_ou', 'label': _('Services organizational unit'), 'options': ous}
)
if 'users_ou' in action.filters:
filters.append(
{'id': 'users_ou', 'label': _('Users organizational unit'), 'options': ous}
)
if 'service' in action.filters:
filters.append({'id': 'service', 'label': _('Service'), 'options': services})
data = {
@ -1208,19 +1216,25 @@ class StatisticsAPI(ViewSet):
}
allowed_filters = getattr(self, self.action).filters
service, ou = data.get('service'), data.get('ou')
service = data.get('service')
services_ou = data.get('services_ou') or data.get('ou') # legacy 'ou' filter
users_ou = data.get('users_ou')
if service and 'service' in allowed_filters:
service_slug, ou_slug = service
kwargs['service'] = get_object_or_404(Service, slug=service_slug, ou__slug=ou_slug)
elif ou and 'ou' in allowed_filters:
kwargs['ou'] = get_object_or_404(get_ou_model(), slug=ou)
elif services_ou and 'services_ou' in allowed_filters:
kwargs['services_ou'] = get_object_or_404(get_ou_model(), slug=services_ou)
if users_ou and 'users_ou' in allowed_filters:
kwargs['users_ou'] = get_object_or_404(get_ou_model(), slug=users_ou)
return Response({
'data': getattr(klass, method)(**kwargs),
'err': 0,
})
@stat(name=_('Login count by authentication type'), filters=('ou', 'service'))
@stat(name=_('Login count by authentication type'), filters=('services_ou', 'users_ou', 'service'))
def login(self, request):
return self.get_statistics(request, UserLogin, 'get_method_statistics')
@ -1232,7 +1246,7 @@ class StatisticsAPI(ViewSet):
def service_ou_login(self, request):
return self.get_statistics(request, UserLogin, 'get_service_ou_statistics')
@stat(name=_('Registration count by type'), filters=('ou', 'service'))
@stat(name=_('Registration count by type'), filters=('services_ou', 'users_ou', 'service'))
def registration(self, request):
return self.get_statistics(request, UserRegistration, 'get_method_statistics')

View File

@ -117,6 +117,7 @@ class EventTypeDefinition(metaclass=EventTypeDefinitionMeta):
group_by_field=None,
group_by_references=False,
which_references=None,
users_ou=None,
start=None,
end=None,
):
@ -145,6 +146,9 @@ class EventTypeDefinition(metaclass=EventTypeDefinitionMeta):
if which_references:
qs = qs.which_references(which_references)
if users_ou:
qs = qs.filter(user__ou=users_ou)
if group_by_references:
values.append('reference_ids')

View File

@ -53,12 +53,17 @@ class EventTypeWithHow(EventTypeWithService):
super().record(user=user, session=session, service=service, data={'how': how})
@classmethod
def get_method_statistics(cls, group_by_time, service=None, ou=None, start=None, end=None):
if ou:
service = Service.objects.filter(ou=ou)
def get_method_statistics(cls, group_by_time, service=None, services_ou=None, users_ou=None, start=None, end=None):
if services_ou and not service:
service = Service.objects.filter(ou=services_ou)
qs = cls.get_statistics(
group_by_time=group_by_time, group_by_field='how', which_references=service, start=start, end=end
group_by_time=group_by_time,
group_by_field='how',
which_references=service,
users_ou=users_ou,
start=start,
end=end
)
stats = Statistics(qs, time_interval=group_by_time)

View File

@ -2064,8 +2064,13 @@ def test_api_statistics_list(app, admin):
"default": "month",
},
{
'id': 'ou',
'label': 'Organizational Unit',
'id': 'services_ou',
'label': 'Services organizational unit',
'options': [{'id': 'default', 'label': 'Default organizational unit'}],
},
{
'id': 'users_ou',
'label': 'Users organizational unit',
'options': [{'id': 'default', 'label': 'Default organizational unit'}],
},
{'id': 'service', 'label': 'Service', 'options': []},
@ -2093,8 +2098,8 @@ def test_api_statistics_list(app, admin):
service = Service.objects.create(name='Service1', slug='service1', ou=get_default_ou())
service = Service.objects.create(name='Service2', slug='service2', ou=get_default_ou())
login_stats['filters'][2]['options'].append({'id': 'service1 default', 'label': 'Service1'})
login_stats['filters'][2]['options'].append({'id': 'service2 default', 'label': 'Service2'})
login_stats['filters'][3]['options'].append({'id': 'service1 default', 'label': 'Service1'})
login_stats['filters'][3]['options'].append({'id': 'service2 default', 'label': 'Service2'})
resp = app.get('/api/statistics/', headers=headers)
assert login_stats in resp.json['data']
@ -2105,13 +2110,15 @@ def test_api_statistics_list(app, admin):
'event_type_name,event_name', [('user.login', 'login'), ('user.registration', 'registration')]
)
def test_api_statistics(app, admin, freezer, event_type_name, event_name):
OU = get_ou_model()
headers = basic_authorization_header(admin)
resp = app.get('/api/statistics/login/?time_interval=month', headers=headers)
assert resp.json == {"data": {"series": [], "x_labels": []}, "err": 0}
user = User.objects.create(username='john.doe', email='john.doe@example.com')
portal = Service.objects.create(name='portal', slug='portal', ou=get_default_ou())
user = User.objects.create(username='john.doe', email='john.doe@example.com', ou=get_default_ou())
ou = OU.objects.create(name='Second OU', slug='second')
portal = Service.objects.create(name='portal', slug='portal', ou=ou)
agendas = Service.objects.create(name='agendas', slug='agendas', ou=get_default_ou())
method = {'how': 'password-on-https'}
@ -2122,7 +2129,7 @@ def test_api_statistics(app, admin, freezer, event_type_name, event_name):
freezer.move_to('2020-02-03 12:00')
event = Event.objects.create(type=event_type, references=[portal], data=method)
event = Event.objects.create(type=event_type, references=[agendas], data=method)
event = Event.objects.create(type=event_type, references=[agendas, user], user=user, data=method)
freezer.move_to('2020-03-04 13:00')
event = Event.objects.create(type=event_type, references=[agendas], data=method)
@ -2143,14 +2150,36 @@ def test_api_statistics(app, admin, freezer, event_type_name, event_name):
data['series'].sort(key=lambda x: x['label'])
assert month_data == data
resp = app.get('/api/statistics/%s/?time_interval=month&ou=default' % event_name, headers=headers)
resp = app.get(
'/api/statistics/%s/?time_interval=month&services_ou=default' % event_name, headers=headers
)
data = resp.json['data']
data['series'].sort(key=lambda x: x['label'])
assert data == {
'x_labels': ['2020-02', '2020-03'],
'series': [{'label': 'FranceConnect', 'data': [None, 1]}, {'label': 'password', 'data': [2, 1]}],
'series': [{'label': 'password', 'data': [1, 1]}],
}
# legacy way to filter by service OU
services_ou_data = data
resp = app.get('/api/statistics/%s/?time_interval=month&ou=default' % event_name, headers=headers)
data = resp.json['data']
data['series'].sort(key=lambda x: x['label'])
assert services_ou_data == data
resp = app.get(
'/api/statistics/%s/?time_interval=month&users_ou=default&service=agendas default' % event_name, headers=headers
)
data = resp.json['data']
assert data == {
'x_labels': ['2020-02'],
'series': [{'label': 'password', 'data': [1]}],
}
resp = app.get('/api/statistics/%s/?time_interval=month&users_ou=default' % event_name, headers=headers)
data = resp.json['data']
assert data == {'x_labels': ['2020-02'], 'series': [{'label': 'password', 'data': [1]}]}
resp = app.get(
'/api/statistics/%s/?time_interval=month&service=agendas default' % event_name, headers=headers
)
@ -2180,7 +2209,7 @@ def test_api_statistics(app, admin, freezer, event_type_name, event_name):
assert data == {'x_labels': ['2020-02'], 'series': [{'label': 'password', 'data': [2]}]}
resp = app.get(
'/api/statistics/%s/?time_interval=year&service=portal default' % event_name, headers=headers
'/api/statistics/%s/?time_interval=year&service=portal second' % event_name, headers=headers
)
data = resp.json['data']
data['series'].sort(key=lambda x: x['label'])
@ -2199,9 +2228,13 @@ def test_api_statistics(app, admin, freezer, event_type_name, event_name):
resp = app.get('/api/statistics/service_ou_%s/?time_interval=month' % event_name, headers=headers)
data = resp.json['data']
data['series'].sort(key=lambda x: x['label'])
assert data == {
'x_labels': ['2020-02', '2020-03'],
'series': [{'label': 'Default organizational unit', 'data': [2, 2]}],
'series': [
{'label': 'Default organizational unit', 'data': [1, 1]},
{'label': 'Second OU', 'data': [1, 1]},
],
}

View File

@ -450,9 +450,9 @@ def test_message_in_context_exception_handling(db, some_event_types, caplog):
@pytest.mark.parametrize('event_type_name', ['user.login', 'user.registration'])
def test_statistics(db, event_type_name, freezer):
user = User.objects.create(username='john.doe', email='john.doe@example.com')
user2 = User.objects.create(username='jane.doe', email='jane.doe@example.com')
user = User.objects.create(username='john.doe', email='john.doe@example.com', ou=get_default_ou())
ou = OU.objects.create(name='Second OU')
user2 = User.objects.create(username='jane.doe', email='jane.doe@example.com', ou=ou)
portal = Service.objects.create(name='portal', slug='portal', ou=ou)
agendas = Service.objects.create(name='agendas', slug='agendas', ou=get_default_ou())
@ -514,19 +514,26 @@ def test_statistics(db, event_type_name, freezer):
],
}
stats = event_type_definition.get_method_statistics('month', ou=get_default_ou())
stats = event_type_definition.get_method_statistics('month', services_ou=get_default_ou())
assert stats == {
'x_labels': ['2020-03'],
'series': [{'label': 'password', 'data': [2]},],
}
stats = event_type_definition.get_method_statistics('month', ou=ou)
stats = event_type_definition.get_method_statistics('month', services_ou=ou)
stats['series'].sort(key=lambda x: x['label'])
assert stats == {
'x_labels': ['2020-02', '2020-03'],
'series': [{'label': 'FranceConnect', 'data': [2, None]}, {'label': 'password', 'data': [2, 1]}],
}
stats = event_type_definition.get_method_statistics('month', users_ou=ou)
stats['series'].sort(key=lambda x: x['label'])
assert stats == {
'x_labels': ['2020-02'],
'series': [{'label': 'FranceConnect', 'data': [1]}, {'label': 'password', 'data': [1]}],
}
stats = event_type_definition.get_method_statistics('month', service=portal)
stats['series'].sort(key=lambda x: x['label'])
assert stats == {
@ -534,6 +541,13 @@ def test_statistics(db, event_type_name, freezer):
'series': [{'label': 'FranceConnect', 'data': [2, None]}, {'label': 'password', 'data': [2, 1]}],
}
stats = event_type_definition.get_method_statistics('month', service=agendas, users_ou=get_default_ou())
stats['series'].sort(key=lambda x: x['label'])
assert stats == {
'x_labels': ['2020-03'],
'series': [{'label': 'password', 'data': [1]}],
}
stats = event_type_definition.get_method_statistics('year')
stats['series'].sort(key=lambda x: x['label'])
assert stats == {