Newer
Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#!/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', '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<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)
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
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