Newer
Older
Import / svn / viewvc-1.1-dev / lib / vcauth / svnauthz / __init__.py
# -*-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 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
        if group_member:
          groups.append(groupname)
        return group_member
      
      # Process the groups
      for group in cp.options('groups'):
        _process_group(group)

    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 user == '*' \
           or user == self.username \
           or (user[0:1] == "@" and user[1:] in groups):
          # 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 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

      # 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)