Newer
Older
#!/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': '<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='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',
'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,
RE_SALTPW = re.compile(r'{(?P<method>[A-Z]+)}(?P<password>.+)')
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
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_name, tag_value):
'''
Add the tag to the user.
:param login: login of the user
:param tag_name: tag name to add to the user
:param tag_value: the tag value
:raise CCConf.UnknownAccount: if user login is unknown
'''
logging.debug('Added tag %s for %s account' % (login, tag))
conf = self._get_conf(login)
conf['tags'][tag_name] = tag_value
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)
if tag in conf['tags']:
conf['tags'][tag]
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'] = self._hash_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