224 lines
7.9 KiB
Python
224 lines
7.9 KiB
Python
# -*-python-*-
|
|
#
|
|
# Copyright (C) 2006-2008 The ViewCVS Group. All Rights Reserved.
|
|
#
|
|
# By using this file, you agree to the terms and conditions set forth in
|
|
# the LICENSE.html file which can be found at the top level of the ViewVC
|
|
# distribution or at http://viewvc.org/license-1.html.
|
|
#
|
|
# For more information, visit http://viewvc.org/
|
|
#
|
|
# -----------------------------------------------------------------------
|
|
# (c) 2006 Sergey Lapin <slapin@dataart.com>
|
|
|
|
import vcauth
|
|
import string
|
|
import os.path
|
|
import debug
|
|
|
|
from ConfigParser import ConfigParser
|
|
|
|
class ViewVCAuthorizer(vcauth.GenericViewVCAuthorizer):
|
|
"""Subversion authz authorizer module"""
|
|
|
|
def __init__(self, username, params={}):
|
|
self.username = username
|
|
self.rootpaths = { } # {root -> { paths -> access boolean for USERNAME }}
|
|
|
|
# Get the authz file location from a passed-in parameter.
|
|
self.authz_file = params.get('authzfile')
|
|
if not self.authz_file:
|
|
raise debug.ViewVCException("No authzfile configured")
|
|
if not os.path.exists(self.authz_file):
|
|
raise debug.ViewVCException("Configured authzfile file not found")
|
|
|
|
def _get_paths_for_root(self, rootname):
|
|
if self.rootpaths.has_key(rootname):
|
|
return self.rootpaths[rootname]
|
|
|
|
paths_for_root = { }
|
|
|
|
# Parse the authz file.
|
|
cp = ConfigParser()
|
|
cp.read(self.authz_file)
|
|
|
|
# Figure out if there are any aliases for the current username
|
|
aliases = []
|
|
if cp.has_section('aliases'):
|
|
for alias in cp.options('aliases'):
|
|
entry = cp.get('aliases', alias)
|
|
if entry == self.username:
|
|
aliases.append(alias)
|
|
|
|
# Figure out which groups USERNAME has a part of.
|
|
groups = []
|
|
if cp.has_section('groups'):
|
|
all_groups = []
|
|
|
|
def _process_group(groupname):
|
|
"""Inline function to handle groups within groups.
|
|
|
|
For a group to be within another group in SVN, the group
|
|
definitions must be in the correct order in the config file.
|
|
ie. If group A is a member of group B then group A must be
|
|
defined before group B in the [groups] section.
|
|
|
|
Unfortunately, the ConfigParser class provides no way of
|
|
finding the order in which groups were defined so, for reasons
|
|
of practicality, this function lets you get away with them
|
|
being defined in the wrong order. Recursion is guarded
|
|
against though."""
|
|
|
|
# If we already know the user is part of this already-
|
|
# processed group, return that fact.
|
|
if groupname in groups:
|
|
return 1
|
|
# Otherwise, ensure we don't process a group twice.
|
|
if groupname in all_groups:
|
|
return 0
|
|
# Store the group name in a global list so it won't be processed again
|
|
all_groups.append(groupname)
|
|
group_member = 0
|
|
groupname = groupname.strip()
|
|
entries = string.split(cp.get('groups', groupname), ',')
|
|
for entry in entries:
|
|
entry = string.strip(entry)
|
|
if entry == self.username:
|
|
group_member = 1
|
|
break
|
|
elif entry[0:1] == "@" and _process_group(entry[1:]):
|
|
group_member = 1
|
|
break
|
|
elif entry[0:1] == "&" and entry[1:] in aliases:
|
|
group_member = 1
|
|
break
|
|
if group_member:
|
|
groups.append(groupname)
|
|
return group_member
|
|
|
|
# Process the groups
|
|
for group in cp.options('groups'):
|
|
_process_group(group)
|
|
|
|
def _userspec_matches_user(userspec):
|
|
# If there is an inversion character, recurse and return the
|
|
# opposite result.
|
|
if userspec[0:1] == '~':
|
|
return not _userspec_matches_user(userspec[1:])
|
|
|
|
# See if the userspec applies to our current user.
|
|
return userspec == '*' \
|
|
or userspec == self.username \
|
|
or (self.username is not None and userspec == "$authenticated") \
|
|
or (self.username is None and userspec == "$anonymous") \
|
|
or (userspec[0:1] == "@" and userspec[1:] in groups) \
|
|
or (userspec[0:1] == "&" and userspec[1:] in aliases)
|
|
|
|
def _process_access_section(section):
|
|
"""Inline function for determining user access in a single
|
|
config secction. Return a two-tuple (ALLOW, DENY) containing
|
|
the access determination for USERNAME in a given authz file
|
|
SECTION (if any)."""
|
|
|
|
# Figure if this path is explicitly allowed or denied to USERNAME.
|
|
allow = deny = 0
|
|
for user in cp.options(section):
|
|
user = string.strip(user)
|
|
if _userspec_matches_user(user):
|
|
# See if the 'r' permission is among the ones granted to
|
|
# USER. If so, we can stop looking. (Entry order is not
|
|
# relevant -- we'll use the most permissive entry, meaning
|
|
# one 'allow' is all we need.)
|
|
allow = string.find(cp.get(section, user), 'r') != -1
|
|
deny = not allow
|
|
if allow:
|
|
break
|
|
return allow, deny
|
|
|
|
# Read the other (non-"groups") sections, and figure out in which
|
|
# repositories USERNAME or his groups have read rights. We'll
|
|
# first check groups that have no specific repository designation,
|
|
# then superimpose those that have a repository designation which
|
|
# matches the one we're asking about.
|
|
root_sections = []
|
|
for section in cp.sections():
|
|
|
|
# Skip the "groups" section -- we handled that already.
|
|
if section == 'groups':
|
|
continue
|
|
|
|
if section == 'aliases':
|
|
continue
|
|
|
|
# Process root-agnostic access sections; skip (but remember)
|
|
# root-specific ones that match our root; ignore altogether
|
|
# root-specific ones that don't match our root. While we're at
|
|
# it, go ahead and figure out the repository path we're talking
|
|
# about.
|
|
if section.find(':') == -1:
|
|
path = section
|
|
else:
|
|
name, path = string.split(section, ':', 1)
|
|
if name == rootname:
|
|
root_sections.append(section)
|
|
continue
|
|
|
|
# Check for a specific access determination.
|
|
allow, deny = _process_access_section(section)
|
|
|
|
# If we got an explicit access determination for this path and this
|
|
# USERNAME, record it.
|
|
if allow or deny:
|
|
if path != '/':
|
|
path = '/' + string.join(filter(None, string.split(path, '/')), '/')
|
|
paths_for_root[path] = allow
|
|
|
|
# Okay. Superimpose those root-specific values now.
|
|
for section in root_sections:
|
|
|
|
# Get the path again.
|
|
name, path = string.split(section, ':', 1)
|
|
|
|
# Check for a specific access determination.
|
|
allow, deny = _process_access_section(section)
|
|
|
|
# If we got an explicit access determination for this path and this
|
|
# USERNAME, record it.
|
|
if allow or deny:
|
|
if path != '/':
|
|
path = '/' + string.join(filter(None, string.split(path, '/')), '/')
|
|
paths_for_root[path] = allow
|
|
|
|
# If the root isn't readable, there's no point in caring about all
|
|
# the specific paths the user can't see. Just point the rootname
|
|
# to a None paths dictionary.
|
|
root_is_readable = 0
|
|
for path in paths_for_root.keys():
|
|
if paths_for_root[path]:
|
|
root_is_readable = 1
|
|
break
|
|
if not root_is_readable:
|
|
paths_for_root = None
|
|
|
|
self.rootpaths[rootname] = paths_for_root
|
|
return paths_for_root
|
|
|
|
def check_root_access(self, rootname):
|
|
paths = self._get_paths_for_root(rootname)
|
|
return (paths is not None) and 1 or 0
|
|
|
|
def check_path_access(self, rootname, path_parts, pathtype, rev=None):
|
|
# Crawl upward from the path represented by PATH_PARTS toward to
|
|
# the root of the repository, looking for an explicitly grant or
|
|
# denial of access.
|
|
paths = self._get_paths_for_root(rootname)
|
|
if paths is None:
|
|
return 0
|
|
parts = path_parts[:]
|
|
while parts:
|
|
path = '/' + string.join(parts, '/')
|
|
if paths.has_key(path):
|
|
return paths[path]
|
|
del parts[-1]
|
|
return paths.get('/', 0)
|