# 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 . import ldap import logging import logging.config import urllib2 import uuid import json from ConfigParser import ConfigParser config = ConfigParser() config.read('ldap_sync.conf') logging.config.fileConfig('ldap_sync.conf') logger = logging.getLogger() class InvalidResponseIDError(Exception): ''' Request and response don't have the same UUID. ''' class RhodecodeResponseError(Exception): ''' 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. ''' class RhodecodeAPI(): def __init__(self, url, key): self.url = url self.key = key def get_api_data(self, id, method, args): """Prepare dict for API post.""" return { "id": id, "api_key": self.key, "method": method, "args": args } def rhodecode_api_post(self, method, args): """Send a generic API post to Rhodecode. This will generate the UUID for validation check after the response is returned. Handle errors and get the result back. """ id = str(uuid.uuid1()) data = self.get_api_data(id, method, args) data = json.dumps(data) headers = {'content-type': 'text/plain'} req = urllib2.Request(self.url, data, headers) response = urllib2.urlopen(req) response = json.load(response) if id != response["id"]: raise InvalidResponseIDError("UUID does not match.") if response["error"] != None: raise RhodecodeResponseError(response["error"]) return response["result"] def create_group(self, name, active=True): """Create the Rhodecode user group.""" args = { "group_name": name, "active": str(active) } self.rhodecode_api_post("create_users_group", args) def add_membership(self, group, username): """Add specific user to a group.""" args = { "usersgroupid": group, "userid": username } result = self.rhodecode_api_post("add_user_to_users_group", args) 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 } result = self.rhodecode_api_post("remove_user_from_users_group", args) 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} members = self.rhodecode_api_post("get_users_group", args)['members'] 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} return self.rhodecode_api_post("get_users_group", args) def get_user(self, username): """Return user info.""" args = {"userid": username} return self.rhodecode_api_post("get_user", args) class LdapClient(): 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 def __del__(self): 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(): def __init__(self): self.ldap_client = LdapClient(config.get("ldap_config", "ldap_uri"), config.get("ldap_config", "ldap_user"), config.get("ldap_config", "ldap_key"), config.get("ldap_config", "base_dn")) self.rhodecode_api = RhodecodeAPI(config.get("ldap_config", "api_url"), config.get("ldap_config", "api_key")) def update_groups_from_ldap(self): """Add all the groups from LDAP to Rhodecode.""" logger.info("Start importing groups to Rhodecode.") added = existing = 0 groups = self.ldap_client.get_groups() for group in groups: try: self.rhodecode_api.create_group(group) logger.debug("Added group: %s" % group) added += 1 except Exception as e: logger.warn("Skip group %s. Reason: %s" % (group, e)) existing += 1 logger.info("End importing groups to Rhodecode. " "Added: %s, Existing: %s." % (added, existing)) def update_memberships_from_ldap(self, group): """Update memberships in rhodecode based on the LDAP groups.""" 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. logger.debug("Remove memberships for users not in LDAP group.") try: rhodecode_members = self.rhodecode_api.get_group_members(group) except Exception as e: logger.warn("Could not get members for group %s. Reason: %s" % (group, e)) for rhodecode_member in rhodecode_members: if rhodecode_member not in group_users: try: self.rhodocode_api.remove_membership(group, rhodecode_member) except UserNotInGroupError: # This should not actually happen but log it if it does. logger.debug("User not in Rhodecode group.") except RhodecodeResponseError as e: logger.warn("Membership for user %s could not be " "removed from group %s. Reason" % (rhodecode_member, group, e)) # Add memberships. for member in group_users: try: self.rhodecode_api.add_membership(group, member) except UserAlreadyInGroupError: logger.debug("User already in Rhodecode group.") except RhodecodeResponseError as e: logger.warn("Membership for user %s could not be " "added to the group %s. Reason: %s" % (member, group, e)) if __name__ == '__main__': sync = LdapSync() sync.update_groups_from_ldap() groups = sync.ldap_client.get_groups() for group in groups: sync.update_memberships_from_ldap(group)