0001"""Objects representing users, their passwords, roles and groups
0002
0003The objects defined in this file used in conjunction with authentication,
0004authorization and permission objects form a complete user management system.
0005
0006However, there is no requirement to use this user management API at all. If you
0007define your own authentication checks by specifying your own
0008``valid_password()`` or ``digest_password()`` methods when setting up the
0009authentication middleware, and you create your own permissions objects based on
0010your own requriements then you will have no need for this implementation. It is
0011simply provided as a useful default implementation for users looking for a
0012simple, ready made solution that doesn't require any integration. 
0013
0014The implementation consists of the following:
0015
0016``authkit.authenticate.valid_password()``
0017    A ``valid_password()`` implementation used by default with the
0018    ``basic`` or ``form`` authentication methods that checks usernames and
0019    passwords against those defined in the user management API object.
0020 
0021``authkit.authenticate.digest_password()``
0022    A ``digest_password()`` implementation used by default with the
0023    ``digest`` authentication which produces a digest from the users set up
0024    in the user management API object.
0025
0026``authkit.permissions.HasAuthKitRole``
0027    A permission object which checks the signed in user's role from the 
0028    user management API object.
0029
0030``authkit.permissions.HasAuthKitGroup``
0031    A permission object which checks the signed in user's group from the 
0032    user management API object.
0033
0034``authkit.permissions.ValidAuthKitUser``
0035    A permission object which checks the signed in user is defined in the 
0036    user management API object.
0037
0038Of course since the user management API is fairly generic, it is possible to
0039have different implementations. This module has two implementations both
0040derived from the base ``Users`` class. They are ``UsersFromString`` and
0041``UsersFromFile``. By default, ``authkit.authenticate.middleware`` uses
0042``UsersFromString`` and expects you to specify your users, groups and roles as
0043a string in the config file in the way described in the main AuthKit manual but
0044you can also specify you wish to use the alternative implementation to load
0045your user data from a file.
0046
0047Of course you are also free to create your own implementation derived from
0048``Users`` and as long as it keeps the same API, the existing functions and
0049permissions mentioned earlier will work without modification when using your
0050user management API object. This means that if your requirements are very
0051simple you might prefer to create a custom ``Users`` object rather than
0052integrate AuthKit into your project in the slightly lower level fashion by
0053defining the ``valid_password()`` and ``digest_password()`` functions and any
0054necessary permissions.
0055
0056If you are using the authentication middleware with users, the ``Users`` object
0057will be available in your code as ``environ[authkit.users]``.  
0058"""
0059
0060import os.path
0061import md5 as _md5
0062from authkit.authenticate import AuthKitConfigError
0063
0064#
0065# Encryption Functions
0066#
0067
0068def md5(password, secret=''):
0069    result = _md5.md5(password)
0070    result.update(secret)
0071    return result.hexdigest()
0072
0073#
0074# Exceptions
0075#
0076
0077class AuthKitNoSuchUserError(Exception):
0078    pass
0079
0080class AuthKitNoSuchRoleError(Exception):
0081    pass
0082
0083class AuthKitNoSuchGroupError(Exception):
0084    pass
0085
0086class AuthKitNotSupportedError(Exception):
0087    pass
0088
0089class AuthKitError(Exception):
0090    pass
0091
0092#
0093# Users classes
0094#
0095
0096class Users(object):
0097    """
0098    Base class from which all other Users classes should be derived.
0099    """
0100    def __init__(self, data, encrypt=None):
0101        self.data = data
0102        if encrypt is None:
0103            def encrypt(password):
0104                return password
0105        self.encrypt = encrypt
0106
0107    # Create Methods
0108    def user_create(self, username, password, group=None):
0109        """
0110        Create a new user with the username, password and group name specified.
0111        """
0112        raise AuthKitNotSupportedError(
0113            "The %s implementation of the User Management API doesn't support this method"%(
0114                self.__class__.__name__
0115            )
0116        )
0117
0118    def role_create(self, role):
0119        """
0120        Add a new role to the system
0121        """
0122        raise AuthKitNotSupportedError(
0123            "The %s implementation of the User Management API doesn't support this method"%(
0124                self.__class__.__name__
0125            )
0126        )
0127
0128    def group_create(self, group):
0129        """
0130        Add a new group to the system
0131        """
0132        raise AuthKitNotSupportedError(
0133            "The %s implementation of the User Management API doesn't support this method"%(
0134                self.__class__.__name__
0135            )
0136        )
0137
0138    # Delete Methods
0139    def user_delete(self, username):
0140        """
0141        Remove the user with the specified username 
0142        """
0143        raise AuthKitNotSupportedError(
0144            "The %s implementation of the User Management API doesn't support this method"%(
0145                self.__class__.__name__
0146            )
0147        )
0148
0149    def role_delete(self, role):
0150        """
0151        Remove the role specified. Rasies an exception if the role is still in use. 
0152        To delete the role and remove it from all existing users use ``role_delete_cascade()``
0153        """
0154        raise AuthKitNotSupportedError(
0155            "The %s implementation of the User Management API doesn't support this method"%(
0156                self.__class__.__name__
0157            )
0158        )
0159
0160    def group_delete(self, group):
0161        """
0162        Remove the group specified. Rasies an exception if the group is still in use. 
0163        To delete the group and remove it from all existing users use ``group_delete_cascade()``
0164        """
0165        raise AuthKitNotSupportedError(
0166            "The %s implementation of the User Management API doesn't support this method"%(
0167                self.__class__.__name__
0168            )
0169        )
0170
0171    # Delete Cascade Methods
0172    def role_delete_cascade(self, role):
0173        """
0174        Remove the role specified and remove the role from any users who used it
0175        """
0176        raise AuthKitNotSupportedError(
0177            "The %s implementation of the User Management API doesn't support this method"%(
0178                self.__class__.__name__
0179            )
0180        )
0181
0182    def group_delete_cascade(self, group):
0183        """
0184        Remove the group specified and remove the group from any users who used it
0185        """
0186        raise AuthKitNotSupportedError(
0187            "The %s implementation of the User Management API doesn't support this method"%(
0188                self.__class__.__name__
0189            )
0190        )
0191
0192    # Existence Methods
0193    def user_exists(self, username):
0194        """
0195        Returns ``True`` if a user exists with the given username, ``False`` otherwise. Usernames are case insensitive.
0196        """
0197        raise AuthKitNotSupportedError(
0198            "The %s implementation of the User Management API doesn't support this method"%(
0199                self.__class__.__name__
0200            )
0201        )
0202
0203    def role_exists(self, role):
0204        """
0205        Returns ``True`` if the role exists, ``False`` otherwise. Roles are case insensitive.
0206        """
0207        raise AuthKitNotSupportedError(
0208            "The %s implementation of the User Management API doesn't support this method"%(
0209                self.__class__.__name__
0210            )
0211        )
0212
0213    def group_exists(self, group):
0214        """
0215        Returns ``True`` if the group exists, ``False`` otherwise. Groups are case insensitive.
0216        """
0217        raise AuthKitNotSupportedError(
0218            "The %s implementation of the User Management API doesn't support this method"%(
0219                self.__class__.__name__
0220            )
0221        )
0222
0223    # List Methods
0224    def list_roles(self):
0225        """
0226        Returns a lowercase list of all role names ordered alphabetically
0227        """
0228        raise AuthKitNotSupportedError(
0229            "The %s implementation of the User Management API doesn't support this method"%(
0230                self.__class__.__name__
0231            )
0232        )
0233
0234    def list_users(self):
0235        """
0236        Returns a lowecase list of all usernames ordered alphabetically
0237        """
0238        raise AuthKitNotSupportedError(
0239            "The %s implementation of the User Management API doesn't support this method"%(
0240                self.__class__.__name__
0241            )
0242        )
0243
0244    def list_groups(self):
0245        """
0246        Returns a lowercase list of all groups ordered alphabetically
0247        """
0248        raise AuthKitNotSupportedError(
0249            "The %s implementation of the User Management API doesn't support this method"%(
0250                self.__class__.__name__
0251            )
0252        )
0253
0254    # User Methods
0255    def user(self, username):
0256        """
0257        Returns a dictionary in the following format:
0258
0259        .. code-block :: Python
0260        
0261            {
0262                'username': username,
0263                'group':    group,
0264                'password': password,
0265                'roles':    [role1,role2,role3... etc]
0266            }
0267
0268        The role names are ordered alphabetically
0269        Raises an exception if the user doesn't exist.
0270        """
0271        raise AuthKitNotSupportedError(
0272            "The %s implementation of the User Management API doesn't support this method"%(
0273                self.__class__.__name__
0274            )
0275        )
0276
0277    def user_roles(self, username):
0278        """
0279        Returns a list of all the role names for the given username ordered alphabetically. Raises an exception if
0280        the username doesn't exist.
0281        """
0282        raise AuthKitNotSupportedError(
0283            "The %s implementation of the User Management API doesn't support this method"%(
0284                self.__class__.__name__
0285            )
0286        )
0287
0288    def user_group(self, username):
0289        """
0290        Returns the group associated with the user or ``None`` if no group is associated.
0291        Raises an exception is the user doesn't exist.
0292        """
0293        raise AuthKitNotSupportedError(
0294            "The %s implementation of the User Management API doesn't support this method"%(
0295                self.__class__.__name__
0296            )
0297        )
0298
0299    def user_password(self, username):
0300        """
0301        Returns the password associated with the user or ``None`` if no password exists.
0302        Raises an exception is the user doesn't exist.
0303        """
0304        raise AuthKitNotSupportedError(
0305            "The %s implementation of the User Management API doesn't support this method"%(
0306                self.__class__.__name__
0307            )
0308        )
0309
0310    def user_has_role(self, username, role):
0311        """
0312        Returns ``True`` if the user has the role specified, ``False`` otherwise. Raises an exception if the user doesn't exist.
0313        """
0314        raise AuthKitNotSupportedError(
0315            "The %s implementation of the User Management API doesn't support this method"%(
0316                self.__class__.__name__
0317            )
0318        )
0319
0320    def user_has_group(self, username, group):
0321        """
0322        Returns ``True`` if the user has the group specified, ``False`` otherwise. The value for ``group`` can be ``None`` to test that the user doesn't belong to a group. Raises an exception if the user doesn't exist.
0323        """
0324        raise AuthKitNotSupportedError(
0325            "The %s implementation of the User Management API doesn't support this method"%(
0326                self.__class__.__name__
0327            )
0328        )
0329
0330    def user_has_password(self, username, password):
0331        """
0332        Returns ``True`` if the user has the password specified, ``False`` otherwise. Passwords are case sensitive.
0333        Raises an exception if the user doesn't exist.
0334        """
0335        raise AuthKitNotSupportedError(
0336            "The %s implementation of the User Management API doesn't support this method"%(
0337                self.__class__.__name__
0338            )
0339        )
0340
0341    def user_set_username(self, username, new_username):
0342        """
0343        Sets the user's username to the lowercase of new_username. 
0344        Raises an exception if the user doesn't exist or if there is already a user with the username specified by ``new_username``.
0345        """
0346        raise AuthKitNotSupportedError(
0347            "The %s implementation of the User Management API doesn't support this method"%(
0348                self.__class__.__name__
0349            )
0350        )
0351
0352    def user_set_group(self, username, group, add_if_necessary=False):
0353        """
0354        Sets the user's group to the lowercase of ``group`` or ``None``. If the group doesn't exist and ``add_if_necessary`` is ``True`` the group will also be added. Otherwise an ``AuthKitNoSuchGroupError`` will be raised.
0355        Raises an exception if the user doesn't exist.
0356        """
0357        raise AuthKitNotSupportedError(
0358            "The %s implementation of the User Management API doesn't support this method"%(
0359                self.__class__.__name__
0360            )
0361        )
0362
0363    def user_add_role(self, username, role, add_if_necessary=False):
0364        """
0365        Sets the user's role to the lowercase of ``role``. If the role doesn't exist and ``add_if_necessary`` is ``True`` the role will also be added. Otherwise an ``AuthKitNoSuchRoleError`` will be raised.
0366        Raises an exception if the user doesn't exist.
0367        """
0368        raise AuthKitNotSupportedError(
0369            "The %s implementation of the User Management API doesn't support this method"%(
0370                self.__class__.__name__
0371            )
0372        )
0373
0374    def user_remove_role(self, username, role):
0375        """
0376        Removes the role from the user specified by ``username``. Raises an exception if the user doesn't exist.
0377        """
0378        raise AuthKitNotSupportedError(
0379            "The %s implementation of the User Management API doesn't support this method"%(
0380                self.__class__.__name__
0381            )
0382        )
0383
0384    def user_remove_group(self, username):
0385        """
0386        Sets the group to ``None`` for the user specified by ``username``. Raises an exception if the user doesn't exist.
0387        """
0388        raise AuthKitNotSupportedError(
0389            "The %s implementation of the User Management API doesn't support this method"%(
0390                self.__class__.__name__
0391            )
0392        )
0393
0394class UsersReadOnly(Users):
0395    """
0396    Like the ``Users`` class except that user information is read only. All the information
0397    is obtained from the attributes self.usernames, self.passwords, self.roles, self.groups
0398    which are expected to be setup in ``__init__()``.
0399    
0400    ``usernames`` should be a list of lowercase usernames
0401    ``passwords``, ``groups`` should be a dictionary where the keys are lowercase usernames
0402    and the values are the corresponding lowercase group name or password.
0403    ``roles`` is similar to ``passwords`` and ``groups`` except values are lists of lowercase role names.
0404    """
0405
0406    # Existence Methods
0407    def user_exists(self, username):
0408        """
0409        Returns ``True`` if a user exists with the given username, ``False`` otherwise. Usernames are case insensitive.
0410        """
0411        return username.lower() in self.list_users()
0412
0413    def role_exists(self, role):
0414        """
0415        Returns ``True`` if the role exists, ``False`` otherwise. Roles are case insensitive.
0416        """
0417        return role.lower() in self.list_roles()
0418
0419    def group_exists(self, group):
0420        """
0421        Returns ``True`` if the group exists, ``False`` otherwise. Groups are case insensitive.
0422        """
0423        return group.lower() in self.list_groups()
0424
0425    # List Methods
0426    def list_roles(self):
0427        """
0428        Returns a lowercase list of all role names ordered alphabetically
0429        """
0430        roles = []
0431        for k,v in self.roles.items():
0432            for role in v:
0433                role_ = role.lower()
0434                if role_ and role_ not in roles:
0435                    roles.append(role_)
0436        roles.sort()
0437        return roles
0438
0439    def list_users(self):
0440        """
0441        Returns a lowecase list of all usernames ordered alphabetically
0442        """
0443        # Return a copy in case someone starts modifying it.
0444        return [u for u in self.usernames]
0445
0446    def list_groups(self):
0447        """
0448        Returns a lowercase list of all groups ordered alphabetically
0449        """
0450        groups = []
0451        for k,v in self.groups.items():
0452            if v and v not in groups:
0453                groups.append(v)
0454        groups.sort()
0455        return groups
0456
0457    # User Methods
0458    def user(self, username):
0459        """
0460        Returns a dictionary in the following format:
0461
0462        .. code-block :: Python
0463        
0464            {
0465                'username': username,
0466                'group':    group,
0467                'password': password,
0468                'roles':    [role1,role2,role3... etc]
0469            }
0470
0471        The role names are ordered alphabetically
0472        Raises an exception if the user doesn't exist.
0473        """
0474        username = username.lower()
0475        if not username in self.usernames:
0476            raise AuthKitNoSuchUserError("No user named %r"%username)
0477        else:
0478            return {
0479                'username': username,
0480                'group':    self.user_group(username),
0481                'password': self.user_password(username),
0482                'roles':    self.user_roles(username),
0483            }
0484
0485    def user_roles(self, username):
0486        """
0487        Returns a list of all the role names for the given username ordered alphabetically. Raises an exception if
0488        the username doesn't exist.
0489        """
0490        username = username.lower()
0491        if not username in self.usernames:
0492            raise AuthKitNoSuchUserError("No user named %r"%username)
0493        return self.roles[username]
0494
0495    def user_group(self, username):
0496        """
0497        Returns the group associated with the user or ``None`` if no group is associated.
0498        Raises an exception is the user doesn't exist.
0499        """
0500        username = username.lower()
0501        if not username in self.usernames:
0502            raise AuthKitNoSuchUserError("No user named %r"%username)
0503        return self.groups[username]
0504
0505    def user_password(self, username):
0506        """
0507        Returns the password associated with the user or ``None`` if no password exists.
0508        Raises an exception is the user doesn't exist.
0509        """
0510        username = username.lower()
0511        if not username in self.usernames:
0512            raise AuthKitNoSuchUserError("No user named %r"%username)
0513        return self.passwords[username]
0514
0515    def user_has_role(self, username, role):
0516        """
0517        Returns ``True`` if the user has the role specified, ``False`` otherwise. Raises an exception if the user doesn't exist.
0518        """
0519        return role.lower() in self.user_roles(username)
0520
0521    def user_has_group(self, username, group):
0522        """
0523        Returns ``True`` if the user has the group specified, ``False`` otherwise. Raises an exception if the user doesn't exist.
0524        """
0525        return group.lower() == self.user_group(username.lower())
0526
0527    def user_has_password(self, username, password):
0528        """
0529        Passwords are case sensitive.
0530        Returns ``True`` if the user has the password specified, ``False`` otherwise. 
0531        Raises an exception if the user doesn't exist.
0532        """
0533        return self.encrypt(password) == self.user_password(username.lower())
0534
0535def parse(data):
0536    """
0537    Parses the user data
0538    """
0539    passwords = {}
0540    roles = {}
0541    groups = {}
0542    counter = 1
0543    for line in data.split('\n'):
0544        line = line.strip()
0545        if not line:
0546            continue
0547        role_list = []
0548        parts = line.split(' ')
0549        if len(parts) > 1:
0550            for role in parts[1:]:
0551                if role:
0552                    role_list.append(role.strip().lower())
0553        role_list.sort()
0554        group = None
0555        parts = parts[0].split(':')
0556        if len(parts) > 1:
0557            password = parts[1]
0558            if not password:
0559                'Password for %s is empty'%(
0560                    username,
0561                )
0562            username = parts[0].lower()
0563            if not username:
0564                'Username on line %s is empty'%(
0565                    counter,
0566                )
0567        if len(parts) == 3:
0568            group = parts[2].lower()
0569        if len(parts) <= 1 or len(parts) > 3:
0570            raise AuthKitConfigError(
0571                'Syntax error on line %s of authenticate list'%(
0572                    counter,
0573                )
0574            )
0575        if passwords.has_key(username):
0576            raise AuthKitConfigError(
0577                'Username %r defined twice in authenticate list %r'%(
0578                    username,
0579                    passwords
0580                )
0581            )
0582        <