def test_coarse_permissions(self): policy = AuthzSourcePolicy(self.env) # Granted to all due to wildcard self.assertPathPerm(True, 'unknown') self.assertPathPerm(True, 'joe') self.assertRevPerm(True, 'unknown') self.assertRevPerm(True, 'joe') # Granted if at least one fine permission is granted policy._mtime = 0 create_file( self.authz_file, textwrap.dedent("""\ [/somepath] joe = r denied = [module:/otherpath] Jane = r $anonymous = r [inactive:/not-in-this-instance] unknown = r """)) self.assertPathPerm(None, 'unknown') self.assertRevPerm(None, 'unknown') self.assertPathPerm(None, 'denied') self.assertRevPerm(None, 'denied') self.assertPathPerm(True, 'joe') self.assertRevPerm(True, 'joe') self.assertPathPerm(True, 'Jane') self.assertRevPerm(True, 'Jane') self.assertPathPerm(True, 'anonymous') self.assertRevPerm(True, 'anonymous')
def assertRevPerm(self, result, user, reponame=None, rev=None): """Assert that `user` is granted access `result` to `rev` within the repository `reponame`. """ policy = AuthzSourcePolicy(self.env) resource = None if reponame is not None: resource = Resource('changeset', rev, parent=Resource('repository', reponame)) check = policy.check_permission('CHANGESET_VIEW', user, resource, None) self.assertEqual(result, check)
def assertPathPerm(self, result, user, reponame=None, path=None): """Assert that `user` is granted access `result` to `path` within the repository `reponame`. """ policy = AuthzSourcePolicy(self.env) resource = None if reponame is not None: resource = Resource('source', path, parent=Resource('repository', reponame)) for perm in ('BROWSER_VIEW', 'FILE_VIEW', 'LOG_VIEW'): check = policy.check_permission(perm, user, resource, None) self.assertEqual(result, check)
def test_get_authz_file_empty_raises(self): """ConfigurationError exception is raised if the option `[svn] authz_file` is empty.""" self.env.config.set('svn', 'authz_file', '') policy = AuthzSourcePolicy(self.env) self.assertRaises(ConfigurationError, policy.check_permission, 'BROWSER_VIEW', 'user', None, None)
def test_get_authz_file_notdefined_raises(self): """ConfigurationError exception is raised if the option `[svn] authz_file` is not specified in trac.ini.""" self.env.config.remove('svn', 'authz_file') policy = AuthzSourcePolicy(self.env) self.assertRaises(ConfigurationError, policy.check_permission, 'BROWSER_VIEW', 'user', None, None)
def test_get_authz_file_notfound_raises(self): """ConfigurationError exception is raised if file not found.""" authz_file = os.path.join(self.env.path, 'some-nonexistent-file') self.env.config.set('svn', 'authz_file', authz_file) policy = AuthzSourcePolicy(self.env) self.assertRaises(ConfigurationError, policy.check_permission, 'BROWSER_VIEW', 'user', None, None)
def test_parse_error_raises(self): """ConfigurationError exception is raised when exception occurs parsing the `[svn authz_file`.""" create_file(self.authz_file, """\ [/somepath joe = r """) policy = AuthzSourcePolicy(self.env) self.assertRaises(ConfigurationError, policy.check_permission, 'BROWSER_VIEW', 'user', None, None)
def __init__(self): """Make sure that the configured authz file is available. AuthzSourcePolicy requires its authz file to exist. Otherwise, it would not allow to see the browser until the first occurence of access configuration, which is done via the browser. """ authz_source_file = AuthzSourcePolicy(self.env).authz_file if authz_source_file: authz_source_path = os.path.join(self.env.path, authz_source_file) if not os.path.exists(authz_source_path): RepositoryManager(self.env).update_auth_files()
def test_get_authz_file_removed_raises(self): """ConfigurationError exception is raised if file is removed.""" policy = AuthzSourcePolicy(self.env) os.remove(self.authz_file) self.assertRaises(ConfigurationError, policy.check_permission, 'BROWSER_VIEW', 'user', None, None)
def setUp(self): self.env = EnvironmentStub(path=mkdtemp()) self.env.config.filename = os.path.join(self.env.path, 'trac.ini') AuthzSourcePolicy(self.env) RepositoryManager(self.env)
class AuthzSourcePolicyTestCase(unittest.TestCase): def setUp(self): tmpdir = os.path.realpath(tempfile.gettempdir()) self.authz = os.path.join(tmpdir, 'trac-authz') create_file(self.authz, """\ [groups] group1 = user group2 = @group1 cycle1 = @cycle2 cycle2 = @cycle3 cycle3 = @cycle1, user alias1 = &jekyll alias2 = @alias1 [aliases] jekyll = Mr Hyde # Read / write permissions [/readonly] user = r [/writeonly] user = w [/readwrite] user = rw [/empty] user = # Trailing slashes [/trailing_a] user = r [/trailing_b/] user = r # Sub-paths [/sub/path] user = r # Module usage [module:/module_a] user = r [other:/module_b] user = r [/module_c] user = r [module:/module_d] user = [/module_d] user = r # Wildcards [/wildcard] * = r # Special tokens [/special/anonymous] $anonymous = r [/special/authenticated] $authenticated = r # Groups [/groups_a] @group1 = r [/groups_b] @group2 = r [/cyclic] @cycle1 = r # Precedence [module:/precedence_a] user = [/precedence_a] user = r [/precedence_b] user = r [/precedence_b/sub] user = [/precedence_b/sub/test] user = r [/precedence_c] user = @group1 = r [/precedence_d] @group1 = r user = # Aliases [/aliases_a] &jekyll = r [/aliases_b] @alias2 = r # Scoped repository [scoped:/scope/dir1] joe = r [scoped:/scope/dir2] jane = r """) self.env = EnvironmentStub(enable=[AuthzSourcePolicy]) self.env.config.set('trac', 'authz_file', self.authz) self.policy = AuthzSourcePolicy(self.env) # Monkey-subclass RepositoryManager to serve mock repositories rm = RepositoryManager(self.env) class TestRepositoryManager(rm.__class__): def get_real_repositories(self): return set([Mock(reponame='module'), Mock(reponame='other'), Mock(reponame='scoped')]) def get_repository(self, reponame): if reponame == 'scoped': def get_changeset(rev): if rev == 123: def get_changes(): yield ('/dir1/file',) elif rev == 456: def get_changes(): yield ('/dir2/file',) else: def get_changes(): return iter([]) return Mock(get_changes=get_changes) return Mock(scope='/scope', get_changeset=get_changeset) return Mock(scope='/') rm.__class__ = TestRepositoryManager def tearDown(self): self.env.reset_db() os.remove(self.authz) def assertPathPerm(self, result, user, reponame=None, path=None): """Assert that `user` is granted access `result` to `path` within the repository `reponame`. """ resource = None if reponame is not None: resource = Resource('source', path, parent=Resource('repository', reponame)) for perm in ('BROWSER_VIEW', 'FILE_VIEW', 'LOG_VIEW'): check = self.policy.check_permission(perm, user, resource, None) self.assertEqual(result, check) def assertRevPerm(self, result, user, reponame=None, rev=None): """Assert that `user` is granted access `result` to `rev` within the repository `reponame`. """ resource = None if reponame is not None: resource = Resource('changeset', rev, parent=Resource('repository', reponame)) check = self.policy.check_permission('CHANGESET_VIEW', user, resource, None) self.assertEqual(result, check) def test_coarse_permissions(self): # Granted to all due to wildcard self.assertPathPerm(True, 'unknown') self.assertPathPerm(True, 'joe') self.assertRevPerm(True, 'unknown') self.assertRevPerm(True, 'joe') # Granted if at least one fine permission is granted self.policy._mtime = 0 create_file(self.authz, """\ [/somepath] joe = r denied = [module:/otherpath] jane = r $anonymous = r [inactive:/not-in-this-instance] unknown = r """) self.assertPathPerm(None, 'unknown') self.assertRevPerm(None, 'unknown') self.assertPathPerm(None, 'denied') self.assertRevPerm(None, 'denied') self.assertPathPerm(True, 'joe') self.assertRevPerm(True, 'joe') self.assertPathPerm(True, 'jane') self.assertRevPerm(True, 'jane') self.assertPathPerm(True, 'anonymous') self.assertRevPerm(True, 'anonymous') def test_default_permission(self): # By default, permissions are undecided self.assertPathPerm(None, 'joe', '', '/not_defined') self.assertPathPerm(None, 'jane', 'repo', '/not/defined/either') def test_read_write(self): # Allow 'r' and 'rw' entries, deny 'w' and empty entries self.assertPathPerm(True, 'user', '', '/readonly') self.assertPathPerm(True, 'user', '', '/readwrite') self.assertPathPerm(False, 'user', '', '/writeonly') self.assertPathPerm(False, 'user', '', '/empty') def test_trailing_slashes(self): # Combinations of trailing slashes in the file and in the path self.assertPathPerm(True, 'user', '', '/trailing_a') self.assertPathPerm(True, 'user', '', '/trailing_a/') self.assertPathPerm(True, 'user', '', '/trailing_b') self.assertPathPerm(True, 'user', '', '/trailing_b/') def test_sub_path(self): # Permissions are inherited from containing directories self.assertPathPerm(True, 'user', '', '/sub/path') self.assertPathPerm(True, 'user', '', '/sub/path/test') self.assertPathPerm(True, 'user', '', '/sub/path/other/sub') def test_module_usage(self): # If a module name is specified, the rules are specific to the module self.assertPathPerm(True, 'user', 'module', '/module_a') self.assertPathPerm(None, 'user', 'module', '/module_b') # If a module is specified, but the configuration contains a non-module # path, the non-module path can still apply self.assertPathPerm(True, 'user', 'module', '/module_c') # The module-specific rule takes precedence self.assertPathPerm(False, 'user', 'module', '/module_d') def test_wildcard(self): # The * wildcard matches all users, including anonymous self.assertPathPerm(True, 'anonymous', '', '/wildcard') self.assertPathPerm(True, 'joe', '', '/wildcard') self.assertPathPerm(True, 'jane', '', '/wildcard') def test_special_tokens(self): # The $anonymous token matches only anonymous users self.assertPathPerm(True, 'anonymous', '', '/special/anonymous') self.assertPathPerm(None, 'user', '', '/special/anonymous') # The $authenticated token matches all authenticated users self.assertPathPerm(None, 'anonymous', '', '/special/authenticated') self.assertPathPerm(True, 'joe', '', '/special/authenticated') self.assertPathPerm(True, 'jane', '', '/special/authenticated') def test_groups(self): # Groups are specified in a separate section and used with an @ prefix self.assertPathPerm(True, 'user', '', '/groups_a') # Groups can also be members of other groups self.assertPathPerm(True, 'user', '', '/groups_b') # Groups should not be defined cyclically, but they are still handled # correctly to avoid infinite loops self.assertPathPerm(True, 'user', '', '/cyclic') def test_precedence(self): # Module-specific sections take precedence over non-module sections self.assertPathPerm(False, 'user', 'module', '/precedence_a') # The most specific section applies self.assertPathPerm(True, 'user', '', '/precedence_b/sub/test') # ... intentional deviation from SVN's rules as we need to # make '/precedence_b/sub' browseable so that the user can see # '/precedence_b/sub/test': self.assertPathPerm(True, 'user', '', '/precedence_b/sub') self.assertPathPerm(True, 'user', '', '/precedence_b') # Within a section, the first matching rule applies self.assertPathPerm(False, 'user', '', '/precedence_c') self.assertPathPerm(True, 'user', '', '/precedence_d') def test_aliases(self): # Aliases are specified in a separate section and used with an & prefix self.assertPathPerm(True, 'Mr Hyde', '', '/aliases_a') # Aliases can also be used in groups self.assertPathPerm(True, 'Mr Hyde', '', '/aliases_b') def test_scoped_repository(self): # Take repository scope into account self.assertPathPerm(True, 'joe', 'scoped', '/dir1') self.assertPathPerm(None, 'joe', 'scoped', '/dir2') self.assertPathPerm(True, 'joe', 'scoped', '/') self.assertPathPerm(None, 'jane', 'scoped', '/dir1') self.assertPathPerm(True, 'jane', 'scoped', '/dir2') self.assertPathPerm(True, 'jane', 'scoped', '/') def test_changesets(self): # Changesets are allowed if at least one changed path is allowed, or # if the changeset is empty self.assertRevPerm(True, 'joe', 'scoped', 123) self.assertRevPerm(None, 'joe', 'scoped', 456) self.assertRevPerm(True, 'joe', 'scoped', 789) self.assertRevPerm(None, 'jane', 'scoped', 123) self.assertRevPerm(True, 'jane', 'scoped', 456) self.assertRevPerm(True, 'jane', 'scoped', 789) self.assertRevPerm(None, 'user', 'scoped', 123) self.assertRevPerm(None, 'user', 'scoped', 456) self.assertRevPerm(True, 'user', 'scoped', 789)
def setUp(self): tmpdir = os.path.realpath(tempfile.gettempdir()) self.authz = os.path.join(tmpdir, 'trac-authz') create_file(self.authz, """\ [groups] group1 = user group2 = @group1 cycle1 = @cycle2 cycle2 = @cycle3 cycle3 = @cycle1, user alias1 = &jekyll alias2 = @alias1 [aliases] jekyll = Mr Hyde # Read / write permissions [/readonly] user = r [/writeonly] user = w [/readwrite] user = rw [/empty] user = # Trailing slashes [/trailing_a] user = r [/trailing_b/] user = r # Sub-paths [/sub/path] user = r # Module usage [module:/module_a] user = r [other:/module_b] user = r [/module_c] user = r [module:/module_d] user = [/module_d] user = r # Wildcards [/wildcard] * = r # Special tokens [/special/anonymous] $anonymous = r [/special/authenticated] $authenticated = r # Groups [/groups_a] @group1 = r [/groups_b] @group2 = r [/cyclic] @cycle1 = r # Precedence [module:/precedence_a] user = [/precedence_a] user = r [/precedence_b] user = r [/precedence_b/sub] user = [/precedence_b/sub/test] user = r [/precedence_c] user = @group1 = r [/precedence_d] @group1 = r user = # Aliases [/aliases_a] &jekyll = r [/aliases_b] @alias2 = r # Scoped repository [scoped:/scope/dir1] joe = r [scoped:/scope/dir2] jane = r """) self.env = EnvironmentStub(enable=[AuthzSourcePolicy]) self.env.config.set('trac', 'authz_file', self.authz) self.policy = AuthzSourcePolicy(self.env) # Monkey-subclass RepositoryManager to serve mock repositories rm = RepositoryManager(self.env) class TestRepositoryManager(rm.__class__): def get_real_repositories(self): return set([Mock(reponame='module'), Mock(reponame='other'), Mock(reponame='scoped')]) def get_repository(self, reponame): if reponame == 'scoped': def get_changeset(rev): if rev == 123: def get_changes(): yield ('/dir1/file',) elif rev == 456: def get_changes(): yield ('/dir2/file',) else: def get_changes(): return iter([]) return Mock(get_changes=get_changes) return Mock(scope='/scope', get_changeset=get_changeset) return Mock(scope='/') rm.__class__ = TestRepositoryManager
def update_auth_files(self): """Rewrites all configured auth files for all managed repositories. """ types = self.get_supported_types() all_repositories = [] for repo in self.manager.get_real_repositories(): try: convert_managed_repository(self.env, repo) all_repositories.append(repo) except: pass for type in types: repos = [repo for repo in all_repositories if repo.type == type] self._get_repository_connector(type).update_auth_files(repos) authz_source_file = AuthzSourcePolicy(self.env).authz_file if authz_source_file: authz_source_path = os.path.join(self.env.path, authz_source_file) authz = ConfigParser() groups = set() for repo in all_repositories: groups |= { name for name in repo.maintainers() if name[0] == '@' } groups |= {name for name in repo.writers() if name[0] == '@'} groups |= {name for name in repo.readers() if name[0] == '@'} authz.add_section('groups') for group in groups: members = expand_user_set(self.env, [group]) authz.set('groups', group[1:], ', '.join(sorted(members))) authenticated = sorted({u[0] for u in self.env.get_known_users()}) authz.set('groups', 'authenticated', ', '.join(authenticated)) for repo in all_repositories: section = repo.reponame + ':/' authz.add_section(section) r = repo.maintainers() | repo.writers() | repo.readers() def apply_user_list(users, action): if not users: return if 'anonymous' in users: authz.set(section, '*', action) return if 'authenticated' in users: authz.set(section, '@authenticated', action) return for user in sorted(users): authz.set(section, user, action) apply_user_list(r, 'r') self._prepare_base_directory(authz_source_path) with open(authz_source_path, 'wb') as authz_file: authz.write(authz_file) try: modes = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IWGRP os.chmod(authz_source_path, modes) except: pass