#!/usr/bin/env python from ldap import filter from ldap3 import Connection, Server, ANONYMOUS, SIMPLE, SYNC, ASYNC, core from getpass import getpass import configparser import logging import argparse import os import requests import urllib3 import json import mysql.connector def request(resource, seafileURL, seafileToken, method='GET', data=None, dataIsJson=True): if data is None: httpHeaders = {'Accept': 'application/json; charset=utf-8; indent=4', 'Authorization': 'Token {0}'.format(seafileToken)} data = '' else: if dataIsJson: data = json.dumps(data) httpHeaders = {'Content-type': 'application/json', 'Accept': 'application/json; charset=utf-8; indent=4', 'Authorization': 'Token {0}'.format(seafileToken)} else: httpHeaders = {'Content-type': 'application/x-www-form-urlencoded', 'Accept': 'application/json; charset=utf-8; indent=4', 'Authorization': 'Token {0}'.format(seafileToken)} url = '{0}/api/v2.1/{1}'.format(seafileURL, resource) logger.debug('Request URL: {0}'.format(url)) logger.debug('Request Method: {0}'.format(method)) logger.debug('Request Headers: {0}'.format(httpHeaders)) logger.debug('Request Data: {0}'.format(data)) r = requests.request( method, url, data = data, headers = httpHeaders, ) logger.debug('Response: {0}'.format(r)) logger.debug('Request Status Code: {0}'.format(r.status_code)) if r.ok: try: logger.debug('Request Returned JSON: {0}'.format(r.json())) return {'ok': r.ok, 'status_code': r.status_code, 'response': r.json()} except: logger.debug('Request Returned Text: {0}'.format(r.text)) return {'ok': r.ok, 'status_code': r.status_code, 'response': r.text} raise ValueError(r) #def parser = argparse.ArgumentParser(description='Sync LDAP with Seafile') #group = parser.add_mutually_exclusive_group() #group.add_argument('-e', '--encrypt', action='store_true', help='encrypt') #group.add_argument('-d', '--decrypt', action='store_true', help='decrypt') #parser.add_argument('-t', '--text', type=str, help='text to encrypt/decrypt') #group2 = parser.add_mutually_exclusive_group() #group2.add_argument('-t', '--text', type=str, help='text to encrypt/decrypt') #group2.add_argument('-f', '--file', type=str, help='file to encrypt/decrypt, will create a .pynacl file when encrypting, and requires .pynacl file to decrypt') parser.add_argument('-v', '--verbose', action='store_true', help='increase log level to debug') args = parser.parse_args() logger = logging.getLogger(__name__) logFormatter = logging.Formatter('%(asctime)s - [%(levelname)s] %(message)s', datefmt='%Y-%m-%d %H:%M:%S') logger.setLevel(logging.INFO) if args.verbose: logger.setLevel(logging.DEBUG) else: logger.setLevel(logging.INFO) #fileHandler = logging.FileHandler("/tmp/seafile-ldap.log") #fileHandler.setFormatter(logFormatter) #logger.addHandler(fileHandler) consoleHandler = logging.StreamHandler() consoleHandler.setFormatter(logFormatter) logger.addHandler(consoleHandler) # get directory of script cwd = os.path.dirname(os.path.realpath(__file__)) configPath = os.path.join(cwd, 'config.ini') # import the config file logger.debug("Starting to read the config.ini file.") logger.debug("Using file {0}.".format(configPath)) if os.path.exists(configPath): config = configparser.ConfigParser() config.read(configPath) else: logger.critical("Unable to find/read config file. Please ensure the config.ini is in the same directory as this script and readable.") exit(1) seafileToken = config['Seafile']['token'] ccnetPath = config['Seafile']['ccnetPath'] adminEmail = config['Seafile']['adminEmail'] logger.debug("Seafile Token: {0}".format(seafileToken)) logger.debug("Seafile ccnet.conf File Path: {0}".format(ccnetPath)) logger.debug("Seafile Admin Email: {0}".format(adminEmail)) logger.debug("Finished reading the config.ini file.") logger.debug("Starting to read the ccnet.conf file.") if os.path.exists(ccnetPath): ccnetConfig = configparser.ConfigParser() ccnetConfig.read(ccnetPath) else: logger.critical("Unable to find/read ccnet.conf file. Please ensure the ccnet.conf path is correct in the config.ini.") exit(2) # Seafile url seafileURL = ccnetConfig['General']['SERVICE_URL'] # DB config dbEngine = ccnetConfig['Database']['ENGINE'] dbHost = ccnetConfig['Database']['HOST'] dbPort = ccnetConfig['Database'].getint('PORT') dbUser = ccnetConfig['Database']['USER'] dbPassword = ccnetConfig['Database']['PASSWD'] dbName = ccnetConfig['Database']['DB'] dbCharset = ccnetConfig['Database']['CONNECTION_CHARSET'] logger.debug("DB Engine: {0}, DB Host: {1}, DB Port: {2}, DB User: {3}, DB Name: {4}, DB Connection Charset: {5}".format(dbEngine, dbHost, dbPort, dbUser, dbName, dbCharset)) # ldap Config ldapHost = ccnetConfig['LDAP']['HOST'] #ldapPort = ccnetConfig['LDAP SERVER'].getint('port') #ldapSSL = ccnetConfig['LDAP SERVER'].getboolean('ssl') ldapBase = ccnetConfig['LDAP']['BASE'] ldapUserDN = ccnetConfig['LDAP']['USER_DN'] ldapUserPassword = ccnetConfig['LDAP']['PASSWORD'] ldapFilter = ccnetConfig['LDAP']['FILTER'] logger.debug("LDAP Host: {0}, LDAP Base: {1}, LDAP User DN: {2}, LDAP Filter: {3}".format(ldapHost, ldapBase, ldapUserDN, ldapFilter)) logger.debug("Finished reading the ccnet.conf file.") # Config DB Varaibles dbconfig = { 'user': dbUser, 'password': dbPassword, 'host': dbHost, 'port': dbPort, 'database': dbName, 'charset': dbCharset, 'raise_on_warnings': True } # setup the server ldapServer = Server(ldapHost) logger.debug("Setup LDAP server connection uri: {0}".format(ldapServer)) try: ldap = Connection(ldapServer, ldapUserDN, ldapUserPassword, auto_bind=True) except core.exceptions.LDAPBindError as e: logger.critical("LDAP Bind Failed. {0}".format(e)) exit() logger.debug("Bind successful.") # Get seafile users from LDAP logger.debug("Searching for users that have a email address, are enabled, and in the {} group.".format(ldapFilter)) ldap.search(ldapBase, '(&(mail=*)(!(userAccountControl:1.2.840.113556.1.4.803:=2))({0}))'.format(ldapFilter), attributes=['*']) logger.debug("Found {0} LDAP users.".format(len(ldap.entries))) ldapUsers = ldap.entries for user in ldapUsers: logger.debug("User: {0} - Email: {1} - UserDN: {2}".format(user.displayName, user.mail, user.distinguishedName)) # Starting query for seafile ldap users seafileLDAPUsers = [] logger.debug("Starting query for LDAPUsers in Seafile") seafileUsers = request('admin/search-user/?query=@{0}'.format(adminEmail.split("@")[1]), seafileURL, seafileToken)['response']['user_list'] # need to substract one from the len as the admin account is in the list logger.debug("Found {0} Seafile LDAP users".format(len(seafileUsers)-1)) for seafileUser in seafileUsers: if seafileUser['email'] == adminEmail: continue else: logger.debug("User: {0} - Active: {1}".format(seafileUser['email'], bool(seafileUser['is_active']))) seafileLDAPUsers.append(seafileUser) # Loop through the ldap users and make sure they are in the sql ldap users table # if they are not in the sql table, insert a new row to add them # if they are disabled in the sql table, enable them for ldapUser in ldapUsers: logger.debug("Searching if LDAP user {0} is in Seafile".format(ldapUser.mail)) checkSeafileUser = request('admin/search-user/?query={0}'.format(ldapUser.mail), seafileURL, seafileToken)['response']['user_list'] # loop through the results and make sure we match on the email for seafileUser in checkSeafileUser: if seafileUser['email'] == ldapUser.mail: # User is in the sql table # are they active is_active = bool(seafileUser['is_active']) # log the results logger.debug("LDAP User {0} is already in Seafile, Is Active: {1}".format(ldapUser.mail, is_active)) # if user is not active, they should be if not is_active: logger.info("User {0} is NOT active in Seafile".format(ldapUser.mail)) # call the api to enable the user in seafile enableSeafileUser = request('admin/users/{0}/'.format(ldapUser.mail), seafileURL, seafileToken, "PUT", {"is_active": "true"})['response'] if enableSeafileUser['is_active']: logger.info("User {0} was set to active in Seafile".format(ldapUser.mail)) else: logger.error("There was an error setting user {0} to active in Seafile".format(ldapUser.mail)) # user is not in the SQL table else: logger.info("LDAP User {0} is NOT in Seafile".format(ldapUser.mail)) # add user to ldap table cnx = mysql.connector.connect(**dbconfig) cursor = cnx.cursor() query = "INSERT INTO LDAPUsers (email, password, is_staff, is_active) VALUES ('{0}', '', {1}, {2})".format(ldapUser.mail, 0, 1) logger.debug("Query: {0}".format(query)) cursor.execute(query) cnx.commit() row_count = cursor.rowcount if row_count == 1: logger.info("LDAP user {0} was added to the Seafile SQL Table".format(ldapUser.mail)) else: logger.error("Failed to add LDAP user {0} to the Seafile SQL Table".format(ldapUser.mail)) cnx.close() # Update seafile user profile with new name updateSeafileUserName = request('admin/users/{0}/'.format(ldapUser.mail), seafileURL, seafileToken, "PUT", {"name": "{0}".format(ldapUser.displayName)}) if updateSeafileUserName['ok']: logger.debug("User {0} name was updated to {1}".format(ldapUser.mail, ldapUser.displayName)) else: logger.error("There was an error setting user {0} name to {1}".format(ldapUser.mail, ldapUser.displayName)) # Loop through the sql ldap users and disable those not in the ldap list for seafileLDAPUser in seafileLDAPUsers: if not seafileLDAPUser['is_active']: logger.debug("User {0} is already disabled in Seafile".format(seafileLDAPUser['email'])) continue logger.debug("Searching for user {0} that has an email address, are enabled, and in the {1} group.".format(seafileLDAPUser['email'], ldapFilter)) ldap.search(ldapBase, '(&(mail={0})(!(userAccountControl:1.2.840.113556.1.4.803:=2))({1}))'.format(seafileLDAPUser['email'], ldapFilter), attributes=['*']) count = len(ldap.entries) logger.debug("Found {0} LDAP user.".format(count)) if count == 0: # User is not enabled, have email, or in the group, disable their account disableUserinSeafile = request('admin/users/{0}/'.format(seafileLDAPUser['email']), seafileURL, seafileToken, "PUT", {"is_active": "false"})['response'] if not disableUserinSeafile['is_active']: logger.info("User {0} was set to disabled in Seafile".format(seafileLDAPUser['email'])) else: logger.error("There was an error setting user {0} to disabled in Seafile".format(seafileLDAPUser['email'])) # get ad groups and import them into seafile # loop through each group and list members # compare members to users in seafile group # add users to group if missing and in the seafile group # remove members in not in group or seafile group # remove seafile groups if ad group is removed ldap.search(ldapBase, '(objectClass=group)', attributes=['*']) ldapGroups = [] for group in ldap.entries: try: if group.member: logger.debug("Group {0} has {1} members".format(group.name, len(group.member))) ldapGroups.append(group) finally: continue for ldapGroup in ldapGroups: # loop through the results and make sure we match on the group name searchSeafileGroup = request('admin/search-group/?query={0}'.format(ldapGroup.name), seafileURL, seafileToken)['response']['group_list'] if len(searchSeafileGroup) == 0: # group not in Seafile createSeafileGroup = request('admin/groups/', seafileURL, seafileToken, "POST", {"group_name": "{0}".format(ldapGroup.name), "group_owner": "{0}".format(adminEmail)})['response'] logger.info("Created Seafile group {0}".format(ldapGroup.name)) searchSeafileGroup = request('admin/search-group/?query={0}'.format(ldapGroup.name), seafileURL, seafileToken)['response']['group_list'] elif len(searchSeafileGroup) > 0 and not [item for item in searchSeafileGroup if ldapGroup.name == item['name']]: # group not in Seafile but similar group name is createSeafileGroup = request('admin/groups/', seafileURL, seafileToken, "POST", {"group_name": "{0}".format(ldapGroup.name), "group_owner": "{0}".format(adminEmail)})['response'] logger.info("Created Seafile group {0}".format(ldapGroup.name)) searchSeafileGroup = request('admin/search-group/?query={0}'.format(ldapGroup.name), seafileURL, seafileToken)['response']['group_list'] for seafileGroup in searchSeafileGroup: if seafileGroup['name'] == ldapGroup.name: for ldapGroupMember in ldapGroup.member: logger.debug("Searching for LDAP user {0}".format(ldapGroupMember)) #ldap.search(ldapBase, '(&(distinguishedName={0})(mail=*)(!(userAccountControl:1.2.840.113556.1.4.803:=2))({1}))'.format(ldapGroupMember, ldapFilter), attributes=['mail']) ldap.search(ldapBase, '(&(distinguishedName={0})(mail=*)(!(userAccountControl:1.2.840.113556.1.4.803:=2))({1}))'.format(ldap.filter.filter_format('%s', (ldapGroupMember,)), ldapFilter), attributes=['mail']) count = len(ldap.entries) logger.debug("Found {0} LDAP user.".format(count)) if count == 1: addMembertoSeafileGroup = request('admin/groups/{0}/members/'.format(seafileGroup['id']), seafileURL, seafileToken, "POST", "email={0}".format(ldap.entries[0]['mail']), False)['response'] if len(addMembertoSeafileGroup['failed']) == 1: # user wasn't added to group if addMembertoSeafileGroup['failed'][0]['error_msg'].endswith(" is already a group member."): logger.debug("User {0} is already member of Seafile Group {1}".format(ldap.entries[0]['mail'], seafileGroup['name'])) else: logger.error("User {0} was NOT added to Seafile Group {1}".format(ldap.entries[0]['mail'], seafileGroup['name'])) logger.error("{0}".format(addMembertoSeafileGroup['failed'][0]['error_msg'])) elif len(addMembertoSeafileGroup['success']) == 1: # user was added to group logger.info("User {0} was added to Seafile Group {1}".format(ldap.entries[0]['mail'], seafileGroup['name'])) seafileGroups = request('admin/groups/', seafileURL, seafileToken)['response']['groups'] # loop through seafile groups and remove those groups no longer in AD or empty # also check seafile group membership against ldap group for seafileGroup in seafileGroups: # need to check if group exists in ad first and if not delete it logger.debug("Searching for ldap group {0}.".format(seafileGroup)) ldap.search(ldapBase, '(samaccountname={1})'.format(seafileGroup), attributes=['*']) count = len(ldap.entries) logger.debug("Found {0} LDAP group.".format(count)) seafileGroupMembers = request('admin/groups/{0}/members/'.format(seafileGroup['id']), seafileURL, seafileToken)['response']['members'] #for seafileGroupMember in seafileGroupMembers: #seafileGroupMembers = request('admin/groups/{0}/members/'.format(seafileGroup['id']), seafileURL, seafileToken)['response']['members'] #for ldapGroupMember in ldapGroup.member: # logger.debug("Searching for LDAP user {0}".format(ldapGroupMember)) # ldap.search(ldapBase, '(&(distinguishedName={0})(mail=*)(!(userAccountControl:1.2.840.113556.1.4.803:=2))({1}))'.format(ldapGroupMember, ldapFilter), attributes=['mail']) # count = len(ldap.entries) # logger.debug("Found {0} LDAP user.".format(count)) # if count == 1: # if [item for item in seafileGroupMembers if ldap.entries[0]['mail'] == item['email']]: # logger.debug("User {0} is already member of Seafile Group {1}".format(ldap.entries[0]['mail'], seafileGroup['name'])) # continue # else: # logger.info("User {0} is not a member of Seafile Group {1}".format(ldap.entries[0]['mail'], seafileGroup['name'])) # addMembertoSeafileGroup = request('admin/groups/{0}/members/'.format(seafileGroup['id']), seafileURL, seafileToken, "POST", {"email": "{0}".format(ldap.entries[0]['mail'])})['response'] # else: # logger.debug("User {0} is not enabled, have no email, or not in the Seafile Group".format(ldap.entries[0]['mail'])) #logger.debug("Checking if LDAP group {0} is in SQL Table".format(ldapGroup.name)) #cnx = mysql.connector.connect(**dbconfig) #cursor = cnx.cursor() #query = "SELECT * FROM Group WHERE group_name = '{0}'".format(ldapGroup.name) #logger.debug("Query: {0}".format(query)) #cursor.execute(query) #sqlLDAPgroup = cursor.fetchall() #row_count = cursor.rowcount #logger.debug("Found {0} SQL LDAP Group".format(row_count)) #cnx.close() #if row_count == 1: # group is in seafile #else: # create the seafile group # createSeafileGroup = request('admin/groups/', seafileURL, seafileToken, "POST", {"group_name": "{0}".format(ldapGroup.name), "group_owner": "{0}".format(adminEmail)})['response']