2012-10-08 18:16:43 +02:00
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
|
|
import os
|
|
|
|
import sys
|
2012-10-16 14:56:54 +02:00
|
|
|
try:
|
|
|
|
import ldap
|
|
|
|
except ImportError:
|
|
|
|
sys.stderr.write('Error: Yunohost CLI Require LDAP lib\n')
|
|
|
|
sys.stderr.write('apt-get install python-ldap\n')
|
|
|
|
sys.exit(1)
|
2012-10-08 18:16:43 +02:00
|
|
|
import ldap.modlist as modlist
|
2012-10-15 22:48:05 +02:00
|
|
|
import json
|
2012-10-08 18:16:43 +02:00
|
|
|
import re
|
|
|
|
import getpass
|
2012-10-23 19:55:40 +02:00
|
|
|
if not __debug__:
|
|
|
|
import traceback
|
2012-10-08 18:16:43 +02:00
|
|
|
|
|
|
|
|
|
|
|
def colorize(astr, color):
|
2012-10-08 22:00:16 +02:00
|
|
|
"""
|
|
|
|
Print with style ;)
|
|
|
|
|
|
|
|
Keyword arguments:
|
|
|
|
astr -- String to colorize
|
|
|
|
color -- Name of the color
|
|
|
|
|
|
|
|
"""
|
2012-10-08 18:16:43 +02:00
|
|
|
color_dict = {
|
|
|
|
'red' : '31',
|
|
|
|
'green' : '32',
|
|
|
|
'yellow': '33',
|
|
|
|
'cyan' : '34',
|
|
|
|
'purple': '35'
|
|
|
|
}
|
|
|
|
return "\033["+ color_dict[color] +"m\033[1m" + astr + "\033[m"
|
|
|
|
|
2012-10-10 14:19:17 +02:00
|
|
|
def pretty_print_dict(d, depth=0):
|
|
|
|
for k,v in sorted(d.items(), key=lambda x: x[0]):
|
2012-10-10 14:47:11 +02:00
|
|
|
k = colorize(k, 'purple')
|
2012-10-28 17:27:47 +01:00
|
|
|
if isinstance(v, list) and len(v) == 1:
|
|
|
|
v = v[0]
|
2012-10-10 14:19:17 +02:00
|
|
|
if isinstance(v, dict):
|
2012-10-28 17:27:47 +01:00
|
|
|
print((" ") * depth + ("%s: " % k))
|
2012-10-10 14:19:17 +02:00
|
|
|
pretty_print_dict(v, depth+1)
|
2012-10-28 17:27:47 +01:00
|
|
|
elif isinstance(v, list):
|
|
|
|
print((" ") * depth + ("%s: " % k))
|
2012-10-10 14:47:11 +02:00
|
|
|
for value in v:
|
2012-10-28 17:27:47 +01:00
|
|
|
print((" ") * (depth+1) + "- " + value)
|
2012-10-10 14:19:17 +02:00
|
|
|
else:
|
2012-10-28 17:27:47 +01:00
|
|
|
print((" ") * depth + "%s: %s" % (k, v))
|
2012-10-10 14:19:17 +02:00
|
|
|
|
2012-10-08 18:16:43 +02:00
|
|
|
def win_msg(astr):
|
2012-10-08 22:00:16 +02:00
|
|
|
"""
|
|
|
|
Display a success message if isatty
|
|
|
|
|
|
|
|
Keyword arguments:
|
|
|
|
astr -- Win message to display
|
|
|
|
|
|
|
|
"""
|
2012-10-08 18:16:43 +02:00
|
|
|
if os.isatty(1):
|
|
|
|
print('\n' + colorize(_("Success: "), 'green') + astr + '\n')
|
|
|
|
|
2012-10-08 22:00:16 +02:00
|
|
|
|
2012-10-08 18:16:43 +02:00
|
|
|
def str_to_func(astr):
|
|
|
|
"""
|
|
|
|
Call a function from a string name
|
|
|
|
|
|
|
|
Keyword arguments:
|
|
|
|
astr -- Name of function to call
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
Function
|
|
|
|
|
|
|
|
"""
|
|
|
|
try:
|
2012-10-10 19:47:57 +02:00
|
|
|
module, _, function = astr.rpartition('.')
|
|
|
|
if module:
|
|
|
|
__import__(module)
|
|
|
|
mod = sys.modules[module]
|
|
|
|
else:
|
|
|
|
mod = sys.modules['__main__'] # default module
|
|
|
|
|
2012-10-08 18:16:43 +02:00
|
|
|
func = getattr(mod, function)
|
2012-10-10 19:47:57 +02:00
|
|
|
except (AttributeError, ImportError):
|
2012-10-15 22:48:05 +02:00
|
|
|
#raise YunoHostError(168, _('Function is not defined'))
|
|
|
|
return None
|
2012-10-08 18:16:43 +02:00
|
|
|
else:
|
|
|
|
return func
|
|
|
|
|
|
|
|
|
2012-11-09 18:04:15 +01:00
|
|
|
def validate(pattern, array):
|
2012-10-10 19:47:57 +02:00
|
|
|
"""
|
|
|
|
Validate attributes with a pattern
|
|
|
|
|
|
|
|
Keyword arguments:
|
2012-11-09 18:04:15 +01:00
|
|
|
pattern -- Regex to match with the strings
|
|
|
|
array -- List of strings to check
|
2012-10-10 19:47:57 +02:00
|
|
|
|
|
|
|
Returns:
|
|
|
|
Boolean | YunoHostError
|
|
|
|
|
|
|
|
"""
|
2012-11-09 18:04:15 +01:00
|
|
|
if isinstance(array, str):
|
|
|
|
array = [array]
|
|
|
|
for string in array:
|
|
|
|
if re.match(pattern, string):
|
|
|
|
pass
|
2012-10-10 19:47:57 +02:00
|
|
|
else:
|
2012-11-09 18:04:15 +01:00
|
|
|
raise YunoHostError(22, _('Invalid attribute') + ' ' + string)
|
|
|
|
return True
|
2012-10-10 19:47:57 +02:00
|
|
|
|
2012-10-26 15:26:50 +02:00
|
|
|
def get_required_args(args, required_args, password=False):
|
|
|
|
"""
|
|
|
|
Input missing values or raise Exception
|
|
|
|
|
|
|
|
Keyword arguments:
|
|
|
|
args -- Available arguments
|
|
|
|
required_args -- Dictionary of required arguments and input phrase
|
|
|
|
password -- True|False Hidden password double-input needed
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
args
|
|
|
|
|
|
|
|
"""
|
|
|
|
try:
|
|
|
|
for arg, phrase in required_args.items():
|
|
|
|
if not args[arg] and arg != 'password':
|
|
|
|
if os.isatty(1):
|
|
|
|
args[arg] = raw_input(colorize(phrase + ': ', 'cyan'))
|
|
|
|
else:
|
|
|
|
raise Exception #FIX
|
|
|
|
# Password
|
2012-10-25 19:52:52 +02:00
|
|
|
if 'password' in required_args and password:
|
|
|
|
if not args['password']:
|
|
|
|
if os.isatty(1):
|
|
|
|
args['password'] = getpass.getpass(colorize(required_args['password'] + ': ', 'cyan'))
|
|
|
|
pwd2 = getpass.getpass(colorize('Retype ' + required_args['password'][0].lower() + required_args['password'][1:] + ': ', 'cyan'))
|
|
|
|
if args['password'] != pwd2:
|
|
|
|
raise YunoHostError(22, _("Passwords doesn't match"))
|
|
|
|
else:
|
|
|
|
raise YunoHostError(22, _("Missing arguments"))
|
2012-10-26 15:26:50 +02:00
|
|
|
except KeyboardInterrupt, EOFError:
|
2012-10-29 17:38:05 +01:00
|
|
|
raise YunoHostError(125, _("Interrupted"))
|
2012-10-26 15:26:50 +02:00
|
|
|
|
|
|
|
return args
|
|
|
|
|
2012-10-10 19:47:57 +02:00
|
|
|
|
2012-10-14 21:48:16 +02:00
|
|
|
def display_error(error):
|
|
|
|
"""
|
|
|
|
Nice error displaying
|
|
|
|
|
|
|
|
"""
|
|
|
|
if not __debug__ :
|
|
|
|
traceback.print_exc()
|
|
|
|
if os.isatty(1):
|
|
|
|
print('\n' + colorize(_("Error: "), 'red') + error.message)
|
|
|
|
else:
|
|
|
|
print(json.dumps({ 'error' : error.message }))
|
|
|
|
|
|
|
|
|
2012-10-08 18:16:43 +02:00
|
|
|
class YunoHostError(Exception):
|
2012-10-08 22:00:16 +02:00
|
|
|
"""
|
|
|
|
Custom exception
|
|
|
|
|
|
|
|
Keyword arguments:
|
|
|
|
code -- Integer error code
|
|
|
|
message -- Error message to display
|
|
|
|
|
|
|
|
"""
|
2012-10-08 18:16:43 +02:00
|
|
|
def __init__(self, code, message):
|
|
|
|
code_dict = {
|
|
|
|
1 : _('Fail'),
|
|
|
|
13 : _('Permission denied'),
|
|
|
|
17 : _('Already exists'),
|
|
|
|
22 : _('Invalid arguments'),
|
|
|
|
87 : _('Too many users'),
|
|
|
|
111 : _('Connection refused'),
|
|
|
|
122 : _('Quota exceeded'),
|
|
|
|
125 : _('Operation canceled'),
|
|
|
|
167 : _('Not found'),
|
|
|
|
168 : _('Undefined'),
|
|
|
|
169 : _('LDAP operation error')
|
|
|
|
}
|
|
|
|
self.code = code
|
|
|
|
self.message = message
|
|
|
|
if code_dict[code]:
|
|
|
|
self.desc = code_dict[code]
|
|
|
|
else:
|
|
|
|
self.desc = code
|
|
|
|
|
2012-10-25 20:40:44 +02:00
|
|
|
def singleton(cls):
|
|
|
|
instances = {}
|
|
|
|
def get_instance():
|
|
|
|
if cls not in instances:
|
|
|
|
instances[cls] = cls()
|
|
|
|
return instances[cls]
|
|
|
|
return get_instance
|
|
|
|
|
|
|
|
@singleton
|
2012-11-08 19:20:13 +01:00
|
|
|
class YunoHostLDAP(object):
|
2012-10-08 18:16:43 +02:00
|
|
|
""" Specific LDAP functions for YunoHost """
|
|
|
|
|
2012-11-08 19:20:13 +01:00
|
|
|
def __enter__(self, password=False):
|
2012-10-25 20:40:44 +02:00
|
|
|
self.__init__(password)
|
|
|
|
return self
|
|
|
|
|
|
|
|
def __init__(self, password=False):
|
2012-10-08 22:00:16 +02:00
|
|
|
"""
|
|
|
|
Connect to LDAP base
|
|
|
|
|
|
|
|
Initialize to localhost, base yunohost.org, prompt for password
|
2012-10-08 18:16:43 +02:00
|
|
|
|
2012-10-08 22:00:16 +02:00
|
|
|
"""
|
2012-10-25 20:40:44 +02:00
|
|
|
self.conn = ldap.initialize('ldap://localhost:389')
|
|
|
|
self.base = 'dc=yunohost,dc=org'
|
|
|
|
if password:
|
|
|
|
self.pwd = password
|
|
|
|
else:
|
2012-11-08 19:20:13 +01:00
|
|
|
try:
|
2012-10-25 20:40:44 +02:00
|
|
|
self.pwd = getpass.getpass(colorize(_('Admin Password: '), 'yellow'))
|
|
|
|
except KeyboardInterrupt, EOFError:
|
|
|
|
raise YunoHostError(125, _("Interrupted"))
|
|
|
|
try:
|
|
|
|
self.conn.simple_bind_s('cn=admin,' + self.base, self.pwd)
|
|
|
|
except ldap.INVALID_CREDENTIALS:
|
|
|
|
raise YunoHostError(13, _('Invalid credentials'))
|
2012-11-08 19:20:13 +01:00
|
|
|
|
|
|
|
def __exit__(self, type, value, traceback):
|
|
|
|
self.disconnect()
|
2012-10-08 22:00:16 +02:00
|
|
|
|
2012-10-08 18:16:43 +02:00
|
|
|
def disconnect(self):
|
2012-10-08 22:00:16 +02:00
|
|
|
"""
|
|
|
|
Unbind from LDAP
|
|
|
|
|
|
|
|
Returns
|
|
|
|
Boolean | YunoHostError
|
2012-10-08 18:16:43 +02:00
|
|
|
|
2012-10-08 22:00:16 +02:00
|
|
|
"""
|
2012-10-08 18:16:43 +02:00
|
|
|
try:
|
|
|
|
self.conn.unbind_s()
|
|
|
|
except:
|
|
|
|
raise YunoHostError(169, _('An error occured during disconnection'))
|
|
|
|
else:
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
2012-10-08 22:00:16 +02:00
|
|
|
def search(self, base=None, filter='(objectClass=*)', attrs=['dn']):
|
|
|
|
"""
|
|
|
|
Search in LDAP base
|
|
|
|
|
|
|
|
Keyword arguments:
|
|
|
|
base -- Base to search into
|
|
|
|
filter -- LDAP filter
|
|
|
|
attrs -- Array of attributes to fetch
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
Boolean | Dict
|
|
|
|
|
|
|
|
"""
|
2012-10-08 18:16:43 +02:00
|
|
|
if not base:
|
|
|
|
base = self.base
|
|
|
|
|
|
|
|
try:
|
|
|
|
result = self.conn.search_s(base, ldap.SCOPE_SUBTREE, filter, attrs)
|
|
|
|
except:
|
|
|
|
raise YunoHostError(169, _('An error occured during LDAP search'))
|
|
|
|
|
|
|
|
if result:
|
|
|
|
result_list = []
|
|
|
|
for dn, entry in result:
|
2012-10-23 19:55:40 +02:00
|
|
|
if attrs != None:
|
|
|
|
if 'dn' in attrs:
|
|
|
|
entry['dn'] = [dn]
|
2012-10-08 18:16:43 +02:00
|
|
|
result_list.append(entry)
|
|
|
|
return result_list
|
|
|
|
else:
|
|
|
|
return False
|
|
|
|
|
2012-10-08 22:00:16 +02:00
|
|
|
|
2012-10-08 18:16:43 +02:00
|
|
|
def add(self, rdn, attr_dict):
|
2012-10-08 22:00:16 +02:00
|
|
|
"""
|
|
|
|
Add LDAP entry
|
|
|
|
|
|
|
|
Keyword arguments:
|
|
|
|
rdn -- DN without domain
|
|
|
|
attr_dict -- Dictionnary of attributes/values to add
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
Boolean | YunoHostError
|
2012-10-08 18:16:43 +02:00
|
|
|
|
2012-10-08 22:00:16 +02:00
|
|
|
"""
|
2012-10-08 18:16:43 +02:00
|
|
|
dn = rdn + ',' + self.base
|
|
|
|
ldif = modlist.addModlist(attr_dict)
|
|
|
|
|
|
|
|
try:
|
|
|
|
self.conn.add_s(dn, ldif)
|
|
|
|
except:
|
|
|
|
raise YunoHostError(169, _('An error occured during LDAP entry creation'))
|
|
|
|
else:
|
|
|
|
return True
|
|
|
|
|
2012-10-29 12:35:29 +01:00
|
|
|
def remove(self, rdn):
|
|
|
|
"""
|
|
|
|
Remove LDAP entry
|
|
|
|
|
|
|
|
Keyword arguments:
|
|
|
|
rdn -- DN without domain
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
Boolean | YunoHostError
|
|
|
|
|
|
|
|
"""
|
|
|
|
dn = rdn + ',' + self.base
|
|
|
|
try:
|
|
|
|
self.conn.delete_s(dn)
|
|
|
|
except:
|
|
|
|
raise YunoHostError(169, _('An error occured during LDAP entry deletion'))
|
|
|
|
else:
|
|
|
|
return True
|
|
|
|
|
2012-10-08 18:16:43 +02:00
|
|
|
|
2012-10-23 19:55:40 +02:00
|
|
|
def update(self, rdn, attr_dict, new_rdn=False):
|
|
|
|
"""
|
|
|
|
Modify LDAP entry
|
|
|
|
|
|
|
|
Keyword arguments:
|
|
|
|
rdn -- DN without domain
|
|
|
|
attr_dict -- Dictionnary of attributes/values to add
|
|
|
|
new_rdn -- New RDN for modification
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
Boolean | YunoHostError
|
|
|
|
|
|
|
|
"""
|
|
|
|
dn = rdn + ',' + self.base
|
|
|
|
actual_entry = self.search(base=dn, attrs=None)
|
2012-10-29 15:43:43 +01:00
|
|
|
ldif = modlist.modifyModlist(actual_entry[0], attr_dict, ignore_oldexistent=1)
|
2012-10-23 19:55:40 +02:00
|
|
|
|
|
|
|
try:
|
|
|
|
if new_rdn:
|
|
|
|
self.conn.rename_s(dn, new_rdn)
|
|
|
|
dn = new_rdn + ',' + self.base
|
|
|
|
|
|
|
|
self.conn.modify_ext_s(dn, ldif)
|
|
|
|
except:
|
|
|
|
raise YunoHostError(169, _('An error occured during LDAP entry update'))
|
|
|
|
else:
|
2012-10-23 22:21:18 +02:00
|
|
|
return True
|
2012-10-23 19:55:40 +02:00
|
|
|
|
|
|
|
|
2012-10-08 18:16:43 +02:00
|
|
|
def validate_uniqueness(self, value_dict):
|
2012-10-08 22:00:16 +02:00
|
|
|
"""
|
|
|
|
Check uniqueness of values
|
|
|
|
|
|
|
|
Keyword arguments:
|
|
|
|
value_dict -- Dictionnary of attributes/values to check
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
Boolean | YunoHostError
|
|
|
|
|
|
|
|
"""
|
2012-10-08 18:16:43 +02:00
|
|
|
for attr, value in value_dict.items():
|
|
|
|
if not self.search(filter=attr + '=' + value):
|
|
|
|
continue
|
|
|
|
else:
|
|
|
|
raise YunoHostError(17, _('Attribute already exists') + ' "' + attr + '=' + value + '"')
|
|
|
|
return True
|