mirror of
https://github.com/YunoHost/yunohost.git
synced 2024-09-03 20:06:10 +02:00
Merge pull request #350 from YunoHost/clean_user.py
[fix] Use SHA-512 for passwords in LDAP
This commit is contained in:
commit
c864565683
3 changed files with 70 additions and 61 deletions
|
@ -78,17 +78,6 @@ user:
|
||||||
--fields:
|
--fields:
|
||||||
help: fields to fetch
|
help: fields to fetch
|
||||||
nargs: "+"
|
nargs: "+"
|
||||||
-f:
|
|
||||||
full: --filter
|
|
||||||
help: LDAP filter used to search
|
|
||||||
-l:
|
|
||||||
full: --limit
|
|
||||||
help: Maximum number of user fetched
|
|
||||||
type: int
|
|
||||||
-o:
|
|
||||||
full: --offset
|
|
||||||
help: Starting number for user fetching
|
|
||||||
type: int
|
|
||||||
|
|
||||||
### user_create()
|
### user_create()
|
||||||
create:
|
create:
|
||||||
|
|
|
@ -122,8 +122,11 @@ def tools_adminpw(auth, new_password):
|
||||||
new_password
|
new_password
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
from yunohost.user import _hash_user_password
|
||||||
try:
|
try:
|
||||||
auth.con.passwd_s('cn=admin,dc=yunohost,dc=org', None, new_password)
|
auth.update("cn=admin", {
|
||||||
|
"userPassword": _hash_user_password(new_password),
|
||||||
|
})
|
||||||
except:
|
except:
|
||||||
logger.exception('unable to change admin password')
|
logger.exception('unable to change admin password')
|
||||||
raise MoulinetteError(errno.EPERM,
|
raise MoulinetteError(errno.EPERM,
|
||||||
|
|
|
@ -24,13 +24,13 @@
|
||||||
Manage users
|
Manage users
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
|
import json
|
||||||
|
import errno
|
||||||
import crypt
|
import crypt
|
||||||
import random
|
import random
|
||||||
import string
|
import string
|
||||||
import json
|
|
||||||
import errno
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import re
|
|
||||||
|
|
||||||
from moulinette import m18n
|
from moulinette import m18n
|
||||||
from moulinette.core import MoulinetteError
|
from moulinette.core import MoulinetteError
|
||||||
|
@ -40,7 +40,7 @@ from yunohost.service import service_status
|
||||||
logger = getActionLogger('yunohost.user')
|
logger = getActionLogger('yunohost.user')
|
||||||
|
|
||||||
|
|
||||||
def user_list(auth, fields=None, filter=None, limit=None, offset=None):
|
def user_list(auth, fields=None):
|
||||||
"""
|
"""
|
||||||
List users
|
List users
|
||||||
|
|
||||||
|
@ -51,21 +51,17 @@ def user_list(auth, fields=None, filter=None, limit=None, offset=None):
|
||||||
fields -- fields to fetch
|
fields -- fields to fetch
|
||||||
|
|
||||||
"""
|
"""
|
||||||
user_attrs = {'uid': 'username',
|
user_attrs = {
|
||||||
'cn': 'fullname',
|
'uid': 'username',
|
||||||
'mail': 'mail',
|
'cn': 'fullname',
|
||||||
'maildrop': 'mail-forward',
|
'mail': 'mail',
|
||||||
'mailuserquota': 'mailbox-quota'}
|
'maildrop': 'mail-forward',
|
||||||
|
'mailuserquota': 'mailbox-quota'
|
||||||
|
}
|
||||||
|
|
||||||
attrs = ['uid']
|
attrs = ['uid']
|
||||||
users = {}
|
users = {}
|
||||||
|
|
||||||
# Set default arguments values
|
|
||||||
if offset is None:
|
|
||||||
offset = 0
|
|
||||||
if limit is None:
|
|
||||||
limit = 1000
|
|
||||||
if filter is None:
|
|
||||||
filter = '(&(objectclass=person)(!(uid=root))(!(uid=nobody)))'
|
|
||||||
if fields:
|
if fields:
|
||||||
keys = user_attrs.keys()
|
keys = user_attrs.keys()
|
||||||
for attr in fields:
|
for attr in fields:
|
||||||
|
@ -77,18 +73,19 @@ def user_list(auth, fields=None, filter=None, limit=None, offset=None):
|
||||||
else:
|
else:
|
||||||
attrs = ['uid', 'cn', 'mail', 'mailuserquota']
|
attrs = ['uid', 'cn', 'mail', 'mailuserquota']
|
||||||
|
|
||||||
result = auth.search('ou=users,dc=yunohost,dc=org', filter, attrs)
|
result = auth.search('ou=users,dc=yunohost,dc=org',
|
||||||
|
'(&(objectclass=person)(!(uid=root))(!(uid=nobody)))',
|
||||||
|
attrs)
|
||||||
|
|
||||||
|
for user in result:
|
||||||
|
entry = {}
|
||||||
|
for attr, values in user.items():
|
||||||
|
if values:
|
||||||
|
entry[user_attrs[attr]] = values[0]
|
||||||
|
|
||||||
|
uid = entry[user_attrs['uid']]
|
||||||
|
users[uid] = entry
|
||||||
|
|
||||||
if len(result) > offset and limit > 0:
|
|
||||||
for user in result[offset:offset + limit]:
|
|
||||||
entry = {}
|
|
||||||
for attr, values in user.items():
|
|
||||||
try:
|
|
||||||
entry[user_attrs[attr]] = values[0]
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
uid = entry[user_attrs['uid']]
|
|
||||||
users[uid] = entry
|
|
||||||
return {'users': users}
|
return {'users': users}
|
||||||
|
|
||||||
|
|
||||||
|
@ -118,33 +115,27 @@ def user_create(auth, username, firstname, lastname, mail, password,
|
||||||
})
|
})
|
||||||
|
|
||||||
# Validate uniqueness of username in system users
|
# Validate uniqueness of username in system users
|
||||||
try:
|
all_existing_usernames = {x.pw_name for x in pwd.getpwall()}
|
||||||
pwd.getpwnam(username)
|
if username in all_existing_usernames:
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
raise MoulinetteError(errno.EEXIST, m18n.n('system_username_exists'))
|
raise MoulinetteError(errno.EEXIST, m18n.n('system_username_exists'))
|
||||||
|
|
||||||
# Check that the mail domain exists
|
# Check that the mail domain exists
|
||||||
if mail[mail.find('@') + 1:] not in domain_list(auth)['domains']:
|
if mail.split("@")[1] not in domain_list(auth)['domains']:
|
||||||
raise MoulinetteError(errno.EINVAL,
|
raise MoulinetteError(errno.EINVAL,
|
||||||
m18n.n('mail_domain_unknown',
|
m18n.n('mail_domain_unknown',
|
||||||
domain=mail[mail.find('@') + 1:]))
|
domain=mail.split("@")[1]))
|
||||||
|
|
||||||
# Get random UID/GID
|
# Get random UID/GID
|
||||||
uid_check = gid_check = 0
|
all_uid = {x.pw_uid for x in pwd.getpwall()}
|
||||||
while uid_check == 0 and gid_check == 0:
|
all_gid = {x.pw_gid for x in pwd.getpwall()}
|
||||||
|
|
||||||
|
uid_guid_found = False
|
||||||
|
while not uid_guid_found:
|
||||||
uid = str(random.randint(200, 99999))
|
uid = str(random.randint(200, 99999))
|
||||||
uid_check = os.system("getent passwd %s" % uid)
|
uid_guid_found = uid not in all_uid and uid not in all_gid
|
||||||
gid_check = os.system("getent group %s" % uid)
|
|
||||||
|
|
||||||
# Adapt values for LDAP
|
# Adapt values for LDAP
|
||||||
fullname = '%s %s' % (firstname, lastname)
|
fullname = '%s %s' % (firstname, lastname)
|
||||||
rdn = 'uid=%s,ou=users' % username
|
|
||||||
char_set = string.ascii_uppercase + string.digits
|
|
||||||
salt = ''.join(random.sample(char_set, 8))
|
|
||||||
salt = '$1$' + salt + '$'
|
|
||||||
user_pwd = '{CRYPT}' + crypt.crypt(str(password), salt)
|
|
||||||
attr_dict = {
|
attr_dict = {
|
||||||
'objectClass': ['mailAccount', 'inetOrgPerson', 'posixAccount'],
|
'objectClass': ['mailAccount', 'inetOrgPerson', 'posixAccount'],
|
||||||
'givenName': firstname,
|
'givenName': firstname,
|
||||||
|
@ -155,7 +146,7 @@ def user_create(auth, username, firstname, lastname, mail, password,
|
||||||
'mail': mail,
|
'mail': mail,
|
||||||
'maildrop': username,
|
'maildrop': username,
|
||||||
'mailuserquota': mailbox_quota,
|
'mailuserquota': mailbox_quota,
|
||||||
'userPassword': user_pwd,
|
'userPassword': _hash_user_password(password),
|
||||||
'gidNumber': uid,
|
'gidNumber': uid,
|
||||||
'uidNumber': uid,
|
'uidNumber': uid,
|
||||||
'homeDirectory': '/home/' + username,
|
'homeDirectory': '/home/' + username,
|
||||||
|
@ -192,7 +183,7 @@ def user_create(auth, username, firstname, lastname, mail, password,
|
||||||
raise MoulinetteError(errno.EPERM,
|
raise MoulinetteError(errno.EPERM,
|
||||||
m18n.n('ssowat_persistent_conf_write_error', error=e.strerror))
|
m18n.n('ssowat_persistent_conf_write_error', error=e.strerror))
|
||||||
|
|
||||||
if auth.add(rdn, attr_dict):
|
if auth.add('uid=%s,ou=users' % username, attr_dict):
|
||||||
# Invalidate passwd to take user creation into account
|
# Invalidate passwd to take user creation into account
|
||||||
subprocess.call(['nscd', '-i', 'passwd'])
|
subprocess.call(['nscd', '-i', 'passwd'])
|
||||||
|
|
||||||
|
@ -298,10 +289,7 @@ def user_update(auth, username, firstname=None, lastname=None, mail=None,
|
||||||
new_attr_dict['cn'] = new_attr_dict['displayName'] = firstname + ' ' + lastname
|
new_attr_dict['cn'] = new_attr_dict['displayName'] = firstname + ' ' + lastname
|
||||||
|
|
||||||
if change_password:
|
if change_password:
|
||||||
char_set = string.ascii_uppercase + string.digits
|
new_attr_dict['userPassword'] = _hash_user_password(change_password)
|
||||||
salt = ''.join(random.sample(char_set, 8))
|
|
||||||
salt = '$1$' + salt + '$'
|
|
||||||
new_attr_dict['userPassword'] = '{CRYPT}' + crypt.crypt(str(change_password), salt)
|
|
||||||
|
|
||||||
if mail:
|
if mail:
|
||||||
auth.validate_uniqueness({'mail': mail})
|
auth.validate_uniqueness({'mail': mail})
|
||||||
|
@ -453,3 +441,32 @@ def _convertSize(num, suffix=''):
|
||||||
return "%3.1f%s%s" % (num, unit, suffix)
|
return "%3.1f%s%s" % (num, unit, suffix)
|
||||||
num /= 1024.0
|
num /= 1024.0
|
||||||
return "%.1f%s%s" % (num, 'Yi', suffix)
|
return "%.1f%s%s" % (num, 'Yi', suffix)
|
||||||
|
|
||||||
|
|
||||||
|
def _hash_user_password(password):
|
||||||
|
"""
|
||||||
|
This function computes and return a salted hash for the password in input.
|
||||||
|
This implementation is inspired from [1].
|
||||||
|
|
||||||
|
The hash follows SHA-512 scheme from Linux/glibc.
|
||||||
|
Hence the {CRYPT} and $6$ prefixes
|
||||||
|
- {CRYPT} means it relies on the OS' crypt lib
|
||||||
|
- $6$ corresponds to SHA-512, the strongest hash available on the system
|
||||||
|
|
||||||
|
The salt is generated using random.SystemRandom(). It is the crypto-secure
|
||||||
|
pseudo-random number generator according to the python doc [2] (c.f. the
|
||||||
|
red square). It internally relies on /dev/urandom
|
||||||
|
|
||||||
|
The salt is made of 16 characters from the set [./a-zA-Z0-9]. This is the
|
||||||
|
max sized allowed for salts according to [3]
|
||||||
|
|
||||||
|
[1] https://www.redpill-linpro.com/techblog/2016/08/16/ldap-password-hash.html
|
||||||
|
[2] https://docs.python.org/2/library/random.html
|
||||||
|
[3] https://www.safaribooksonline.com/library/view/practical-unix-and/0596003234/ch04s03.html
|
||||||
|
"""
|
||||||
|
|
||||||
|
char_set = string.ascii_uppercase + string.ascii_lowercase + string.digits + "./"
|
||||||
|
salt = ''.join([random.SystemRandom().choice(char_set) for x in range(16)])
|
||||||
|
|
||||||
|
salt = '$6$' + salt + '$'
|
||||||
|
return '{CRYPT}' + crypt.crypt(str(password), salt)
|
||||||
|
|
Loading…
Add table
Reference in a new issue