# Copyright (c) 2002 Infrae. All rights reserved.
# See also LICENSE.txt
# $Id: GroupsService.py,v 1.14 2003-09-16 07:45:41 zagy Exp $

import time

from AccessControl import ClassSecurityInfo
import Globals
from Products.PageTemplates.PageTemplateFile import PageTemplateFile

import GroupsPermissions
from GroupsErrors import GroupsError
from helpers import guessHost
from ipparse import IPRange

class Record:
    pass
CACHE_TIMEOUT = 3600  # 1 hour


class GroupsService:
    """Attach users to groups. This is the most simple implementation.
    Other implementations may get groups data from LDAP, etc.

    There are two kinds of groups:

        * normal group (ZODB defined, LDAP, or both etc).
        * virtual group (a user is in a virtual group if that user is in a
                         defined set of normal groups)
    """
    security = ClassSecurityInfo()
    
    def __init__(self):
        
        # userid -> {group1 -> 1, group2 -> 1}
        self._users = {}
        
        # virtual group name -> [group1, group2, ...] 
        # mapping virtual group to members of virtual group
        self._virtual_groups = {}

        # group name -> [vgroup1, vgroup2, ...]
        # mapping of group name to virtual groups the group is in
        self._group_to_virtual = {}

        # group name -> 1
        # mapping of group name to 1, meaning it is a group if the key exists
        # ``is this a normal group?''
        self._normal_groups = {}

        # don't know what those are for 
        self._user_to_managed_normal_groups = {}
        self._user_to_managed_virtual_groups = {}

        # mapping of groupname -> 1
        # if the key exists the group exists
        self._ip_groups = {}

        # IPRange -> [group1, group2, ...]
        self._iprange_to_group = {}
        

    # MANIPULATORS
    def addGroupToManagedNormalGroups(self, user, group):
        self._user_to_managed_normal_groups.setdefault(user, {})[group] = 1
        self._p_changed = 1

    def removeGroupFromManagedNormalGroups(self, user, group):
        del self._user_to_managed_normal_groups[user][group]
        self._p_changed = 1

    def addGroupToManagedVirtualGroups(self, user, group):
        self._user_to_managed_virtual_groups.setdefault(user, {})[group] = 1
        self._p_changed = 1

    def removeGroupFromManagedVirtualGroups(self, user, group):
        del self._user_to_managed_virtual_groups[user][group]
        self._p_changed = 1

    def getManagedNormalGroups(self, user):
        return self._user_to_managed_normal_groups.get(user, {})

    def getManagedVirtualGroups(self, user):
        return self._user_to_managed_virtual_groups.get(user, {})

    security.declareProtected(GroupsPermissions.ChangeGroups,
                              'addUserToZODBGroup')
    def addUserToZODBGroup(self, userid, group):
        """Add a user to a group defined in the ZODB.
        """
        self._users.setdefault(userid, {})[group] = 1
        self._p_changed = 1

    security.declareProtected(GroupsPermissions.ChangeGroups,
                              'removeUserFromZODBGroup')
    def removeUserFromZODBGroup(self, userid, group):
        """Remove a user from a group defined in the ZODB.
        """
        del self._users[userid][group]
        self._p_changed = 1

    security.declareProtected(GroupsPermissions.ChangeGroups,
                              'addNormalGroup')
    def addNormalGroup(self, group):
        """Register the name of a normal group. This is intended to be used by
        the user interface mainly; in the logic unregistered groups will work
        as well. Any externally defined group (say by LDAP) is also
        considered to be a normal group.
        """
        self._normal_groups[group] = 1
        self._p_changed = 1

    security.declareProtected(GroupsPermissions.ChangeGroups,
                              'removeNormalGroup')
    def removeNormalGroup(self, group):
        """Unregister a group.
        """
        if self._group_to_virtual.has_key(group):
            raise GroupsError, "This group is used by a virtual group"
        del self._normal_groups[group]
        self._p_changed = 1
        
    security.declareProtected(
        GroupsPermissions.ChangeGroups, 'addVirtualGroup')
    def addVirtualGroup(self, virtual_group):
        """Add a new virtual group 
        """
        if self._normal_groups.has_key(virtual_group):
            raise GroupsError, "Can't create virtual group that is a normal group."
        self._virtual_groups[virtual_group] = []
        self._p_changed = 1

    security.declareProtected(
        GroupsPermissions.ChangeGroups, 'removeVirtualGroup')
    def removeVirtualGroup(self, virtual_group):
        """Remove a virtual group.
        """
        del self._virtual_groups[virtual_group]
        for group, virtual_groups  in self._group_to_virtual.items():
            if virtual_group in virtual_groups:
                virtual_groups.remove(virtual_group)
                self._group_to_virtual[group] = virtual_groups
        self._p_changed = 1

    security.declareProtected(
        GroupsPermissions.ChangeGroups, 'addGroupToVirtualGroup')    
    def addGroupToVirtualGroup(self, group, virtual_group):
        """Add a group to a virtual group the user
        will automatically be in.
        """
        virtual_groups = self._virtual_groups
        g_to_v = self._group_to_virtual

        if virtual_groups.has_key(group):
            raise GroupsError, "Can't create virtual group that depends on another virtual group: %s" % virtual_group
        virtual_groups[virtual_group].append(group)
        g_to_v.setdefault(group, []).append(virtual_group)
        self._p_changed = 1

    security.declareProtected(
        GroupsPermissions.ChangeGroups, 'removeGroupFromVirtualGroup')
    def removeGroupFromVirtualGroup(self, group, virtual_group):
        virtual_groups = self._virtual_groups
        g_to_v = self._group_to_virtual
        if virtual_groups.has_key(virtual_group):                        
            virtual_groups[virtual_group].remove(group)
            g_to_v[group].remove(virtual_group)
            if not g_to_v[group]:
                del g_to_v[group]
            self._p_changed = 1

    security.declareProtected(GroupsPermissions.ChangeGroups,
                              'clear')
    def clear(self):
        """Clear all group info
        """
        self._users = {}
        self._virtual_groups = {}
        self._group_to_virtual = {}
        self._normal_groups = {}
        self._v_cache_ipgroups = {}
        self._iprange_to_group = {}
        self._ip_groups = {}
    
    def _getNormalGroups(self, user):
        """Get the normal (non-virtual) groups a user is in. Can
        be overridden by subclasses. In this case, the user only
        is in ZODB defined groups but other GroupsService implementations
        could list groups defined in LDAP, etc.
        """
        return self._users.get(user.getUserName(), {})
        

    def addIPGroup(self, group):
        """add an ip group"""
        self._ip_groups[group] = 1
        self._p_changed = 1
    
    def addIPRangeToIPGroup(self, iprange, group):
        if not self._ip_groups.get(group):
            raise GroupsError, "IPGroup %s doesn't exist" % group
        self._iprange_to_group.setdefault(iprange, {})[group] = 1
        self._v_cache_ipgroups = {} # invalidate cache
        self._p_changed = 1
   
    def removeIPGroup(self, group):
        del self._ip_groups[group]
        for iprange, groups in self._iprange_to_group.items():
            if group in groups.keys():
                del groups[group]
        self._v_cache_ipgroups = {} # invalidate cache
        self._p_changed = 1
        
    def removeIPRangeFromIPGroup(self, iprange, group):
        del self._iprange_to_group[iprange][group]
        self._v_cache_ipgroups = {} # invalidate cache
        self._p_changed = 1
        
    # ACCESSORS
    security.declareProtected(
        GroupsPermissions.AccessGroups, 'getGroups')
    def getGroups(self, user):
        """Get the groups (normal and virtual) the user is in. Expects user
        object that has at least getUserName() defined.
        """
        # get the non-virtual groups the user is in
        groups = {}
        self._getGroups_normal(user, groups)
        self._getGroups_virtual(user, groups)
        self._getGroups_ip(user, groups)
        return groups


    def _getGroups_normal(self, user, groups):
        groups.update(self._getNormalGroups(user))

    def _getGroups_virtual(self, user, groups):
        # check whether we also have some virtual groups
        virtual_groups = self._virtual_groups
        g_to_v = self._group_to_virtual
        found_possible_virtual_groups = {}
        for group in groups.keys():
            for possible_virtual_group in g_to_v.get(group, []):
                count = found_possible_virtual_groups.get(
                    possible_virtual_group, 0)
                found_possible_virtual_groups[possible_virtual_group] = \
                    count + 1
        for possible_virtual_group, count in \
                found_possible_virtual_groups.items():
            if len(virtual_groups[possible_virtual_group]) == count:
                groups[possible_virtual_group] = 1

    def _getGroups_ip(self, user, groups):
        # checking ip groups
        try:
            request = user.REQUEST
        except AttributeError:
            # fails if user is not acquisition wrapped
            return
        ip = guessHost(request)
        cache = getattr(self, '_v_cache_ipgroups', None)
        if cache is None:
            cache = self._v_cache_ipgroups = {}
        cache_record = cache.get(ip, None)
        if cache_record is not None:
            cache_record.lastaccess = time.time()
            groups.update(cache_record.groups)
            return
        ip_groups = {}
        for iprange, groups_in_range in self._iprange_to_group.items():
            if ip in iprange:
                ip_groups.update(groups_in_range)
        cache[ip] = cache_record = Record()
        cache_record.lastaccess = time.time()
        cache_record.groups = ip_groups
        groups.update(ip_groups)
        self._clean_ip_cache()

    def _clean_ip_cache(self):
        now = time.time()
        lastcheck = getattr(self, '_v_last_clean', now)
        if now - lastcheck > CACHE_TIMEOUT:
            cache = getattr(self, '_v_cache_ipgroups', None)
            if cache is None:
                self._v_cache_ipgroups = {}
                return
            for ip_range, cache_record in self._v_cache_ipgroups.items():
                if now - cache_record.lastaccess > CACHE_TIMEOUT:
                    del self._v_cache_ipgroups[ip_range]
        self._v_last_clean = now
        
    security.declareProtected(GroupsPermissions.AccessGroups,
                              'isNormalGroup')
    def isNormalGroup(self, group):
        """Return true if group is a (non-virtual) normal group.
        """
        return self._normal_groups.has_key(group)

    security.declareProtected(
        GroupsPermissions.Permissions.access_contents_information, 
        'isVirtualGroup')
    def isVirtualGroup(self, group):
        """Returns true if group is a virtual group.
        """
        return self._virtual_groups.has_key(group)

    
    def isIPGroup(self, group):
        """Returns True if group is an IP group"""
        return self._ip_groups.has_key(group)

    security.declareProtected(
        GroupsPermissions.Permissions.access_contents_information, 
        'isGroup')
    def isGroup(self, group):
        """Returns true if group is a group (virtual or not).
        """
        return (self.isNormalGroup(group) or 
            self.isVirtualGroup(group) or
            self.isIPGroup(group))

    security.declareProtected(
        GroupsPermissions.Permissions.access_contents_information,
        'listNormalGroups')
    def listNormalGroups(self):
        result = self._normal_groups.keys()
        result.sort()
        return result

    security.declareProtected(
        GroupsPermissions.Permissions.access_contents_information,
        'listVirtualGroups')
    def listVirtualGroups(self):
        result = self._virtual_groups.keys()
        result.sort()
        return result

    security.declareProtected(
        GroupsPermissions.Permissions.access_contents_information, 
        'listAllGroups')
    def listAllGroups(self):
        result = self.listNormalGroups() + self.listVirtualGroups() + \
            self.listIPGroups()
        result.sort()
        return result

    security.declareProtected(
        GroupsPermissions.AccessGroups, 'listUsersInZODBGroup')
    def listUsersInZODBGroup(self, group):
        result = {}
        for user, groups in self._users.items():
            if groups.has_key(group):
                result[user] = 1
        if not result:
            return []
        return result.keys()
    
    security.declareProtected(
            GroupsPermissions.AccessGroups, 'listGroupsInVirtualGroup')
    def listGroupsInVirtualGroup(self, virtual_group):
        result = {}
        for group, virtual_groups in self._group_to_virtual.items():
            if virtual_group in virtual_groups:
                result[group] = 1
        if not result:
            return []
        return result.keys()

    security.declareProtected(
        GroupsPermissions.AccessGroups, 'groupExists')
    def groupExists(self, group):
        """returns True if group exists"""
        return (self._virtual_groups.has_key(group) or
            self._normal_groups.has_key(group) or
            self._ip_groups.has_key(group))

    security.declareProtected(
        GroupsPermissions.AccessGroups, 'listIPGroups')
    def listIPGroups(self):
        """list ip groups"""
        result = self._ip_groups.keys()
        result.sort()
        return result

    security.declareProtected(
        GroupsPermissions.AccessGroups, 'listIPRangesInIPGroup')
    def listIPRangesInIPGroup(self, ipgroup):
        """return list of ranges in ip group"""
        result = {}
        for iprange, ipgroups in self._iprange_to_group.items():
            if ipgroup in ipgroups.keys():
                result[iprange] = 1
        result = result.keys()
        return result

    # helper
    security.declarePublic('createRange')
    def createIPRange(self, ip_str):
        """factory for creating IPRange objects"""
        return IPRange(ip_str)
    
    
Globals.InitializeClass(GroupsService)
