1# Copyright 2012 United States Government as represented by the
2# Administrator of the National Aeronautics and Space Administration.
3# All Rights Reserved.
4#
5# Copyright 2012 OpenStack Foundation
6# Copyright 2012 Nebula, Inc.
7#
8#    Licensed under the Apache License, Version 2.0 (the "License"); you may
9#    not use this file except in compliance with the License. You may obtain
10#    a copy of the License at
11#
12#         http://www.apache.org/licenses/LICENSE-2.0
13#
14#    Unless required by applicable law or agreed to in writing, software
15#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
16#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
17#    License for the specific language governing permissions and limitations
18#    under the License.
19
20import importlib
21
22from django.conf import settings
23from django.contrib.auth.models import User
24from django.core.exceptions import ImproperlyConfigured
25from django.test.utils import override_settings
26from django import urls
27
28import horizon
29from horizon import base
30from horizon import conf
31from horizon.test import helpers as test
32from horizon.test.test_dashboards.cats.dashboard import Cats
33from horizon.test.test_dashboards.cats.kittens.panel import Kittens
34from horizon.test.test_dashboards.cats.tigers.panel import Tigers
35from horizon.test.test_dashboards.dogs.dashboard import Dogs
36from horizon.test.test_dashboards.dogs.puppies.panel import Puppies
37
38
39class MyDash(horizon.Dashboard):
40    name = "My Dashboard"
41    slug = "mydash"
42    default_panel = "myslug"
43
44
45class MyOtherDash(horizon.Dashboard):
46    name = "My Other Dashboard"
47    slug = "mydash2"
48    default_panel = "myslug2"
49
50
51class MyPanel(horizon.Panel):
52    name = "My Panel"
53    slug = "myslug"
54    urls = 'horizon.test.test_dashboards.cats.kittens.urls'
55
56
57class RbacNoAccessPanel(horizon.Panel):
58    name = "RBAC Panel No"
59    slug = "rbac_panel_no"
60
61    def allowed(self, context):
62        return False
63
64
65class RbacYesAccessPanel(horizon.Panel):
66    name = "RBAC Panel Yes"
67    slug = "rbac_panel_yes"
68
69
70class BaseHorizonTests(test.TestCase):
71
72    def setUp(self):
73        super().setUp()
74        # Adjust our horizon config and register our custom dashboards/panels.
75        self.old_default_dash = settings.HORIZON_CONFIG['default_dashboard']
76        settings.HORIZON_CONFIG['default_dashboard'] = 'cats'
77        self.old_dashboards = settings.HORIZON_CONFIG['dashboards']
78        settings.HORIZON_CONFIG['dashboards'] = ('cats', 'dogs')
79        base.Horizon.register(Cats)
80        base.Horizon.register(Dogs)
81        Cats.register(Kittens)
82        Cats.register(Tigers)
83        Dogs.register(Puppies)
84        # Trigger discovery, registration, and URLconf generation if it
85        # hasn't happened yet.
86        base.Horizon._urls()
87        # Store our original dashboards
88        self._discovered_dashboards = list(base.Horizon._registry)
89        # Gather up and store our original panels for each dashboard
90        self._discovered_panels = {}
91        for dash in self._discovered_dashboards:
92            panels = list(base.Horizon._registry[dash]._registry)
93            self._discovered_panels[dash] = panels
94
95    def tearDown(self):
96        super().tearDown()
97        # Restore our settings
98        settings.HORIZON_CONFIG['default_dashboard'] = self.old_default_dash
99        settings.HORIZON_CONFIG['dashboards'] = self.old_dashboards
100        # Destroy our singleton and re-create it.
101        base.HorizonSite._instance = None
102        del base.Horizon
103        base.Horizon = base.HorizonSite()
104        # Reload the convenience references to Horizon stored in __init__
105        importlib.reload(importlib.import_module("horizon"))
106        # Re-register our original dashboards and panels.
107        # This is necessary because autodiscovery only works on the first
108        # import, and calling reload introduces innumerable additional
109        # problems. Manual re-registration is the only good way for testing.
110        self._discovered_dashboards.remove(Cats)
111        self._discovered_dashboards.remove(Dogs)
112        for dash in self._discovered_dashboards:
113            base.Horizon.register(dash)
114            for panel in self._discovered_panels[dash]:
115                dash.register(panel)
116
117    def _reload_urls(self):
118        """Clears out the URL caches, and reloads the root urls module.
119
120        It re-triggers the autodiscovery mechanism for Horizon.
121        Allows URLs to be re-calculated after registering new dashboards.
122        Useful only for testing and should never be used on a live site.
123        """
124        urls.clear_url_caches()
125        importlib.reload(importlib.import_module(settings.ROOT_URLCONF))
126        base.Horizon._urls()
127
128
129class HorizonTests(BaseHorizonTests):
130
131    def test_registry(self):
132        """Verify registration and autodiscovery work correctly.
133
134        Please note that this implicitly tests that autodiscovery works
135        by virtue of the fact that the dashboards listed in
136        ``settings.INSTALLED_APPS`` are loaded from the start.
137        """
138        # Registration
139        self.assertEqual(2, len(base.Horizon._registry))
140        horizon.register(MyDash)
141        self.assertEqual(3, len(base.Horizon._registry))
142        with self.assertRaises(ValueError):
143            horizon.register(MyPanel)
144        with self.assertRaises(ValueError):
145            horizon.register("MyPanel")
146
147        # Retrieval
148        my_dash_instance_by_name = horizon.get_dashboard("mydash")
149        self.assertIsInstance(my_dash_instance_by_name, MyDash)
150        my_dash_instance_by_class = horizon.get_dashboard(MyDash)
151        self.assertEqual(my_dash_instance_by_name, my_dash_instance_by_class)
152        with self.assertRaises(base.NotRegistered):
153            horizon.get_dashboard("fake")
154        self.assertQuerysetEqual(horizon.get_dashboards(),
155                                 ['<Dashboard: cats>',
156                                  '<Dashboard: dogs>',
157                                  '<Dashboard: mydash>'])
158
159        # Removal
160        self.assertEqual(3, len(base.Horizon._registry))
161        horizon.unregister(MyDash)
162        self.assertEqual(2, len(base.Horizon._registry))
163        with self.assertRaises(base.NotRegistered):
164            horizon.get_dashboard(MyDash)
165
166    def test_registry_two_dashboards(self):
167        "Verify registration of 2 dashboards"
168
169        # Registration
170        self.assertEqual(2, len(base.Horizon._registry))
171        horizon.register(MyDash)
172        horizon.register(MyOtherDash)
173        self.assertEqual(4, len(base.Horizon._registry))
174
175        # Retrieval
176        self.assertQuerysetEqual(horizon.get_dashboards(),
177                                 ['<Dashboard: cats>',
178                                  '<Dashboard: dogs>',
179                                  '<Dashboard: mydash>',
180                                  '<Dashboard: mydash2>'])
181
182        # Removal
183        self.assertEqual(4, len(base.Horizon._registry))
184        horizon.unregister(MyDash)
185        horizon.unregister(MyOtherDash)
186        self.assertEqual(2, len(base.Horizon._registry))
187        with self.assertRaises(base.NotRegistered):
188            horizon.get_dashboard(MyDash)
189        with self.assertRaises(base.NotRegistered):
190            horizon.get_dashboard(MyOtherDash)
191
192    def test_site(self):
193        self.assertEqual("Horizon", str(base.Horizon))
194        self.assertEqual("<Site: horizon>", repr(base.Horizon))
195        dash = base.Horizon.get_dashboard('cats')
196        self.assertEqual(dash, base.Horizon.get_default_dashboard())
197        test_user = User()
198        self.assertEqual(dash.get_absolute_url(),
199                         base.Horizon.get_user_home(test_user))
200
201    def test_dashboard(self):
202        cats = horizon.get_dashboard("cats")
203        self.assertEqual(base.Horizon, cats._registered_with)
204        self.assertQuerysetEqual(cats.get_panels(),
205                                 ['<Panel: kittens>',
206                                  '<Panel: tigers>'])
207        self.assertEqual("/cats/", cats.get_absolute_url())
208        self.assertEqual("Cats", cats.name)
209
210        # Test registering a module with a dashboard that defines panels
211        # as a panel group.
212        cats.register(MyPanel)
213        self.assertQuerysetEqual(cats.get_panel_groups()['other'],
214                                 ['<Panel: myslug>'])
215        # Test that panels defined as a tuple still return a PanelGroup
216        dogs = horizon.get_dashboard("dogs")
217        self.assertQuerysetEqual(dogs.get_panel_groups().values(),
218                                 ['<PanelGroup: default>'])
219
220        # Test registering a module with a dashboard that defines panels
221        # as a tuple.
222        dogs = horizon.get_dashboard("dogs")
223        dogs.register(MyPanel)
224        self.assertQuerysetEqual(dogs.get_panels(),
225                                 ['<Panel: puppies>',
226                                  '<Panel: myslug>'])
227        cats.unregister(MyPanel)
228        dogs.unregister(MyPanel)
229
230    def test_panels(self):
231        cats = horizon.get_dashboard("cats")
232        tigers = cats.get_panel("tigers")
233        self.assertEqual(cats, tigers._registered_with)
234        self.assertEqual("/cats/tigers/", tigers.get_absolute_url())
235
236    def test_panel_without_slug_fails(self):
237        class InvalidPanel(horizon.Panel):
238            name = 'Invalid'
239
240        self.assertRaises(ImproperlyConfigured, InvalidPanel)
241
242    def test_registry_without_registerable_class_attr_fails(self):
243        class InvalidRegistry(base.Registry):
244            pass
245
246        self.assertRaises(ImproperlyConfigured, InvalidRegistry)
247
248    def test_index_url_name(self):
249        cats = horizon.get_dashboard("cats")
250        tigers = cats.get_panel("tigers")
251        tigers.index_url_name = "does_not_exist"
252        with self.assertRaises(urls.NoReverseMatch):
253            tigers.get_absolute_url()
254        tigers.index_url_name = "index"
255        self.assertEqual("/cats/tigers/", tigers.get_absolute_url())
256
257    def test_lazy_urls(self):
258        urlpatterns = horizon.urls[0]
259        self.assertIsInstance(urlpatterns, base.LazyURLPattern)
260        # The following two methods simply should not raise any exceptions
261        iter(urlpatterns)
262        reversed(urlpatterns)
263
264    def test_horizon_test_isolation_1(self):
265        """Isolation Test Part 1: sets a value."""
266        cats = horizon.get_dashboard("cats")
267        cats.evil = True
268
269    def test_horizon_test_isolation_2(self):
270        """Isolation Test Part 2: The value set in part 1 should be gone."""
271        cats = horizon.get_dashboard("cats")
272        self.assertFalse(hasattr(cats, "evil"))
273
274    def test_public(self):
275        dogs = horizon.get_dashboard("dogs")
276        # Known to have no restrictions on it other than being logged in.
277        puppies = dogs.get_panel("puppies")
278        url = puppies.get_absolute_url()
279
280        # Get a clean, logged out client instance.
281        self.client.logout()
282
283        resp = self.client.get(url)
284        redirect_url = "?".join(['http://testserver' + settings.LOGIN_URL,
285                                 "next=%s" % url])
286        self.assertRedirects(resp, redirect_url)
287
288        # Simulate ajax call
289        resp = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
290        # Response should be HTTP 401 with redirect header
291        self.assertEqual(401, resp.status_code)
292        self.assertEqual(redirect_url,
293                         resp["X-Horizon-Location"])
294
295    @override_settings(SESSION_REFRESH=False)
296    def test_required_permissions(self):
297        dash = horizon.get_dashboard("cats")
298        panel = dash.get_panel('tigers')
299
300        # Non-admin user
301        self.assertQuerysetEqual(self.user.get_all_permissions(), [])
302
303        resp = self.client.get(panel.get_absolute_url())
304        self.assertEqual(403, resp.status_code)
305
306        resp = self.client.get(panel.get_absolute_url(),
307                               follow=False,
308                               HTTP_X_REQUESTED_WITH='XMLHttpRequest')
309        self.assertEqual(403, resp.status_code)
310
311        # Test insufficient permissions for logged-in user
312        resp = self.client.get(panel.get_absolute_url(), follow=True)
313        self.assertEqual(403, resp.status_code)
314        self.assertTemplateUsed(resp, "not_authorized.html")
315
316        # Set roles for admin user
317        self.set_permissions(permissions=['test'])
318
319        resp = self.client.get(panel.get_absolute_url())
320        self.assertEqual(200, resp.status_code)
321
322        # Test modal form
323        resp = self.client.get(panel.get_absolute_url(),
324                               follow=False,
325                               HTTP_X_REQUESTED_WITH='XMLHttpRequest')
326        self.assertEqual(200, resp.status_code)
327
328    def test_ssl_redirect_by_proxy(self):
329        dogs = horizon.get_dashboard("dogs")
330        puppies = dogs.get_panel("puppies")
331        url = puppies.get_absolute_url()
332        redirect_url = "?".join([settings.LOGIN_URL,
333                                 "next=%s" % url])
334
335        self.client.logout()
336        resp = self.client.get(url)
337        self.assertRedirects(resp, settings.TESTSERVER + redirect_url)
338
339        # Set SSL settings for test server
340        settings.SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTOCOL',
341                                            'https')
342
343        resp = self.client.get(url, HTTP_X_FORWARDED_PROTOCOL="https")
344        self.assertEqual(302, resp.status_code)
345        self.assertEqual('https://testserver:80%s' % redirect_url,
346                         resp['location'])
347
348        # Restore settings
349        settings.SECURE_PROXY_SSL_HEADER = None
350
351
352class GetUserHomeTests(test.TestCase):
353    """Test get_user_home parameters."""
354
355    def setUp(self):
356        self.orig_user_home = settings.HORIZON_CONFIG['user_home']
357        super().setUp()
358        self.original_username = "testname"
359        self.test_user = User()
360        self.test_user.username = self.original_username
361
362    def tearDown(self):
363        settings.HORIZON_CONFIG['user_home'] = self.orig_user_home
364        conf.HORIZON_CONFIG._setup()
365        super().tearDown()
366
367    def test_using_callable(self):
368        def themable_user_fnc(user):
369            return user.username.upper()
370
371        settings.HORIZON_CONFIG['user_home'] = themable_user_fnc
372        conf.HORIZON_CONFIG._setup()
373
374        self.assertEqual(self.test_user.username.upper(),
375                         base.Horizon.get_user_home(self.test_user))
376
377    def test_using_module_function(self):
378        module_func = 'django.utils.encoding.force_text'
379        settings.HORIZON_CONFIG['user_home'] = module_func
380        conf.HORIZON_CONFIG._setup()
381
382        self.test_user.username = 'testname'
383        self.assertEqual(self.original_username,
384                         base.Horizon.get_user_home(self.test_user))
385
386    def test_using_url(self):
387        fixed_url = "/url"
388        settings.HORIZON_CONFIG['user_home'] = fixed_url
389        conf.HORIZON_CONFIG._setup()
390
391        self.assertEqual(fixed_url,
392                         base.Horizon.get_user_home(self.test_user))
393
394
395class CustomPanelTests(BaseHorizonTests):
396
397    """Test customization of dashboards and panels.
398
399    This tests customization using 'customization_module' to HORIZON_CONFIG.
400    """
401
402    def setUp(self):
403        super().setUp()
404        settings.HORIZON_CONFIG['customization_module'] = \
405            'horizon.test.customization.cust_test1'
406        # refresh config
407        conf.HORIZON_CONFIG._setup()
408        self._reload_urls()
409
410    def tearDown(self):
411        # Restore dash
412        cats = horizon.get_dashboard("cats")
413        cats.name = "Cats"
414        horizon.register(Dogs)
415        self._discovered_dashboards.append(Dogs)
416        Dogs.register(Puppies)
417        Cats.register(Tigers)
418        super().tearDown()
419        settings.HORIZON_CONFIG.pop('customization_module')
420        # refresh config
421        conf.HORIZON_CONFIG._setup()
422
423    def test_customize_dashboard(self):
424        cats = horizon.get_dashboard("cats")
425        self.assertEqual("WildCats", cats.name)
426        self.assertQuerysetEqual(cats.get_panels(),
427                                 ['<Panel: kittens>'])
428        with self.assertRaises(base.NotRegistered):
429            horizon.get_dashboard("dogs")
430
431
432class CustomPermissionsTests(BaseHorizonTests):
433
434    """Test customization of permissions on panels.
435
436    This tests customization using 'customization_module' to HORIZON_CONFIG.
437    """
438
439    def setUp(self):
440        settings.HORIZON_CONFIG['customization_module'] = \
441            'horizon.test.customization.cust_test2'
442        # refresh config
443        conf.HORIZON_CONFIG._setup()
444        super().setUp()
445
446    def tearDown(self):
447        # Restore permissions
448        dogs = horizon.get_dashboard("dogs")
449        puppies = dogs.get_panel("puppies")
450        puppies.permissions = tuple([])
451        super().tearDown()
452        settings.HORIZON_CONFIG.pop('customization_module')
453        # refresh config
454        conf.HORIZON_CONFIG._setup()
455
456    @override_settings(SESSION_REFRESH=False)
457    def test_customized_permissions(self):
458        dogs = horizon.get_dashboard("dogs")
459        panel = dogs.get_panel('puppies')
460
461        # Non-admin user
462        self.assertQuerysetEqual(self.user.get_all_permissions(), [])
463
464        resp = self.client.get(panel.get_absolute_url())
465        self.assertEqual(403, resp.status_code)
466
467        resp = self.client.get(panel.get_absolute_url(),
468                               follow=False,
469                               HTTP_X_REQUESTED_WITH='XMLHttpRequest')
470        self.assertEqual(403, resp.status_code)
471
472        # Test customized permissions for logged-in user
473        resp = self.client.get(panel.get_absolute_url(), follow=True)
474        self.assertEqual(403, resp.status_code)
475        self.assertTemplateUsed(resp, "not_authorized.html")
476
477        # Set roles for admin user
478        self.set_permissions(permissions=['test'])
479
480        resp = self.client.get(panel.get_absolute_url())
481        self.assertEqual(200, resp.status_code)
482
483        # Test modal form
484        resp = self.client.get(panel.get_absolute_url(),
485                               follow=False,
486                               HTTP_X_REQUESTED_WITH='XMLHttpRequest')
487        self.assertEqual(resp.status_code, 200)
488
489
490class RbacHorizonTests(test.TestCase):
491
492    def setUp(self):
493        super().setUp()
494        # Adjust our horizon config and register our custom dashboards/panels.
495        self.old_default_dash = settings.HORIZON_CONFIG['default_dashboard']
496        settings.HORIZON_CONFIG['default_dashboard'] = 'cats'
497        self.old_dashboards = settings.HORIZON_CONFIG['dashboards']
498        settings.HORIZON_CONFIG['dashboards'] = ('cats', 'dogs')
499        base.Horizon.register(Cats)
500        base.Horizon.register(Dogs)
501        Cats.register(RbacNoAccessPanel)
502        Cats.default_panel = 'rbac_panel_no'
503        Dogs.register(RbacYesAccessPanel)
504        Dogs.default_panel = 'rbac_panel_yes'
505        # Trigger discovery, registration, and URLconf generation if it
506        # hasn't happened yet.
507        base.Horizon._urls()
508        # Store our original dashboards
509        self._discovered_dashboards = list(base.Horizon._registry)
510        # Gather up and store our original panels for each dashboard
511        self._discovered_panels = {}
512        for dash in self._discovered_dashboards:
513            panels = list(base.Horizon._registry[dash]._registry)
514            self._discovered_panels[dash] = panels
515
516    def tearDown(self):
517        super().tearDown()
518        # Restore our settings
519        settings.HORIZON_CONFIG['default_dashboard'] = self.old_default_dash
520        settings.HORIZON_CONFIG['dashboards'] = self.old_dashboards
521        # Destroy our singleton and re-create it.
522        base.HorizonSite._instance = None
523        del base.Horizon
524        base.Horizon = base.HorizonSite()
525        # Reload the convenience references to Horizon stored in __init__
526        importlib.reload(importlib.import_module("horizon"))
527
528        # Reset Cats and Dogs default_panel to default values
529        Cats.default_panel = 'kittens'
530        Dogs.default_panel = 'puppies'
531
532        # Re-register our original dashboards and panels.
533        # This is necessary because autodiscovery only works on the first
534        # import, and calling reload introduces innumerable additional
535        # problems. Manual re-registration is the only good way for testing.
536        self._discovered_dashboards.remove(Cats)
537        self._discovered_dashboards.remove(Dogs)
538        for dash in self._discovered_dashboards:
539            base.Horizon.register(dash)
540            for panel in self._discovered_panels[dash]:
541                dash.register(panel)
542
543    def test_rbac_panels(self):
544        context = {'request': self.request}
545        cats = horizon.get_dashboard("cats")
546        self.assertEqual(cats._registered_with, base.Horizon)
547        self.assertQuerysetEqual(cats.get_panels(),
548                                 ['<Panel: rbac_panel_no>'])
549        self.assertFalse(cats.can_access(context))
550
551        dogs = horizon.get_dashboard("dogs")
552        self.assertEqual(dogs._registered_with, base.Horizon)
553        self.assertQuerysetEqual(dogs.get_panels(),
554                                 ['<Panel: rbac_panel_yes>'])
555
556        self.assertTrue(dogs.can_access(context))
557