From 64c54186eff000762c291758973ca8e5db28f606 Mon Sep 17 00:00:00 2001 From: NavaL Date: Fri, 24 Jun 2016 18:37:25 +0200 Subject: Issue #694 add an admin restricted resource for user stats --- service/pixelated/config/services.py | 6 ++ service/pixelated/resources/__init__.py | 4 ++ service/pixelated/resources/root_resource.py | 3 + service/pixelated/resources/users.py | 30 ++++++++++ service/test/integration/test_users_count.py | 47 +++++++++++++++ .../test/support/integration/multi_user_client.py | 1 + .../test_smtp_client_certificate.py | 2 +- service/test/unit/config/test_services.py | 12 +++- service/test/unit/resources/test_users_resource.py | 70 ++++++++++++++++++++++ 9 files changed, 173 insertions(+), 2 deletions(-) create mode 100644 service/pixelated/resources/users.py create mode 100644 service/test/integration/test_users_count.py create mode 100644 service/test/unit/resources/test_users_resource.py diff --git a/service/pixelated/config/services.py b/service/pixelated/config/services.py index b36eb0bb..a49e1df9 100644 --- a/service/pixelated/config/services.py +++ b/service/pixelated/config/services.py @@ -108,6 +108,9 @@ class ServicesFactory(object): def add_session(self, user_id, services): self._services_by_user[user_id] = services + def online_sessions(self): + return len(self._services_by_user.keys()) + @defer.inlineCallbacks def create_services_from(self, leap_session): _services = Services(leap_session) @@ -131,3 +134,6 @@ class SingleUserServicesFactory(object): def destroy_session(self, user_id, using_email=False): reactor.stop() + + def online_sessions(self): + return 1 diff --git a/service/pixelated/resources/__init__.py b/service/pixelated/resources/__init__.py index 0015db16..0bd34619 100644 --- a/service/pixelated/resources/__init__.py +++ b/service/pixelated/resources/__init__.py @@ -77,6 +77,10 @@ class BaseResource(Resource): def get_session(self, request): return IPixelatedSession(request.getSession()) + def is_admin(self, request): + services = self._services(request) + return services._leap_session.user_auth.is_admin() + def _services(self, request): user_id = self._get_user_id_from_request(request) return self._services_factory.services(user_id) diff --git a/service/pixelated/resources/root_resource.py b/service/pixelated/resources/root_resource.py index aacf2b61..c9808a03 100644 --- a/service/pixelated/resources/root_resource.py +++ b/service/pixelated/resources/root_resource.py @@ -33,6 +33,8 @@ from pixelated.resources.tags_resource import TagsResource from pixelated.resources.keys_resource import KeysResource from twisted.web.static import File +from pixelated.resources.users import UsersResource + CSRF_TOKEN_LENGTH = 32 MODE_STARTUP = 1 @@ -90,6 +92,7 @@ class RootResource(BaseResource): self._child_resources.add('mail', MailResource(self._services_factory)) self._child_resources.add('feedback', FeedbackResource(self._services_factory)) self._child_resources.add('user-settings', UserSettingsResource(self._services_factory)) + self._child_resources.add('users', UsersResource(self._services_factory)) self._child_resources.add(LoginResource.BASE_URL, LoginResource(self._services_factory, portal, disclaimer_banner=disclaimer_banner)) self._child_resources.add(LogoutResource.BASE_URL, LogoutResource(self._services_factory)) diff --git a/service/pixelated/resources/users.py b/service/pixelated/resources/users.py new file mode 100644 index 00000000..a3e6118e --- /dev/null +++ b/service/pixelated/resources/users.py @@ -0,0 +1,30 @@ +# +# Copyright (c) 2016 ThoughtWorks, Inc. +# +# Pixelated is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pixelated is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Pixelated. If not, see . + +from pixelated.resources import respond_json_deferred, BaseResource, respond_json, UnAuthorizedResource +from twisted.web import server + + +class UsersResource(BaseResource): + isLeaf = True + + def __init__(self, services_factory): + BaseResource.__init__(self, services_factory) + + def render_GET(self, request): + if self.is_admin(request): + return respond_json({"count": self._services_factory.online_sessions()}, request) + return UnAuthorizedResource().render_GET(request) diff --git a/service/test/integration/test_users_count.py b/service/test/integration/test_users_count.py new file mode 100644 index 00000000..d06ffd39 --- /dev/null +++ b/service/test/integration/test_users_count.py @@ -0,0 +1,47 @@ +# +# Copyright (c) 2014 ThoughtWorks, Inc. +# +# Pixelated is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pixelated is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Pixelated. If not, see . +import json + +from mockito import verify +from mockito import when +from twisted.internet import defer + +from test.support.integration.multi_user_client import MultiUserClient +from test.support.integration.soledad_test_base import SoledadTestBase + + +class UsersResourceTest(MultiUserClient, SoledadTestBase): + + @defer.inlineCallbacks + def wait_for_session_user_id_to_finish(self): + yield self.adaptor.initialize_store(self.soledad) + + @defer.inlineCallbacks + def test_online_users_count_uses_leap_auth_privileges(self): + + response, login_request = yield self.login() + yield response + + yield self.wait_for_session_user_id_to_finish() + + when(self.user_auth).is_admin().thenReturn(True) + response, request = self.get("/users", json.dumps({'csrftoken': [login_request.getCookie('XSRF-TOKEN')]}), + from_request=login_request, as_json=False) + yield response + + self.assertEqual(200, request.code) # redirected + self.assertEqual('{"count": 1}', request.getWrittenData()) # redirected + verify(self.user_auth).is_admin() diff --git a/service/test/support/integration/multi_user_client.py b/service/test/support/integration/multi_user_client.py index 656e0901..d6133e64 100644 --- a/service/test/support/integration/multi_user_client.py +++ b/service/test/support/integration/multi_user_client.py @@ -60,6 +60,7 @@ class MultiUserClient(AppTestClient): leap_session.fresh_account = False self.leap_session = leap_session self.services = self._test_account.services + self.user_auth = user_auth self._set_leap_srp_auth(username, password, user_auth) when(LeapSessionFactory).create(username, password, user_auth).thenReturn(leap_session) diff --git a/service/test/unit/bitmask_libraries/test_smtp_client_certificate.py b/service/test/unit/bitmask_libraries/test_smtp_client_certificate.py index 58a7525f..e938d6f5 100644 --- a/service/test/unit/bitmask_libraries/test_smtp_client_certificate.py +++ b/service/test/unit/bitmask_libraries/test_smtp_client_certificate.py @@ -30,7 +30,7 @@ class TestSmtpClientCertificate(unittest.TestCase): self.tmp_dir = tempdir.TempDir() self.provider = mock() self.provider.domain = 'some-provider.tld' - self.auth = SRPSession('username', 'token', 'uuid', 'session_id') + self.auth = SRPSession('username', 'token', 'uuid', 'session_id', {}) self.pem_path = os.path.join(self.tmp_dir.name, 'providers', 'some-provider.tld', 'keys', 'client', 'smtp.pem') self.downloader = mock() when(session).SmtpCertDownloader(self.provider, self.auth).thenReturn(self.downloader) diff --git a/service/test/unit/config/test_services.py b/service/test/unit/config/test_services.py index 71765d19..8277c919 100644 --- a/service/test/unit/config/test_services.py +++ b/service/test/unit/config/test_services.py @@ -17,7 +17,7 @@ import unittest from mockito import mock, verify -from pixelated.config.services import Services +from pixelated.config.services import Services, ServicesFactory class ServicesTest(unittest.TestCase): @@ -32,3 +32,13 @@ class ServicesTest(unittest.TestCase): def test_close_services_closes_the_underlying_leap_session(self): self.services.close() verify(self.leap_session).close() + + +class ServicesFactoryTest(unittest.TestCase): + + def test_online_sessions_counts_logged_in_users(self): + service_factory = ServicesFactory(mock()) + service_factory.add_session('some_id1', mock()) + service_factory.add_session('some_id2', mock()) + + self.assertEqual(2, service_factory.online_sessions()) diff --git a/service/test/unit/resources/test_users_resource.py b/service/test/unit/resources/test_users_resource.py new file mode 100644 index 00000000..bfd61022 --- /dev/null +++ b/service/test/unit/resources/test_users_resource.py @@ -0,0 +1,70 @@ +import os + +import test.support.mockito + +from leap.exceptions import SRPAuthenticationError +from mock import patch +from mockito import mock, when, any as ANY, verify, verifyZeroInteractions, verifyNoMoreInteractions +from twisted.trial import unittest +from twisted.web.resource import IResource +from twisted.web.test.requesthelper import DummyRequest + +from pixelated.bitmask_libraries.session import LeapSession, LeapSessionFactory +from pixelated.config.services import Services, ServicesFactory +from pixelated.resources.login_resource import LoginResource +from pixelated.resources.users import UsersResource +from test.unit.resources import DummySite + + +class TestUsersResource(unittest.TestCase): + + def setUp(self): + self.services_factory = mock() + self.resource = UsersResource(self.services_factory) + self.web = DummySite(self.resource) + + def test_numbers_of_users_online(self): + number_of_users_online = 6 + self.services_factory.online_sessions = lambda: number_of_users_online + self.resource.is_admin = lambda _: True + request = DummyRequest(['']) + + d = self.web.get(request) + + def assert_users_count(_): + self.assertEqual(200, request.code) + self.assertEqual('{"count": %d}' % number_of_users_online, request.written[0]) + + d.addCallback(assert_users_count) + return d + + def test_numbers_of_users_online_is_only_available_only_for_admin(self): + self.resource.is_admin = lambda _: False + request = DummyRequest(['']) + d = self.web.get(request) + + def assert_is_forbidden(_): + self.assertEqual(401, request.responseCode) + self.assertEqual('Unauthorized!', request.written[0]) + + d.addCallback(assert_is_forbidden) + return d + + def test_is_admin_is_queried_from_leap_auth(self): + leap_session = mock() + auth = mock() + auth.uuid = 'some_id1' + leap_session.user_auth = auth + leap_session.config = mock() + services = Services(leap_session) + service_factory = ServicesFactory(mock()) + service_factory.add_session('some_id1', services) + + when(auth).is_admin().thenReturn(True) + request = mock() + resource = UsersResource(service_factory) + + when(resource)._get_user_id_from_request(request).thenReturn('some_id1') + + self.assertTrue(resource.is_admin(request)) + verify(auth).is_admin() -- cgit v1.2.3