Browse Source

api works, admin check not implemented yet

master
Andrea Cimbalo 8 years ago
commit
785955b6f0
5 changed files with 228 additions and 0 deletions
  1. +2
    -0
      .gitignore
  2. +6
    -0
      config.py.sample
  3. +122
    -0
      lilikusers.py
  4. +53
    -0
      server.py
  5. +45
    -0
      utils.py

+ 2
- 0
.gitignore View File

@ -0,0 +1,2 @@
*.pyc
config.py

+ 6
- 0
config.py.sample View File

@ -0,0 +1,6 @@
DOMAIN = 'dc=lilik', 'dc=it'
PEOPLE = 'o=People'
GROUP = 'o=Group'
#SERVER = ''
#ADMIN_CN = ''
#ADMIN_PASSWORD = ''

+ 122
- 0
lilikusers.py View File

@ -0,0 +1,122 @@
#!/bin/usr/env python3
import ldap3
import utils
import config
def connection_decorator(f):
with ldap3.Connection(config.SERVER, auto_bind=True) as conn:
def wrapped_f(*args, **kwargs):
return f(args[0], conn, *args[1:], **kwargs)
return wrapped_f
def admin_connection_decorator(f):
with ldap3.Connection(config.SERVER, user=config.ADMIN_CN, password=config.ADMIN_PASSWORD, auto_bind=True) as conn:
def wrapped_f(*args, **kwargs):
return f(args[0], conn, *args[1:], **kwargs)
return wrapped_f
class LILiK_USER(object):
_attributes = {'uid': [], 'cn': ['givenName']}
_flags = {'mail': 'accountActive'}
_hosts = ['ltsp',
'users']
_groups = ['admin',
'wiki',
'lilik.it',
'cloud',
'projects',
'teambox',
'im']
_posix_groups = ['users_sites']
def __init__(self, entry, posix_groups):
results = {}
for attribute in self._attributes.keys():
self.__setattr__(attribute, str(entry.__getattr__(attribute)))
services = {}
for service, flag in self._flags.items():
services[service] = flag in entry and bool(entry[flag])
for host in self._hosts:
services[host] = 'host' in entry and host in entry['host']
for group in self._groups:
services[group] = 'memberOf' in entry and utils.ldap_path("cn=%s"%utils.clean_value(group), config.GROUP, config.DOMAIN) in list(entry['memberOf'])
for posix_group in self._posix_groups:
services[posix_group] = posix_group in posix_groups and str(entry.uid) in list(posix_groups[posix_group])
self.__setattr__('services', services)
def to_json(self):
return json.dumps(self, default=lambda o: o.__dict__,)
def to_dict(self):
return self.__dict__
@admin_connection_decorator
def update(self, conn, new_lilik_user):
user_cn = utils.ldap_path('uid=%s'%self.uid, config.PEOPLE, config.DOMAIN)
diff = utils.DictDiffer(new_lilik_user, self.__dict__)
modifiers = {user_cn: {}}
if 'userPassword' in diff.added():
modifiers[user_cn]['userPassword'] = [(ldap3.MODIFY_REPLACE, [new_lilik_user['userPassword']])] #TODO add hash encryption?
for changed in diff.changed():
if changed == 'services':
services_diff = utils.DictDiffer(self.__dict__[changed], new_lilik_user[changed])
for service_changed in services_diff.changed():
if service_changed in self._flags:
flag = self._flags[service_changed]
modifiers[user_cn][flag] = [(ldap3.MODIFY_REPLACE, [new_lilik_user['services'][service_changed]])]
elif service_changed in self._hosts:
action = ldap3.MODIFY_ADD if new_lilik_user['services'][service_changed] else ldap3.MODIFY_DELETE
modifiers[user_cn]['host'] = [(action, [service_changed])]
elif service_changed in self._groups:
group_cn = utils.ldap_path('cn=%s'%service_changed, config.GROUP, config.DOMAIN)
action = ldap3.MODIFY_ADD if new_lilik_user['services'][service_changed] else ldap3.MODIFY_DELETE
modifiers[group_cn] = {'member': [(action, [user_cn])]}
elif service_changed in self._posix_groups:
group_cn = utils.ldap_path('cn=%s'%service_changed, config.GROUP, config.DOMAIN)
action = ldap3.MODIFY_ADD if new_lilik_user['services'][service_changed] else ldap3.MODIFY_DELETE
modifiers[group_cn] = {'memberUid': [(action, [self.uid])]}
else:
raise Exception('Unknown user attribute')
else:
for alias in self._attributes[changed]:
modifiers[user_cn][alias] = [(ldap3.MODIFY_REPLACE, [new_lilik_user[changed]])]
modifiers[user_cn][changed] = [(ldap3.MODIFY_REPLACE, [new_lilik_user[changed]])]
for entry_cn, modifier in modifiers.items():
if modifier:
conn.modify(entry_cn, modifier)
if conn.result['result'] != 0:
return False
return True
class LILiK_LDAP(object):
def login(self, user_name, password):
bind_dn = utils.ldap_path('uid=%s'%utils.clean_user_name(user_name), config.PEOPLE, config.DOMAIN)
c = ldap3.Connection(config.SERVER, user=bind_dn, password=password)
return c.bind()
@connection_decorator
def get_users(self, conn):
conn.search(utils.ldap_path(config.PEOPLE, config.DOMAIN), '(objectclass=posixAccount)', attributes=['uid'])
return [str(a.uid) for a in conn.entries]
@connection_decorator
def get_user(self, conn, user_name):
conn.search(utils.ldap_path(config.PEOPLE, config.DOMAIN), '(&(objectclass=posixAccount)(uid=%s))'%utils.clean_user_name(user_name), attributes=['*', 'memberOf'])
# return clean_user_name(user_name)
if len(conn.entries) == 0:
return None
entry = conn.entries[0]
return LILiK_USER(entry, self.get_posix_groups())
@connection_decorator
def get_posix_groups(self, conn):
conn.search(utils.ldap_path(config.GROUP, config.DOMAIN), '(objectclass=posixGroup)', attributes=['*'])
# return clean_user_name(user_name)
results = {}
for group in conn.entries:
results[str(group.cn)] = list(group.memberUid) if 'memberUid' in group else []
return results

+ 53
- 0
server.py View File

@ -0,0 +1,53 @@
#!/bin/usr/env python3
import lilikusers
import json
from flask import Flask, jsonify
from flask import request, Response
from functools import wraps
app = Flask(__name__)
lilik_ldap = lilikusers.LILiK_LDAP()
def check_auth(user_name, password):
"""This function is called to check if a username /
password combination is valid.
"""
return lilik_ldap.login(user_name, password)
def authenticate():
"""Sends a 401 response that enables basic auth"""
return Response(
'Could not verify your access level for that URL.\n'
'You have to login with proper credentials', 401,
{'WWW-Authenticate': 'Basic realm="Login Required"'})
def requires_auth(f):
@wraps(f)
def decorated(*args, **kwargs):
auth = request.authorization
if not auth or not check_auth(auth.username, auth.password):
return authenticate()
return f(*args, **kwargs)
return decorated
@app.route('/api/users', methods=['GET'])
@requires_auth
def get_users():
''' return the list of users'''
return jsonify(lilik_ldap.get_users())
@app.route('/api/user/<user_name>', methods=['GET'])
@requires_auth
def get_user(user_name):
''' return the list of users'''
return jsonify(lilik_ldap.get_user(user_name).to_dict())
@app.route('/api/user/<user_name>', methods=['POST'])
@requires_auth
def post_user(self, user_name):
new_lilik_user = request.get_json()
return lilik_ldap.get_user(user_name).update(new_lilik_user)
if __name__ == '__main__':
app.run(debug=True)

+ 45
- 0
utils.py View File

@ -0,0 +1,45 @@
def ldap_path(entry, *arg):
if arg:
return "%s,%s"%(ldap_path(entry), ldap_path(arg))
else:
if type(entry) == tuple:
return ldap_path(*entry)
if entry.find(',') != -1:
print(entry)
raise Exception('Warning: ldap query injection detected')
return entry
def clean_user_name(user_name):
return clean_value(user_name.rstrip('lilik.it'))
def clean_value(value):
'''
TODO:
search_filter: the filter of the search request. It must conform to the LDAP
filter syntax specified in RFC4515. If the search filter contains the
following characters you must use the relevant escape ASCII sequence, as per
RFC4515 (section 3):
* -> \\2A, ( -> \\28, ) -> \\29, \ -> \\5C, chr(0) -> \\00.
'''
return value
class DictDiffer(object):
"""
Calculate the difference between two dictionaries as:
(1) items added
(2) items removed
(3) keys same in both but changed values
(4) keys same in both and unchanged values
"""
def __init__(self, current_dict, past_dict):
self.current_dict, self.past_dict = current_dict, past_dict
self.set_current, self.set_past = set(current_dict.keys()), set(past_dict.keys())
self.intersect = self.set_current.intersection(self.set_past)
def added(self):
return self.set_current - self.intersect
def removed(self):
return self.set_past - self.intersect
def changed(self):
return set(o for o in self.intersect if self.past_dict[o] != self.current_dict[o])
def unchanged(self):
return set(o for o in self.intersect if self.past_dict[o] == self.current_dict[o])

Loading…
Cancel
Save