Skip to content
conf.py 12.6 KiB
Newer Older
""" This module provide an abstraction to the clients configuration directory.
Antoine Millet's avatar
Antoine Millet committed

The client configuration directory contains a list of ``.json`` files, each
file contains the configuration for one client. The username of the client is
the filename (excluding the extension).

The schema of the json file is described below::

    {
     'password': '<client password>',
     'role': '<node|client>',
     'tags': {'tag1': 'value'},
Antoine Millet's avatar
Antoine Millet committed
     'perms': Null
    }

Usage example:

>>> conf = CCConf('/etc/cloudcontrol/clients')
>>> conf.create_account(login='rms', password='secret', role='cli')
Antoine Millet's avatar
Antoine Millet committed
>>> conf.create_account(login='server42', password='secret', role='node')
>>> print conf.authentify('server42', 'pouet')
None
>>> print conf.authentify('server42', 'secret')
u'node'
>>> conf.add_tag('rms', 'admin')
>>> conf.show('rms')
{'password': 'secret'
Antoine Millet's avatar
Antoine Millet committed
 'perms': None}
>>> conf.remove_account('rms')
Antoine Millet's avatar
Antoine Millet committed
>>>
Antoine Millet's avatar
Antoine Millet committed

import hashlib
import base64
import random
Antoine Millet's avatar
Antoine Millet committed
import threading
import json
import os
Antoine Millet's avatar
Antoine Millet committed
from functools import wraps

    """ Decorator used to threadsafize methods that made write operations on
        client configuration tree.
    """
Antoine Millet's avatar
Antoine Millet committed
    @wraps(func)
    def f(self, *args, **kwargs):
        with self._lock:
            return func(self, *args, **kwargs)
Antoine Millet's avatar
Antoine Millet committed
    return f
Antoine Millet's avatar
Antoine Millet committed
class CCConf(object):
    """ Create a new configuration interface.
Antoine Millet's avatar
Antoine Millet committed

    :param path_directory: the directory to store the configuration files
Antoine Millet's avatar
Antoine Millet committed

    CONF_TEMPLATE = {'password': None,
Antoine Millet's avatar
Antoine Millet committed
                     'role': None,
                     'rights': []}
Antoine Millet's avatar
Antoine Millet committed

    RE_SALTPW = re.compile(r'{(?P<method>[A-Z]+)}(?P<password>.+)')

Antoine Millet's avatar
Antoine Millet committed
    def __init__(self, logger, path_directory):
        self.logger = logger
Antoine Millet's avatar
Antoine Millet committed
        self._path = path_directory
        self._lock = threading.RLock()
Antoine Millet's avatar
Antoine Millet committed

    def __enter__(self):
        return self._lock.__enter__()

    def __exit__(self, *args, **kwargs):
Antoine Millet's avatar
Antoine Millet committed
        return self._lock.__exit__(*args, **kwargs)
Antoine Millet's avatar
Antoine Millet committed

    def _get_conf(self, login):
Antoine Millet's avatar
Antoine Millet committed
        Return the configuration of a client by its login.

        :param login: login of the client
        :return: the configuration of the client
        :raise CCConf.UnknownAccount: if user login is unknown
Antoine Millet's avatar
Antoine Millet committed

        filename = os.path.join(self._path, '%s.json' % login)
        if os.path.isfile(filename):
            conf = json.load(open(filename, 'r'))
Antoine Millet's avatar
Antoine Millet committed
            self.logger.debug('Getting configuration %s: %s', filename, conf)
Antoine Millet's avatar
Antoine Millet committed
            return conf
        else:
            raise CCConf.UnknownAccount('%s is not a file' % filename)

    def _set_conf(self, login, conf, create=False):
        """ Update the configuration of a client by its login.
Antoine Millet's avatar
Antoine Millet committed

        :param login: login of the client
        :param conf: configuration to set for the client
        :raise CCConf.UnknownAccount: if user login is unknown
Antoine Millet's avatar
Antoine Millet committed

        filename = os.path.join(self._path, '%s.json' % login)
Antoine Millet's avatar
Antoine Millet committed
        self.logger.debug('Writing configuration %s: %s', filename, conf)
Antoine Millet's avatar
Antoine Millet committed
        if os.path.isfile(filename) ^ create:
            json.dump(conf, open(filename, 'w'))
        else:
            raise CCConf.UnknownAccount('%s is not a file' % filename)

        """ Acquire the configuration writing lock for non-atomic configuration
            changes.

        .. warning::
           Don't forget to call the :meth:`release` method after your changes
           for each :meth:`acquire` you made.
        self._lock.acquire()

    def release(self):
        """ Release the configuration writing lock.
        """
Antoine Millet's avatar
Antoine Millet committed
    def show(self, login):
        """ Show the configuration for specified account.
Antoine Millet's avatar
Antoine Millet committed

        :param login: the login of the client
        :return: configuration of user
Antoine Millet's avatar
Antoine Millet committed

        return self._get_conf(login)

    def _unsaltify(self, string, digest_size):
        string = base64.decodestring(string)
        password = string[0:digest_size]
        salt = string[digest_size:]
        return password, salt

    def _get_randsalt(self, size=10):
        salt = ''
        for _ in xrange(size):
            salt += chr(random.randint(1, 255))
        return salt

    def _auth_ssha(self, provided_passwd, configured_passwd=None):
        if configured_passwd is not None:
            salt = self._unsaltify(configured_passwd, 20)[1]
        else:
            salt = self._get_randsalt()
        digest = hashlib.sha1(str(provided_passwd) + salt).digest()
        return '{SSHA}%s' % base64.b64encode(digest + salt)

    def _auth_sha(self, provided_passwd, configured_passwd=None):
        digest = hashlib.sha1(str(provided_passwd)).digest()
        return '{SHA}%s' % base64.b64encode(digest)

    def _auth_smd5(self, provided_passwd, configured_passwd=None):
        if configured_passwd is not None:
            salt = self._unsaltify(configured_passwd, 16)[1]
        else:
            salt = self._get_randsalt()
        digest = hashlib.sha1(str(provided_passwd) + salt).digest()
        return '{SMD5}%s' % base64.b64encode(digest + salt)

    def _auth_md5(self, provided_passwd, configured_passwd=None):
        digest = hashlib.sha1(str(provided_passwd)).digest()
        return '{MD5}%s' % base64.b64encode(digest)

    def _auth_plain(self, provided_passwd, configured_passwd=None):
        return provided_passwd

    def _hash_password(self, password, method='ssha'):
        """ Hash a password using given method and return it.

        :param password: the password to hash
        :param method: the hashing method
        :return: hashed password
Antoine Millet's avatar
Antoine Millet committed

        meth = '_auth_%s' % method.lower()
        if hasattr(self, meth):
            auth = getattr(self, meth)
            return auth(password)
        else:
Antoine Millet's avatar
Antoine Millet committed
            raise CCConf.BadMethodError('Bad hashing method: %s' % repr(method))
Antoine Millet's avatar
Antoine Millet committed
    def authentify(self, login, password):
        """ Authentify the client providing its login and password. The function
            return the role of the client on success, or ``None``.
Antoine Millet's avatar
Antoine Millet committed

        :param login: the login of the client
        :param password: the password of the client
        :return: the client's role or None on failed authentication
        :raise CCConf.UnknownAccount: if user login is unknown
Antoine Millet's avatar
Antoine Millet committed

        conf = self._get_conf(login)
        passwd_conf = conf['password']
Antoine Millet's avatar
Antoine Millet committed

        # Check if account password is disabled or if account is disabled:
        if passwd_conf is None:
Antoine Millet's avatar
Antoine Millet committed
            return None

        m = CCConf.RE_SALTPW.match(passwd_conf)
        if m is None:
            meth = '_auth_plain'
            password_wo_method = passwd_conf
        else:
            meth = '_auth_%s' % m.group('method').lower()
            password_wo_method = m.group('password')

        if hasattr(self, meth):
            auth = getattr(self, meth)
            is_valid = auth(password, password_wo_method) == passwd_conf
        else:
Antoine Millet's avatar
Antoine Millet committed
            self.logger.warning('Bad authentication method for %s: '
                                '%s', login, m.group('method'))
Antoine Millet's avatar
Antoine Millet committed
            return conf['role']
        else:
            return None

    def set_password(self, login, password, method='ssha'):
        """ Update the client's password in the configuration.
Antoine Millet's avatar
Antoine Millet committed

        :param login: login of the user
        :param password: new password
Antoine Millet's avatar
Antoine Millet committed
        :param method: the hashing method to use
Antoine Millet's avatar
Antoine Millet committed
        :raise CCConf.UnknownAccount: if user login is unknown
Antoine Millet's avatar
Antoine Millet committed

        conf = self._get_conf(login)
        password = self._hash_password(password, method)
Antoine Millet's avatar
Antoine Millet committed
        conf['password'] = password
        self._set_conf(login, conf)

    def add_tag(self, login, tag_name, tag_value):
        """ Add the tag to the user.
Antoine Millet's avatar
Antoine Millet committed

        :param login: login of the user
        :param tag_name: tag name to add to the user
        :param tag_value: the tag value
Antoine Millet's avatar
Antoine Millet committed
        :raise CCConf.UnknownAccount: if user login is unknown
Antoine Millet's avatar
Antoine Millet committed

Antoine Millet's avatar
Antoine Millet committed
        self.logger.debug('Added tag %s:%s for %s account',
                          tag_name, tag_value, login)
Antoine Millet's avatar
Antoine Millet committed
        conf = self._get_conf(login)
        conf['tags'][tag_name] = tag_value
Antoine Millet's avatar
Antoine Millet committed
        self._set_conf(login, conf)

Antoine Millet's avatar
Antoine Millet committed
    def remove_tag(self, login, tag):
        """ Remove the tag to the user.
Antoine Millet's avatar
Antoine Millet committed

        :param login: login of the user
        :param tag: tag to remove to the user
        :raise CCConf.UnknownAccount: if user login is unknown
Antoine Millet's avatar
Antoine Millet committed

Antoine Millet's avatar
Antoine Millet committed
        self.logger.debug('Removed tag %s for %s account', login, tag)
Antoine Millet's avatar
Antoine Millet committed
        conf = self._get_conf(login)
        if tag in conf['tags']:
Antoine Millet's avatar
Antoine Millet committed
            del conf['tags'][tag]
Antoine Millet's avatar
Antoine Millet committed
        self._set_conf(login, conf)

Antoine Millet's avatar
Antoine Millet committed
    def remove_account(self, login):
        """ Remove the configuration of the account.
Antoine Millet's avatar
Antoine Millet committed

        :param login: login of the account to remove
        :raise CCConf.UnknownAccount: if user login is unknown
Antoine Millet's avatar
Antoine Millet committed

Antoine Millet's avatar
Antoine Millet committed
        self.logger.debug('Removed %s account', login)
Antoine Millet's avatar
Antoine Millet committed
        filename = os.path.join(self._path, '%s.json' % login)
        if os.path.exists(filename):
            os.remove(filename)
        else:
            raise CCConf.UnknownAccount('%s is not a file' % filename)

Antoine Millet's avatar
Antoine Millet committed
    def create_account(self, login, role, password):
        """ Create a new account.
Antoine Millet's avatar
Antoine Millet committed

        :param login: login of the new user
        :param password: password of the new user
Antoine Millet's avatar
Antoine Millet committed
        :param role: the role of the new user
Antoine Millet's avatar
Antoine Millet committed
        :raise CCConf.AlreadyExistingAccount: if the login is already
Antoine Millet's avatar
Antoine Millet committed

Antoine Millet's avatar
Antoine Millet committed
        self.logger.debug('Creating %s account with role %s', login, role)
Antoine Millet's avatar
Antoine Millet committed
        filename = os.path.join(self._path, '%s.json' % login)
        if os.path.exists(filename):
            raise CCConf.AlreadyExistingAccount('%s found' % filename)
        else:
            conf = CCConf.CONF_TEMPLATE.copy()

Antoine Millet's avatar
Antoine Millet committed
            conf['role'] = role
            if password is not None:
                conf['password'] = self._hash_password(password)
Antoine Millet's avatar
Antoine Millet committed
            self._set_conf(login, conf, create=True)

    @writer
    def copy_account(self, copy_login, login, password):
        """ Create a new account based on another.

        :param copy_login: the login of the account to copy.
        :param password: password of the new user
        :param role: the role of the new user
        :raise CCConf.AlreadyExistingAccount: if the login is already
        :raise CCConf.UnknownAccount: if the copy login doesn't exist

        conf_copy = self._get_conf(copy_login)
        self.create_account(login, conf_copy['role'], password)
        password = self._get_conf(login)['password']
        conf_copy['password'] = password
        self._set_conf(login, conf_copy)

    def add_right(self, login, tql, method=None, target='allow', index=None):
        """ Add a right rule to the provided account.

        :param login: the login of the account
        :param tql: the TQL request to allow
        :param method: the method to which apply the right, None if all
        :param allow: True if the rules allow the call, or False if it deny.
        :param index: the index of the new rule, set None if the rule is
            appended to the end of the ruleset.

        .. note::
           If the index is out of range, the rule will be added to the end of
           the ruleset.

        conf = self._get_conf(login)
        rights = conf['rights']
        rule = {'tql': tql, 'method': method, 'target': target}
        if index is None:
            index = len(rights)
        rights.insert(index, rule)
        self._set_conf(login, conf)

    def remove_right(self, login, index):
        """ Remove a right rule from the provided account.

        :param login: the login of the account
        :param index: the index of the rule to delete or None for all rules

        conf = self._get_conf(login)
        if index is None:
            conf['rights'] = []
        else:
            try:
                conf['rights'].pop(index)
            except IndexError:
                raise CCConf.OutOfRangeIndexError('Bad rule index '
                                                  '%s' % repr(index))
        self._set_conf(login, conf)

    def list_accounts(self):
        """ List all registered accounts.

        :return: :class:`tuple` of :class:`str`, each item being an
            account login

        logins = []
        for filename in os.listdir(self._path):
            login, ext = os.path.splitext(filename)
            if ext == '.json':
                logins.append(login)

        return tuple(logins)

Antoine Millet's avatar
Antoine Millet committed

    class UnknownAccount(Exception):
        pass


    class AlreadyExistingAccount(Exception):
        pass
    class BadMethodError(Exception):
        pass


    class OutOfRangeIndexError(Exception):
        pass