Newer
Older
""" 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': '<client password>',
'role': '<node|client>',
'tags': {'tag1': 'value'},
'perms': Null
}
Usage example:
>>> conf = CCConf('/etc/cloudcontrol/clients')
>>> conf.create_account(login='rms', password='secret', role='cli')
>>> 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'
import hashlib
import base64
import random
import re
""" 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)
""" Create a new configuration interface.
:param path_directory: the directory to store the configuration files
RE_SALTPW = re.compile(r'{(?P<method>[A-Z]+)}(?P<password>.+)')
def __init__(self, logger, path_directory):
self.logger = logger
self._lock = threading.RLock()
def __enter__(self):
return self._lock.__enter__()
def __exit__(self, *args, **kwargs):
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'))
self.logger.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)
self.logger.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 acquire(self):
""" 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.
"""
self._lock.release()
""" Show the configuration for specified account.
:param login: the login of the client
:return: configuration of user
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))
""" 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
passwd_conf = conf['password']
# Check if account password is disabled or if account is disabled:
is_valid = False
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:
self.logger.warning('Bad authentication method for %s: '
'%s', login, m.group('method'))
if is_valid:
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
password = self._hash_password(password, method)
conf['password'] = password
self._set_conf(login, conf)
def add_tag(self, login, tag_name, tag_value):
:param tag_name: tag name to add to the user
:param tag_value: the tag value
self.logger.debug('Added tag %s:%s for %s account',
tag_name, tag_value, login)
conf['tags'][tag_name] = tag_value
:param login: login of the user
:param tag: tag to remove to the user
:raise CCConf.UnknownAccount: if user login is unknown
self.logger.debug('Removed tag %s for %s account', login, tag)
if tag in conf['tags']:
""" Remove the configuration of the account.
:param login: login of the account to remove
:raise CCConf.UnknownAccount: if user login is unknown
self.logger.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)
:param login: login of the new user
:param password: password of the new user
:raise CCConf.AlreadyExistingAccount: if the login is already
self.logger.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['role'] = role
if password is not None:
conf['password'] = self._hash_password(password)
@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)
""" 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
if index is None:
conf['rights'] = []
else:
try:
conf['rights'].pop(index)
except IndexError:
raise CCConf.OutOfRangeIndexError('Bad rule index '
'%s' % repr(index))
def list_accounts(self):
: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
class OutOfRangeIndexError(Exception):
pass