#!/usr/bin/env python #coding=utf8 ''' This module provide an abstraction to the clients configuration directory. 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': '', 'role': '', 'tags': ['tag1', 'tag2'], 'perms': Null } Usage example: >>> conf = CCConf('/etc/cloudcontrol/clients') >>> conf.create_account(login='rms', password='secret', role='client') >>> 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' 'role': 'client', 'tags': ['admin'], 'perms': None} >>> conf.remove_account('rms') >>> ''' import hashlib import base64 import random import threading import logging import json import os import re from functools import wraps class CCConf(object): ''' Create a new configuration interface. :param path_directory: the directory to store the configuration files ''' CONF_TEMPLATE = {'password': None, 'type': None, 'tags': [], 'perms': None} RE_SALTPW = re.compile(r'{(?P[A-Z]+)}(?P.+)') def __init__(self, path_directory): self._path = path_directory self._lock = threading.Lock() def _writer(func): ''' Decorator used to threadsafize methods that made write operations on client configuration tree. ''' @wraps(func) def f(self, *args, **kwargs): with self._lock: return func(self, *args, **kwargs) return f def _get_conf(self, login): ''' 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 ''' filename = os.path.join(self._path, '%s.json' % login) if os.path.isfile(filename): conf = json.load(open(filename, 'r')) logging.debug('Getting configuration %s: %s' % (filename, conf)) 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. :param login: login of the client :param conf: configuration to set for the client :raise CCConf.UnknownAccount: if user login is unknown ''' filename = os.path.join(self._path, '%s.json' % login) logging.debug('Writing configuration %s: %s' % (filename, conf)) if os.path.isfile(filename) ^ create: json.dump(conf, open(filename, 'w')) else: raise CCConf.UnknownAccount('%s is not a file' % filename) def show(self, login): ''' Show the configuration for specified account. :param login: the login of the client :return: configuration of user ''' 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 ''' meth = '_auth_%s' % method.lower() if hasattr(self, meth): auth = getattr(self, meth) return auth(password) else: raise CCConf.BadMethodError('Bad hashing method: %s' % repr(method)) 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``. :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 ''' conf = self._get_conf(login) passwd_conf = conf['password'] is_valid = False m = CCConf.RE_SALTPW.match(passwd_conf) if m is None: is_valid = self._auth_plain(passwd_conf, password) else: meth = '_auth_%s' % m.group('method').lower() if hasattr(self, meth): auth = getattr(self, meth) is_valid = auth(password, m.group('password')) == passwd_conf else: logging.warning('Bad authentication method for %s: ' '%s' % (login, m.group('method'))) if is_valid: return conf['role'] else: return None @_writer def set_password(self, login, password, method='ssha'): ''' Update the client's password in the configuration. :param login: login of the user :param password: new password :raise CCConf.UnknownAccount: if user login is unknown ''' conf = self._get_conf(login) password = self._hash_password(password, method) conf['password'] = password self._set_conf(login, conf) @_writer def add_tag(self, login, tag): ''' Add the tag to the user. :param login: login of the user :param tag: tag to add to the user :raise CCConf.UnknownAccount: if user login is unknown ''' logging.debug('Added tag %s for %s account' % (login, tag)) conf = self._get_conf(login) tags = set(conf['tags']) tags.add(tag) conf['tags'] = list(tags) self._set_conf(login, conf) @_writer def remove_tag(self, login, tag): ''' Remove the tag to the user. :param login: login of the user :param tag: tag to remove to the user :raise CCConf.UnknownAccount: if user login is unknown ''' logging.debug('Removed tag %s for %s account' % (login, tag)) conf = self._get_conf(login) tags = set(conf['tags']) tags.remove(tag) conf['tags'] = list(tags) self._set_conf(login, conf) @_writer def remove_account(self, login): ''' Remove the configuration of the account. :param login: login of the account to remove :raise CCConf.UnknownAccount: if user login is unknown ''' logging.debug('Removed %s account' % login) 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) @_writer def create_account(self, login, password, role): ''' Create a new account. :param login: login of the new user :param password: password of the new user :raise CCConf.AlreadyExistingAccount: if the login is already ''' logging.debug('Creating %s account with role %s' % (login, role)) 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() conf['password'] = password conf['role'] = role self._set_conf(login, conf, create=True) 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) class UnknownAccount(Exception): pass class AlreadyExistingAccount(Exception): pass class BadMethodError(Exception): pass