1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3#
4# Copyright (C) 2009-2021 Edgewall Software
5# All rights reserved.
6#
7# This software is licensed as described in the file COPYING, which
8# you should have received as part of this distribution. The terms
9# are also available at https://trac.edgewall.org/wiki/TracLicense.
10#
11# This software consists of voluntary contributions made by many
12# individuals. For the exact contribution history, see the revision
13# history and logs, available at https://trac.edgewall.org/log/.
14
15import unittest
16
17from trac.perm import PermissionSystem
18from trac.tests.functional import FunctionalTestCaseSetup, tc
19from trac.util.text import unicode_to_base64
20
21
22class AuthorizationTestCaseSetup(FunctionalTestCaseSetup):
23    def test_authorization(self, href, perms, h2_text):
24        """Check permissions required to access an administration panel.
25
26        :param href: the relative href of the administration panel
27        :param perms: list or tuple of permissions required to access
28                      the administration panel
29        :param h2_text: the body of the h2 heading on the administration
30                        panel"""
31        self._tester.go_to_front()
32        self._tester.logout()
33        self._tester.login('user')
34        if isinstance(perms, str):
35            perms = (perms, )
36
37        h2 = r'<h2>[ \t\n]*%s[ \t\n]*' \
38             r'( <span class="trac-count">\(\d+\)</span>)?[ \t\n]*</h2>'
39        try:
40            for perm in perms:
41                try:
42                    tc.go(href)
43                    tc.find("No administration panels available")
44                    self._testenv.grant_perm('user', perm)
45                    tc.go(href)
46                    tc.find(h2 % h2_text)
47                finally:
48                    self._testenv.revoke_perm('user', perm)
49                try:
50                    tc.go(href)
51                    tc.find("No administration panels available")
52                    self._testenv.enable_authz_permpolicy({
53                        href.strip('/').replace('/', ':', 1): {'user': perm},
54                    })
55                    tc.go(href)
56                    tc.find(h2 % h2_text)
57                finally:
58                    self._testenv.disable_authz_permpolicy()
59        finally:
60            self._tester.go_to_front()
61            self._tester.logout()
62            self._tester.login('admin')
63
64
65class TestBasicSettings(FunctionalTestCaseSetup):
66    def runTest(self):
67        """Check basic settings."""
68        self._tester.go_to_admin()
69        tc.formvalue('modbasic', 'url', 'https://my.example.com/something')
70        tc.submit()
71        tc.find('https://my.example.com/something')
72
73        try:
74            tc.formvalue('modbasic', 'default_dateinfo_format', 'absolute')
75            tc.submit()
76            tc.find(r'<option selected="selected" value="absolute">')
77            tc.formvalue('modbasic', 'default_dateinfo_format', 'relative')
78            tc.submit()
79            tc.find(r'<option selected="selected" value="relative">')
80        finally:
81            self._testenv.remove_config('trac', 'default_dateinfo_format')
82            self._tester.go_to_admin()
83            tc.find(r'<option selected="selected" value="relative">')
84            tc.find(r'<option value="absolute">')
85
86
87class TestBasicSettingsAuthorization(AuthorizationTestCaseSetup):
88    def runTest(self):
89        """Check permissions required to access Basic Settings panel."""
90        self.test_authorization('/admin/general/basics', 'TRAC_ADMIN',
91                                "Basic Settings")
92
93
94class TestDefaultHandler(FunctionalTestCaseSetup):
95    def runTest(self):
96        """Set default handler from the Basic Settings page."""
97
98        # Confirm default value.
99        self._tester.go_to_admin("Basic Settings")
100        tc.find(r'<option selected="selected" value="WikiModule">'
101                r'WikiModule</option>')
102        tc.go(self._tester.url)
103        tc.find("Welcome to Trac")
104
105        # Set to another valid default handler.
106        self._tester.go_to_admin("Basic Settings")
107        tc.formvalue('modbasic', 'default_handler', 'TimelineModule')
108        tc.submit()
109        tc.find("Your changes have been saved.")
110        tc.find(r'<option selected="selected" value="TimelineModule">'
111                r'TimelineModule</option>')
112        tc.go(self._tester.url)
113        tc.find(r'<h1>Timeline</h1>')
114
115        # Set to valid disabled default handler.
116        try:
117            self._testenv.set_config('components',
118                                     'trac.timeline.web_ui.TimelineModule',
119                                     'disabled')
120            self._tester.go_to_admin("Basic Settings")
121            tc.find(r'<option value="TimelineModule">TimelineModule</option>')
122            tc.find(r'<span class="hint">\s*TimelineModule is not a valid '
123                    r'IRequestHandler or is not enabled.\s*</span>')
124            tc.go(self._tester.url)
125            tc.find(r'<h1>Configuration Error</h1>')
126            tc.find(r'Cannot find an implementation of the '
127                    r'<code>IRequestHandler</code> interface named '
128                    r'<code>TimelineModule</code>')
129        finally:
130            self._testenv.remove_config('components',
131                                        'trac.timeline.web_ui.timelinemodule')
132
133        # Set to invalid default handler.
134        try:
135            self._testenv.set_config('trac', 'default_handler',
136                                     'BatchModifyModule')
137            self._tester.go_to_admin("Basic Settings")
138            tc.find(r'<option value="BatchModifyModule">BatchModifyModule'
139                    r'</option>')
140            tc.find(r'<span class="hint">\s*BatchModifyModule is not a valid '
141                    r'IRequestHandler or is not enabled.\s*</span>')
142            tc.formvalue('modbasic', 'default_handler', 'BatchModifyModule')
143            tc.submit()  # Invalid value should not be replaced on submit
144            tc.find(r'<option value="BatchModifyModule">BatchModifyModule'
145                    r'</option>')
146            tc.find(r'<span class="hint">\s*BatchModifyModule is not a valid '
147                    r'IRequestHandler or is not enabled.\s*</span>')
148            tc.go(self._tester.url)
149            tc.find(r'<h1>Configuration Error</h1>')
150            tc.find(r'<code>BatchModifyModule</code> is not a valid default '
151                    r'handler.')
152        finally:
153            self._testenv.set_config('trac', 'default_handler', 'WikiModule')
154
155
156class TestLoggingNone(FunctionalTestCaseSetup):
157    def runTest(self):
158        """Turn off logging."""
159        # For now, we just check that it shows up.
160        self._tester.go_to_admin("Logging")
161        tc.find('trac.log')
162        tc.formvalue('modlog', 'log_type', 'none')
163        tc.submit()
164        tc.find('selected="selected" value="none">None</option')
165
166
167class TestLoggingAuthorization(AuthorizationTestCaseSetup):
168    def runTest(self):
169        """Check permissions required to access Logging panel."""
170        self.test_authorization('/admin/general/logging', 'TRAC_ADMIN',
171                                "Logging")
172
173
174class TestLoggingToFile(FunctionalTestCaseSetup):
175    def runTest(self):
176        """Turn logging back on."""
177        # For now, we just check that it shows up.
178        self._tester.go_to_admin("Logging")
179        tc.find('trac.log')
180        tc.formvalue('modlog', 'log_type', 'file')
181        tc.formvalue('modlog', 'log_file', 'trac.log2')
182        tc.formvalue('modlog', 'log_level', 'INFO')
183        tc.submit()
184        tc.find('selected="selected" value="file">File</option')
185        tc.find('id="log_file".*value="trac.log2"')
186        tc.find('selected="selected">INFO</option>')
187
188
189class TestLoggingToFileNormal(FunctionalTestCaseSetup):
190    def runTest(self):
191        """Setting logging back to normal."""
192        # For now, we just check that it shows up.
193        self._tester.go_to_admin("Logging")
194        tc.find('trac.log')
195        tc.formvalue('modlog', 'log_file', 'trac.log')
196        tc.formvalue('modlog', 'log_level', 'DEBUG')
197        tc.submit()
198        tc.find('selected="selected" value="file">File</option')
199        tc.find('id="log_file".*value="trac.log"')
200        tc.find('selected="selected">DEBUG</option>')
201
202
203class TestPermissionsAuthorization(AuthorizationTestCaseSetup):
204    def runTest(self):
205        """Check permissions required to access Permissions panel."""
206        self.test_authorization('/admin/general/perm',
207                                ('PERMISSION_GRANT', 'PERMISSION_REVOKE'),
208                                "Manage Permissions and Groups")
209
210
211class TestCreatePermissionGroup(FunctionalTestCaseSetup):
212    def runTest(self):
213        """Create a permissions group"""
214        self._tester.go_to_admin("Permissions")
215        tc.find('Manage Permissions')
216        tc.formvalue('addperm', 'gp_subject', 'somegroup')
217        tc.formvalue('addperm', 'action', 'REPORT_CREATE')
218        tc.submit()
219        somegroup = unicode_to_base64('somegroup')
220        REPORT_CREATE = unicode_to_base64('REPORT_CREATE')
221        tc.find('%s:%s' % (somegroup, REPORT_CREATE))
222
223
224class TestRemovePermissionGroup(FunctionalTestCaseSetup):
225    def runTest(self):
226        """Remove a permissions group"""
227        self._tester.go_to_admin("Permissions")
228        tc.find('Manage Permissions')
229        somegroup = unicode_to_base64('somegroup')
230        REPORT_CREATE = unicode_to_base64('REPORT_CREATE')
231        tc.find('%s:%s' % (somegroup, REPORT_CREATE))
232        tc.formvalue('revokeform', 'sel', '%s:%s' % (somegroup, REPORT_CREATE))
233        tc.submit(formname='revokeform')
234        tc.notfind('%s:%s' % (somegroup, REPORT_CREATE))
235        tc.notfind(somegroup)
236
237
238class TestAddUserToGroup(FunctionalTestCaseSetup):
239    def runTest(self):
240        """Add a user to a permissions group"""
241        self._tester.go_to_admin("Permissions")
242        tc.find('Manage Permissions')
243        tc.formvalue('addsubj', 'sg_subject', 'authenticated')
244        tc.formvalue('addsubj', 'sg_group', 'somegroup')
245        tc.submit()
246        authenticated = unicode_to_base64('authenticated')
247        somegroup = unicode_to_base64('somegroup')
248        tc.find('%s:%s' % (authenticated, somegroup))
249
250        revoke_checkbox = '%s:%s' % (unicode_to_base64('anonymous'),
251                                     unicode_to_base64('PERMISSION_GRANT'))
252        tc.formvalue('addperm', 'gp_subject', 'anonymous')
253        tc.formvalue('addperm', 'action', 'PERMISSION_GRANT')
254        tc.submit()
255        tc.find(revoke_checkbox)
256        self._testenv.get_trac_environment().config.touch()
257        self._tester.logout()
258        self._tester.go_to_admin("Permissions")
259        try:
260            tc.formvalue('addsubj', 'sg_subject', 'someuser')
261            tc.formvalue('addsubj', 'sg_group', 'authenticated')
262            tc.submit()
263            tc.find("The subject <strong>someuser</strong> was not added "
264                    "to the group <strong>authenticated</strong>. The group "
265                    "has <strong>TICKET_CREATE</strong> permission and you "
266                    "cannot grant permissions you don't possess.")
267        finally:
268            self._tester.login('admin')
269            self._tester.go_to_admin("Permissions")
270            tc.formvalue('revokeform', 'sel', revoke_checkbox)
271            tc.submit(formname='revokeform')
272            tc.notfind(revoke_checkbox)
273
274
275class TestRemoveUserFromGroup(FunctionalTestCaseSetup):
276    def runTest(self):
277        """Remove a user from a permissions group"""
278        self._tester.go_to_admin("Permissions")
279        tc.find('Manage Permissions')
280        authenticated = unicode_to_base64('authenticated')
281        somegroup = unicode_to_base64('somegroup')
282        tc.find('%s:%s' % (authenticated, somegroup))
283        tc.formvalue('revokeform', 'sel', '%s:%s' % (authenticated, somegroup))
284        tc.submit(formname='revokeform')
285        tc.notfind('%s:%s' % (authenticated, somegroup))
286
287
288class TestCopyPermissions(FunctionalTestCaseSetup):
289    def runTest(self):
290        """Tests for the Copy Permissions functionality
291        added in https://trac.edgewall.org/ticket/11099."""
292        checkbox_value = lambda s, p: '%s:%s' % (unicode_to_base64(s),
293                                                 unicode_to_base64(p))
294        grant_msg = "The subject %s has been granted the permission %s\."
295        def grant_permission(subject, action):
296            tc.formvalue('addperm', 'gp_subject', subject)
297            tc.formvalue('addperm', 'action', action)
298            tc.submit()
299            tc.find(grant_msg % (subject, action))
300            tc.find(checkbox_value(subject, action))
301
302        env = self._testenv.get_trac_environment()
303
304        # Copy permissions from subject to target
305        self._tester.go_to_admin('Permissions')
306        perm_sys = PermissionSystem(env)
307        anon_perms = perm_sys.store.get_user_permissions('anonymous')
308        for perm in anon_perms:
309            tc.find(checkbox_value('anonymous', perm))
310            tc.notfind(checkbox_value('user1', perm))
311        tc.formvalue('copyperm', 'cp_subject', 'anonymous')
312        tc.formvalue('copyperm', 'cp_target', 'user1')
313        tc.submit()
314        for perm in anon_perms:
315            tc.find("The subject user1 has been granted the permission %s\."
316                    % perm)
317            tc.find(checkbox_value('user1', perm))
318
319        # Subject doesn't have any permissions
320        tc.notfind(checkbox_value('noperms', ''))
321        tc.formvalue('copyperm', 'cp_subject', 'noperms')
322        tc.formvalue('copyperm', 'cp_target', 'user1')
323        tc.submit()
324        tc.find("The subject noperms does not have any permissions\.")
325
326        # Subject belongs to group but doesn't directly have any permissions
327        grant_permission('group1', 'TICKET_VIEW')
328        tc.formvalue('addsubj', 'sg_subject', 'noperms')
329        tc.formvalue('addsubj', 'sg_group', 'group1')
330        tc.submit()
331        tc.find("The subject noperms has been added to the group group1\.")
332
333        tc.formvalue('copyperm', 'cp_subject', 'noperms')
334        tc.formvalue('copyperm', 'cp_target', 'user1')
335        tc.submit()
336        tc.find("The subject noperms does not have any permissions\.")
337
338        # Target uses reserved all upper-case form
339        tc.formvalue('copyperm', 'cp_subject', 'noperms')
340        tc.formvalue('copyperm', 'cp_target', 'USER1')
341        tc.submit()
342        tc.find("All upper-cased tokens are reserved for permission names\.")
343        self._tester.go_to_admin("Permissions")
344
345        # Subject users reserved all upper-case form
346        tc.formvalue('copyperm', 'cp_subject', 'USER1')
347        tc.formvalue('copyperm', 'cp_target', 'noperms')
348        tc.submit()
349        tc.find("All upper-cased tokens are reserved for permission names\.")
350        self._tester.go_to_admin("Permissions")
351
352        # Target already possess one of the permissions
353        anon_perms = perm_sys.store.get_user_permissions('anonymous')
354        for perm in anon_perms:
355            tc.notfind(checkbox_value('user2', perm))
356        grant_permission('user2', anon_perms[0])
357
358        tc.formvalue('copyperm', 'cp_subject', 'anonymous')
359        tc.formvalue('copyperm', 'cp_target', 'user2')
360        tc.submit()
361
362        tc.notfind("The subject <em>user2</em> has been granted the "
363                   "permission %s\." % anon_perms[0])
364        for perm in anon_perms[1:]:
365            tc.find("The subject user2 has been granted the permission %s\."
366                    % perm)
367            tc.find(checkbox_value('user2', perm))
368
369        # Subject has a permission that is no longer defined
370        try:
371            env.db_transaction("INSERT INTO permission VALUES (%s,%s)",
372                               ('anonymous', 'NOTDEFINED_PERMISSION'))
373        except env.db_exc.IntegrityError:
374            pass
375        env.config.touch()  # invalidate permission cache
376        tc.reload()
377        tc.find(checkbox_value('anonymous', 'NOTDEFINED_PERMISSION'))
378        perm_sys = PermissionSystem(env)
379        anon_perms = perm_sys.store.get_user_permissions('anonymous')
380        for perm in anon_perms:
381            tc.notfind(checkbox_value('user3', perm))
382
383        tc.formvalue('copyperm', 'cp_subject', 'anonymous')
384        tc.formvalue('copyperm', 'cp_target', 'user3')
385        tc.submit()
386
387        for perm in anon_perms:
388            msg = grant_msg % ('user3', perm)
389            if perm == 'NOTDEFINED_PERMISSION':
390                tc.notfind(msg)
391                tc.notfind(checkbox_value('user3', perm))
392            else:
393                tc.find(msg)
394                tc.find(checkbox_value('user3', perm))
395        perm_sys.revoke_permission('anonymous', 'NOTDEFINED_PERMISSION')
396
397        # Actor doesn't posses permission
398        grant_permission('anonymous', 'PERMISSION_GRANT')
399        grant_permission('user3', 'TRAC_ADMIN')
400        self._tester.logout()
401        self._tester.go_to_admin("Permissions")
402
403        try:
404            tc.formvalue('copyperm', 'cp_subject', 'user3')
405            tc.formvalue('copyperm', 'cp_target', 'user4')
406            tc.submit()
407
408            perm_sys = PermissionSystem(env)
409            for perm in [perm[1] for perm in perm_sys.get_all_permissions()
410                                 if perm[0] == 'user3'
411                                 and perm[1] != 'TRAC_ADMIN']:
412                tc.find(grant_msg % ('user4', perm))
413            tc.notfind("The permission TRAC_ADMIN was not granted to user4 "
414                       "because users cannot grant permissions they don't "
415                       "possess.")
416        finally:
417            self._testenv.revoke_perm('anonymous', 'PERMISSION_GRANT')
418            self._tester.login('admin')
419
420
421class TestPluginSettings(FunctionalTestCaseSetup):
422    def runTest(self):
423        """Check plugin settings."""
424        self._tester.go_to_admin("Plugins")
425        tc.find('Manage Plugins')
426        tc.find('Install Plugin')
427
428
429class TestPluginsAuthorization(AuthorizationTestCaseSetup):
430    def runTest(self):
431        """Check permissions required to access Logging panel."""
432        self.test_authorization('/admin/general/plugin', 'TRAC_ADMIN',
433                                "Manage Plugins")
434
435
436class RegressionTestTicket10752(FunctionalTestCaseSetup):
437    def runTest(self):
438        """Test for regression of https://trac.edgewall.org/ticket/10752
439        Permissions on the web admin page should be greyed out when they
440        are no longer defined.
441        """
442        env = self._testenv.get_trac_environment()
443        try:
444            env.db_transaction("INSERT INTO permission VALUES (%s,%s)",
445                               ('user', 'NOTDEFINED_PERMISSION'))
446        except env.db_exc.IntegrityError:
447            pass
448        env.config.touch()
449
450        self._tester.go_to_admin("Permissions")
451        tc.find('<span class="missing" '
452                'title="NOTDEFINED_PERMISSION is no longer defined">'
453                'NOTDEFINED_PERMISSION</span>')
454        tc.notfind('<input type="checkbox" [^>]+ disabled="disabled"')
455        tc.notfind('<input type="checkbox" [^>]+'
456                   'title="You don\'t have permission to revoke this action" '
457                   '[^>]+>')
458
459
460class RegressionTestTicket11069(FunctionalTestCaseSetup):
461    def runTest(self):
462        """Test for regression of https://trac.edgewall.org/ticket/11069
463        The permissions list should only be populated with permissions that
464        the user can grant."""
465        self._tester.go_to_front()
466        self._tester.logout()
467        self._tester.login('user')
468        self._testenv.grant_perm('user', 'PERMISSION_GRANT')
469        env = self._testenv.get_trac_environment()
470        user_perms = PermissionSystem(env).get_user_permissions('user')
471        all_actions = PermissionSystem(env).get_actions()
472        try:
473            self._tester.go_to_admin("Permissions")
474            for action in all_actions:
475                option = r"<option>%s</option>" % action
476                if action in user_perms and user_perms[action] is True:
477                    tc.find(option)
478                else:
479                    tc.notfind(option)
480        finally:
481            self._testenv.revoke_perm('user', 'PERMISSION_GRANT')
482            self._tester.go_to_front()
483            self._tester.logout()
484            self._tester.login('admin')
485
486
487class RegressionTestTicket11095(FunctionalTestCaseSetup):
488    """Test for regression of https://trac.edgewall.org/ticket/11095
489    The permission is truncated if it overflows the available space (CSS)
490    and the full permission name is shown in the title on hover.
491    """
492    def runTest(self):
493        self._tester.go_to_admin("Permissions")
494        tc.find('<span title="MILESTONE_VIEW">MILESTONE_VIEW</span>')
495        tc.find('<span title="WIKI_VIEW">WIKI_VIEW</span>')
496
497
498class RegressionTestTicket11117(FunctionalTestCaseSetup):
499    """Test for regression of https://trac.edgewall.org/ticket/11117
500    Hint should be shown on the Basic Settings admin panel when pytz is not
501    installed.
502    """
503    def runTest(self):
504        self._tester.go_to_admin("Basic Settings")
505        pytz_hint = "Install pytz for a complete list of timezones."
506        from trac.util.datefmt import pytz
507        if pytz is None:
508            tc.find(pytz_hint)
509        else:
510            tc.notfind(pytz_hint)
511
512
513class RegressionTestTicket11257(FunctionalTestCaseSetup):
514    """Test for regression of https://trac.edgewall.org/ticket/11257
515    Hints should be shown on the Basic Settings admin panel when Babel is not
516    installed.
517    """
518    def runTest(self):
519        from trac.util.translation import get_available_locales, has_babel
520
521        babel_hint_lang = "Install Babel for extended language support."
522        babel_hint_date = "Install Babel for localized date formats."
523        catalog_hint = "Message catalogs have not been compiled."
524        language_select = '<select name="default_language">'
525        disabled_language_select = \
526            '<select disabled="disabled" name="default_language" ' \
527            'title="Translations are currently unavailable">'
528
529        self._tester.go_to_admin("Basic Settings")
530        if has_babel:
531            tc.notfind(babel_hint_lang)
532            tc.notfind(babel_hint_date)
533            if get_available_locales():
534                tc.find(language_select)
535                tc.notfind(catalog_hint)
536            else:
537                tc.find(disabled_language_select)
538                tc.find(catalog_hint)
539        else:
540            tc.find(disabled_language_select)
541            tc.find(babel_hint_lang)
542            tc.find(babel_hint_date)
543            tc.notfind(catalog_hint)
544
545
546def functionalSuite(suite=None):
547    if not suite:
548        import trac.tests.functional
549        suite = trac.tests.functional.functionalSuite()
550    suite.addTest(TestBasicSettings())
551    suite.addTest(TestBasicSettingsAuthorization())
552    suite.addTest(TestDefaultHandler())
553    suite.addTest(TestLoggingNone())
554    suite.addTest(TestLoggingAuthorization())
555    suite.addTest(TestLoggingToFile())
556    suite.addTest(TestLoggingToFileNormal())
557    suite.addTest(TestPermissionsAuthorization())
558    suite.addTest(TestCreatePermissionGroup())
559    suite.addTest(TestRemovePermissionGroup())
560    suite.addTest(TestAddUserToGroup())
561    suite.addTest(TestRemoveUserFromGroup())
562    suite.addTest(TestCopyPermissions())
563    suite.addTest(TestPluginSettings())
564    suite.addTest(TestPluginsAuthorization())
565    suite.addTest(RegressionTestTicket10752())
566    suite.addTest(RegressionTestTicket11069())
567    suite.addTest(RegressionTestTicket11095())
568    suite.addTest(RegressionTestTicket11117())
569    suite.addTest(RegressionTestTicket11257())
570    return suite
571
572
573test_suite = functionalSuite
574
575
576if __name__ == '__main__':
577    unittest.main(defaultTest='test_suite')
578