ldap_sync.py
261 lines
| 8.4 KiB
| text/x-python
|
PythonLexer
Bradley M. Kuhn
|
r4187 | # -*- coding: utf-8 -*- | ||
# This program is free software: you can redistribute it and/or modify | ||||
# it under the terms of the GNU General Public License as published by | ||||
# the Free Software Foundation, either version 3 of the License, or | ||||
# (at your option) any later version. | ||||
# | ||||
# This program is distributed in the hope that it will be useful, | ||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||
# GNU General Public License for more details. | ||||
# | ||||
# You should have received a copy of the GNU General Public License | ||||
# along with this program. If not, see <http://www.gnu.org/licenses/>. | ||||
""" | ||||
Mads Kiilerich
|
r5376 | kallithea.bin.ldap_sync | ||
~~~~~~~~~~~~~~~~~~~~~~~ | ||||
Bradley M. Kuhn
|
r4187 | |||
LDAP sync script | ||||
Bradley M. Kuhn
|
r4211 | This file was forked by the Kallithea project in July 2014. | ||
Original author and date, and relevant copyright and licensing information is below: | ||||
Bradley M. Kuhn
|
r4187 | :created_on: Mar 06, 2013 | ||
:author: marcink | ||||
Bradley M. Kuhn
|
r4211 | :copyright: (c) 2013 RhodeCode GmbH, and others. | ||
Bradley M. Kuhn
|
r4208 | :license: GPLv3, see LICENSE.md for more details. | ||
Bradley M. Kuhn
|
r4187 | """ | ||
Mads Kiilerich
|
r7750 | from __future__ import print_function | ||
Mads Kiilerich
|
r8068 | import urllib.request | ||
Bradley M. Kuhn
|
r4187 | import uuid | ||
Mads Kiilerich
|
r8067 | from configparser import ConfigParser | ||
Mads Kiilerich
|
r7718 | |||
import ldap | ||||
Thomas De Schampheleire
|
r7866 | |||
Mads Kiilerich
|
r7987 | from kallithea.lib import ext_json | ||
from kallithea.lib.utils2 import ascii_bytes | ||||
Bradley M. Kuhn
|
r4187 | |||
config = ConfigParser() | ||||
config.read('ldap_sync.conf') | ||||
class InvalidResponseIDError(Exception): | ||||
""" Request and response don't have the same UUID. """ | ||||
Bradley M. Kuhn
|
r4189 | class ResponseError(Exception): | ||
Bradley M. Kuhn
|
r4187 | """ Response has an error, something went wrong with request execution. """ | ||
class UserAlreadyInGroupError(Exception): | ||||
""" User is already a member of the target group. """ | ||||
class UserNotInGroupError(Exception): | ||||
""" User is not a member of the target group. """ | ||||
Bradley M. Kuhn
|
r4189 | class API(object): | ||
Bradley M. Kuhn
|
r4187 | |||
def __init__(self, url, key): | ||||
self.url = url | ||||
self.key = key | ||||
def get_api_data(self, uid, method, args): | ||||
"""Prepare dict for API post.""" | ||||
return { | ||||
"id": uid, | ||||
"api_key": self.key, | ||||
"method": method, | ||||
"args": args | ||||
} | ||||
Bradley M. Kuhn
|
r4189 | def post(self, method, args): | ||
Bradley M. Kuhn
|
r4212 | """Send a generic API post to Kallithea. | ||
Bradley M. Kuhn
|
r4187 | |||
This will generate the UUID for validation check after the | ||||
response is returned. Handle errors and get the result back. | ||||
""" | ||||
uid = str(uuid.uuid1()) | ||||
data = self.get_api_data(uid, method, args) | ||||
Mads Kiilerich
|
r7987 | data = ascii_bytes(ext_json.dumps(data)) | ||
Bradley M. Kuhn
|
r4187 | headers = {'content-type': 'text/plain'} | ||
Mads Kiilerich
|
r8068 | req = urllib.request.Request(self.url, data, headers) | ||
Bradley M. Kuhn
|
r4187 | |||
Mads Kiilerich
|
r8068 | response = urllib.request.urlopen(req) | ||
Mads Kiilerich
|
r7987 | response = ext_json.load(response) | ||
Bradley M. Kuhn
|
r4187 | |||
if uid != response["id"]: | ||||
raise InvalidResponseIDError("UUID does not match.") | ||||
if response["error"] is not None: | ||||
Bradley M. Kuhn
|
r4189 | raise ResponseError(response["error"]) | ||
Bradley M. Kuhn
|
r4187 | |||
return response["result"] | ||||
def create_group(self, name, active=True): | ||||
Bradley M. Kuhn
|
r4212 | """Create the Kallithea user group.""" | ||
Bradley M. Kuhn
|
r4187 | args = { | ||
"group_name": name, | ||||
"active": str(active) | ||||
} | ||||
Bradley M. Kuhn
|
r4189 | self.post("create_user_group", args) | ||
Bradley M. Kuhn
|
r4187 | |||
def add_membership(self, group, username): | ||||
"""Add specific user to a group.""" | ||||
args = { | ||||
"usersgroupid": group, | ||||
"userid": username | ||||
} | ||||
Bradley M. Kuhn
|
r4189 | result = self.post("add_user_to_user_group", args) | ||
Bradley M. Kuhn
|
r4187 | if not result["success"]: | ||
raise UserAlreadyInGroupError("User %s already in group %s." % | ||||
(username, group)) | ||||
def remove_membership(self, group, username): | ||||
"""Remove specific user from a group.""" | ||||
args = { | ||||
"usersgroupid": group, | ||||
"userid": username | ||||
} | ||||
Bradley M. Kuhn
|
r4189 | result = self.post("remove_user_from_user_group", args) | ||
Bradley M. Kuhn
|
r4187 | if not result["success"]: | ||
raise UserNotInGroupError("User %s not in group %s." % | ||||
(username, group)) | ||||
def get_group_members(self, name): | ||||
"""Get the list of member usernames from a user group.""" | ||||
args = {"usersgroupid": name} | ||||
Bradley M. Kuhn
|
r4189 | members = self.post("get_user_group", args)['members'] | ||
Bradley M. Kuhn
|
r4187 | member_list = [] | ||
for member in members: | ||||
member_list.append(member["username"]) | ||||
return member_list | ||||
def get_group(self, name): | ||||
"""Return group info.""" | ||||
args = {"usersgroupid": name} | ||||
Bradley M. Kuhn
|
r4189 | return self.post("get_user_group", args) | ||
Bradley M. Kuhn
|
r4187 | |||
def get_user(self, username): | ||||
"""Return user info.""" | ||||
args = {"userid": username} | ||||
Bradley M. Kuhn
|
r4189 | return self.post("get_user", args) | ||
Bradley M. Kuhn
|
r4187 | |||
class LdapClient(object): | ||||
def __init__(self, uri, user, key, base_dn): | ||||
self.client = ldap.initialize(uri, trace_level=0) | ||||
self.client.set_option(ldap.OPT_REFERRALS, 0) | ||||
self.client.simple_bind(user, key) | ||||
self.base_dn = base_dn | ||||
Mads Kiilerich
|
r7784 | def close(self): | ||
Bradley M. Kuhn
|
r4187 | self.client.unbind() | ||
def get_groups(self): | ||||
"""Get all the groups in form of dict {group_name: group_info,...}.""" | ||||
searchFilter = "objectClass=groupOfUniqueNames" | ||||
result = self.client.search_s(self.base_dn, ldap.SCOPE_SUBTREE, | ||||
searchFilter) | ||||
groups = {} | ||||
for group in result: | ||||
groups[group[1]['cn'][0]] = group[1] | ||||
return groups | ||||
def get_group_users(self, groups, group): | ||||
"""Returns all the users belonging to a single group. | ||||
Based on the list of groups and memberships, returns all the | ||||
users belonging to a single group, searching recursively. | ||||
""" | ||||
users = [] | ||||
for member in groups[group]["uniqueMember"]: | ||||
member = self.parse_member_string(member) | ||||
if member[0] == "uid": | ||||
users.append(member[1]) | ||||
elif member[0] == "cn": | ||||
users += self.get_group_users(groups, member[1]) | ||||
return users | ||||
def parse_member_string(self, member): | ||||
"""Parses the member string and returns a touple of type and name. | ||||
Unique member can be either user or group. Users will have 'uid' as | ||||
prefix while groups will have 'cn'. | ||||
""" | ||||
member = member.split(",")[0] | ||||
return member.split('=') | ||||
class LdapSync(object): | ||||
def __init__(self): | ||||
self.ldap_client = LdapClient(config.get("default", "ldap_uri"), | ||||
config.get("default", "ldap_user"), | ||||
config.get("default", "ldap_key"), | ||||
config.get("default", "base_dn")) | ||||
Mads Kiilerich
|
r5377 | self.kallithea_api = API(config.get("default", "api_url"), | ||
config.get("default", "api_key")) | ||||
Bradley M. Kuhn
|
r4187 | |||
def update_groups_from_ldap(self): | ||||
Bradley M. Kuhn
|
r4212 | """Add all the groups from LDAP to Kallithea.""" | ||
Bradley M. Kuhn
|
r4187 | added = existing = 0 | ||
groups = self.ldap_client.get_groups() | ||||
for group in groups: | ||||
try: | ||||
domruf
|
r6588 | self.kallithea_api.create_group(group) | ||
Bradley M. Kuhn
|
r4187 | added += 1 | ||
except Exception: | ||||
existing += 1 | ||||
return added, existing | ||||
def update_memberships_from_ldap(self, group): | ||||
Bradley M. Kuhn
|
r4189 | """Update memberships based on the LDAP groups.""" | ||
Bradley M. Kuhn
|
r4187 | groups = self.ldap_client.get_groups() | ||
group_users = self.ldap_client.get_group_users(groups, group) | ||||
# Delete memberships first from each group which are not part | ||||
# of the group any more. | ||||
Bradley M. Kuhn
|
r4189 | members = self.kallithea_api.get_group_members(group) | ||
for member in members: | ||||
if member not in group_users: | ||||
Bradley M. Kuhn
|
r4187 | try: | ||
Mads Kiilerich
|
r5377 | self.kallithea_api.remove_membership(group, | ||
Bradley M. Kuhn
|
r4189 | member) | ||
Bradley M. Kuhn
|
r4187 | except UserNotInGroupError: | ||
pass | ||||
# Add memberships. | ||||
for member in group_users: | ||||
try: | ||||
Bradley M. Kuhn
|
r4189 | self.kallithea_api.add_membership(group, member) | ||
Bradley M. Kuhn
|
r4187 | except UserAlreadyInGroupError: | ||
# TODO: handle somehow maybe.. | ||||
pass | ||||
Mads Kiilerich
|
r7784 | def close(self): | ||
self.ldap_client.close() | ||||
Bradley M. Kuhn
|
r4187 | |||
if __name__ == '__main__': | ||||
sync = LdapSync() | ||||
Mads Kiilerich
|
r7750 | print(sync.update_groups_from_ldap()) | ||
Bradley M. Kuhn
|
r4187 | |||
for gr in sync.ldap_client.get_groups(): | ||||
# TODO: exception when user does not exist during add membership... | ||||
# How should we handle this.. Either sync users as well at this step, | ||||
# or just ignore those who don't exist. If we want the second case, | ||||
# we need to find a way to recognize the right exception (we always get | ||||
Bradley M. Kuhn
|
r4189 | # ResponseError with no error code so maybe by return msg (?) | ||
Bradley M. Kuhn
|
r4187 | sync.update_memberships_from_ldap(gr) | ||
Mads Kiilerich
|
r7784 | |||
sync.close() | ||||