print(conn.entries[0]) conn.modify('cn=b.smith,ou=ldap3-tutorial,dc=demo1,dc=freeipa,dc=org', {'sn': [(MODIFY_DELETE, ['Young'])]}) conn.search('ou=ldap3-tutorial,dc=demo1,dc=freeipa,dc=org', '(cn=b.smith)', attributes=['sn', 'cn']) print(conn.entries[0]) conn.modify('cn=b.smith,ou=ldap3-tutorial,dc=demo1,dc=freeipa,dc=org', {'sn': [(MODIFY_REPLACE, ['Smith'])]}) conn.search('ou=ldap3-tutorial,dc=demo1,dc=freeipa,dc=org', '(cn=b.smith)', attributes=['sn', 'cn']) print(conn.entries[0]) print( conn.compare('cn=b.smith,ou=ldap3-tutorial,dc=demo1,dc=freeipa,dc=org', 'departmentNumber', 'DEV')) print( conn.compare( 'cn=b.smith,ou=moved,ou=ldap3-tutorial,dc=demo1,dc=freeipa,dc=org', 'departmentNumber', 'QA')) conn.modify( 'cn=b.smith,ou=ldap3-tutorial,dc=demo1,dc=freeipa,dc=org', { 'sn': [(MODIFY_DELETE, ['Johnson'])], 'givenname': [(MODIFY_REPLACE, ['Beatrix'])] }) conn.modify_dn('cn=b.smith,ou=ldap3-tutorial,dc=demo1,dc=freeipa,dc=org', 'cn=b.young') print( conn.add( 'cn=m.johnson,ou=ldap3-tutorial,dc=demo1,dc=freeipa,dc=org',
class AD(object): '''AD域的操作 ''' def __init__(self): '''初始化加载日志配置 AD域连接 AD基础信息加载 ''' # 初始化加载日志配置 self.__setup_logging(path=LOG_CONF) SERVER = Server( host=LDAP_IP, port=636, # 636安全端口 use_ssl=True, get_info=ALL, connect_timeout=5) # 连接超时为5秒 try: self.conn = Connection( server=SERVER, user=USER, password=PASSWORD, auto_bind=True, read_only=False, # 禁止修改数据True receive_timeout=10) # 10秒内没返回消息则触发超时异常 logging.info("distinguishedName:%s res: %s" % (USER, self.conn.bind())) except BaseException as e: logging.error("AD域连接失败,请检查IP/账户/密码") finally: self.conn.closed def __setup_logging(self, path=LOG_CONF, default_level=logging.INFO, env_key="LOG_CFG"): value = os.getenv(env_key, None) if value: path = value if os.path.exists(path): with open(path, "r") as f: config = yaml.safe_load(f) logging.config.dictConfig(config) else: logging.basicConfig(level=default_level) def get_users(self, attr=ALL_ATTRIBUTES): ''' @param {type} @return: total_entries 此次查询到的记录数目 @msg: 获取所有用户 ''' entry_list = self.conn.extend.standard.paged_search( search_base=ENABLED_BASE_DN, search_filter=USER_SEARCH_FILTER, search_scope=SUBTREE, attributes=attr, paged_size=5, generator=False) # 关闭生成器,结果为列表 total_entries = 0 for entry in entry_list: total_entries += 1 logging.info("共查询到记录条目: " + str(total_entries)) return entry_list def get_ous(self, attr=None): ''' @param {type} @return: res所有OU @msg: 获取所有OU ''' self.conn.search(search_base=ENABLED_BASE_DN, search_filter=OU_SEARCH_FILTER, attributes=attr) result = self.conn.response_to_json() res_list = json.loads(result)['entries'] return res_list[::-1] def get_level_users(self, SEARCH_BASE, attr=ALL_ATTRIBUTES): ''' @param {type} @return: total_entries 此次查询到的记录数目 @msg: 获取某OU下用户 ''' entry_list = self.conn.extend.standard.paged_search( search_base=SEARCH_BASE, search_filter=USER_SEARCH_FILTER, search_scope=LEVEL, attributes=attr, paged_size=5, generator=False) # 关闭生成器,结果为列表 total_entries = 0 for entry in entry_list: total_entries += 1 logging.info("共查询到记录条目: " + str(total_entries)) return entry_list def handle_excel(self, path): ''' @param path{string} excel文件绝对路径 @return: result: { 'page_flag': True, 'person_list': [[], [], ...] } @msg: 表格文件预处理 1.增加行列数判————行数决定AD的查询是否分页,列数用以判断必须列数据完整性与补充列; 2.判断必须列【工号|姓名|部门】是否存在且是否有空值 3.人员列表的使用sort函数排序key用lambda函数,排序条件(i[2].count('.'), i[2], i[0])为(部门层级、部门名称、工号) ''' try: # 1.开始源文件格式扫描 df = pd.read_excel(path) # 读取源文件 a, b = df.shape # 表格行列数 cols = df.columns.tolist() # 表格列名列表 is_ex_null = df.isnull().any().tolist() # 列是否存在空值 dic = dict(zip(cols, is_ex_null)) # 存在空值的列 if int("工号" in cols) + int("姓名" in cols) + int( "部门" in cols) < 3: # 判断必须列是否都存在 logging.error( "表格缺少必要列【工号|姓名|部门】请选择正确的源文件;或者将相应列列名修改为【工号|姓名|部门】") exit() elif int(dic["工号"]) + int(dic["姓名"]) + int( dic["部门"]) > 0: # 判断必须列是否有空值 logging.error("必要列存在空值记录,请检查补全后重试:" + '\n' + str(df[df.isnull().values == True])) else: df = pd.read_excel(path, usecols=[i for i in range(0, b)]) use_cols = ["工号", "姓名", "部门"] # 使用的必须列 for c in ["邮件", "电话", "岗位"]: # 扩展列的列名在这里添加即可 if c in cols: use_cols.append(c) df = df[use_cols] # 调整df使用列顺序 person_list = df.values.tolist() # df数据框转list person_list.sort(key=lambda i: (i[2].count('.'), i[2], i[0]), reverse=False) # 多条件排序 # 2.开始处理列表 for i, row in enumerate(person_list): job_id, name, depart = row[0:3] # 将部门列替换成DN row[2] = 'CN=' + str( name + str(job_id)) + ',' + 'OU=' + ',OU='.join( row[2].split('.')[::-1]) + ',' + ENABLED_BASE_DN row.append(CUSTOM_SAMA + str(job_id).zfill(6) ) # 增加登录名列,对应AD域user的 sAMAccountname 属性 row.append(name + str(job_id)) # 增加CN列,对应user的 cn 属性 # 3.开始处理返回字典 result_dic = dict() # 返回字典 if a > 1000: result_dic['page_flag'] = True else: result_dic['page_flag'] = False result_dic['person_list'] = person_list return result_dic except Exception as e: logging.error(e) return None def generate_pwd(self, count): ''' @param count{int} 所需密码长度 @return: pwd: 生成的随机密码 @msg: 生成随机密码,必有数字、大小写、特殊字符且数目伪均等; ''' pwd_list = [] a, b = count // 4, count % 4 # 四种类别先均分除数个字符 pwd_list.extend(random.sample(string.digits, a)) pwd_list.extend(random.sample(string.ascii_lowercase, a)) pwd_list.extend(random.sample(string.ascii_uppercase, a)) pwd_list.extend(random.sample('!@#$%^&*()', a)) # 从四种类别中再取余数个字符 pwd_list.extend( random.sample( string.digits + string.ascii_lowercase + string.ascii_uppercase + '!@#$%^&*()', b)) random.shuffle(pwd_list) pwd_str = ''.join(pwd_list) return pwd_str def write2txt(self, path, content): ''' @param path{string} 写入文件路径;content{string} 每行写入内容 @return: @msg: 每行写入文件 ''' try: if os.path.exists(path): with open(path, mode='a', encoding='utf-8') as file: file.write(content + '\n') else: with open(path, mode='a', encoding='utf-8') as file: file.write(content + '\n') return True except Exception as e: logging.error(e) return False def del_ou_right(self, flag): ''' @param cmd_l{list} 待执行的powershell命令列表 @return: True/False @msg: 连接远程windows并批量执行powershell命令 ''' # powershell命令 用于启用/关闭OU 防止对象被意外删除 属性 # 防止对象被意外删除× enable_del = [ "Import-Module ActiveDirectory", "Get-ADOrganizationalUnit -filter * -Properties ProtectedFromAccidentalDeletion | where {" "$_.ProtectedFromAccidentalDeletion -eq $true} |Set-ADOrganizationalUnit " "-ProtectedFromAccidentalDeletion $false" ] # 防止对象被意外删除√ disable_del = [ "Import-Module ActiveDirectory", "Get-ADOrganizationalUnit -filter * -Properties ProtectedFromAccidentalDeletion | where {" "$_.ProtectedFromAccidentalDeletion -eq $false} |Set-ADOrganizationalUnit " "-ProtectedFromAccidentalDeletion $true" ] flag_map = {0: enable_del, 1: disable_del} try: win = winrm.Session('http://' + LDAP_IP + ':5985/wsman', auth=(WINRM_USER, WINRM_PWD)) for cmd in flag_map[flag]: ret = win.run_ps(cmd) if ret.status_code == 0: # 调用成功 减少日志写入 # if flag == 0: # logging.info("防止对象被意外删除×") # elif flag == 1: # logging.info("防止对象被意外删除√") return True else: return False except Exception as e: logging.error(e) return False def create_obj(self, dn=None, type='user', info=None): ''' @param dn{string}, type{string}'user'/'ou' @return: res新建结果, self.conn.result修改结果 @msg:新增对象 ''' object_class = { 'user': ['user', 'posixGroup', 'top'], 'ou': ['organizationalUnit', 'posixGroup', 'top'], } if info is not None: [job_id, name, dn, email, tel, title, sam, cn] = info user_attr = { 'sAMAccountname': sam, # 登录名 'userAccountControl': 544, # 启用账户 'title': title, # 头衔 'givenName': name[0:1], # 姓 'sn': name[1:], # 名 'displayname': name, # 姓名 'mail': email, # 邮箱 'telephoneNumber': tel, # 电话号 } else: user_attr = None # 创建之前需要对dn中的OU部分进行判断,如果没有需要创建 dn_base = dn.split(',', 1)[1] check_ou_res = self.check_ou(dn_base) if not check_ou_res: logging.error('check_ou失败,未知原因!') return False else: self.conn.add(dn=dn, object_class=object_class[type], attributes=user_attr) add_result = self.conn.result if add_result['result'] == 0: logging.info('新增对象【' + dn + '】成功!') if type == 'user': # 若是新增用户对象,则需要一些初始化操作 self.conn.modify( dn, {'userAccountControl': [('MODIFY_REPLACE', 512)]} ) # 激活用户 # 如果是用户时 new_pwd = self.generate_pwd(8) old_pwd = '' self.conn.extend.microsoft.modify_password( dn, new_pwd, old_pwd) # 初始化密码 info = 'SAM: ' + sam + ' PWD: ' + new_pwd + ' DN: ' + dn save_res = self.write2txt(PWD_PATH, info) # 将账户密码写入文件中 if save_res: logging.info('保存初始化账号密码成功!') else: logging.error('保存初始化账号密码失败: ' + info) self.conn.modify(dn, {'pwdLastSet': (2, [0])}) # 设置第一次登录必须修改密码 elif add_result['result'] == 68: logging.error('entryAlreadyExists 用户已经存在') elif add_result['result'] == 32: logging.error('noSuchObject 对象不存在ou错误') else: logging.error('新增对象: ' + dn + ' 失败!其他未知错误') return add_result def del_obj(self, dn, type): ''' @param dn{string} @return: res修改结果 @msg: 删除对象 ''' if type == 'ou': self.del_ou_right(flag=0) res = self.conn.delete(dn=dn) self.del_ou_right(flag=1) else: res = self.conn.delete(dn=dn) if res == True: logging.info('删除对象' + dn + '成功!') return res else: return False def update_obj(self, old_dn, info=None): ''' @param {type} @return: @msg: 更新对象 ''' if info is not None: [job_id, name, dn, email, tel, title, sam, cn] = info # 组成更新属性之前需要对dn中的OU部分进行判断,如果没有需要创建 dn_base = dn.split(',', 1)[1] check_ou_res = self.check_ou(dn_base) if not check_ou_res: logging.error('check_ou失败,未知原因!') return False else: attr = { 'distinguishedName': dn, # dn 'sAMAccountname': sam, # 登录名 'title': title, # 头衔 'givenName': name[0:1], # 姓 'sn': name[1:], # 名 'displayname': name, # 姓名 'mail': email, # 邮箱 'telephoneNumber': tel, # 电话号 } else: attr = None changes_dic = {} for k, v in attr.items(): if not self.conn.compare(dn=old_dn, attribute=k, value=v): # 待修改属性 if k == "distinguishedName": # 若属性有distinguishedName则需要移动user或ou # 若dn修改了需要将密码文件这个人的dn信息更新下 self.update_pwd_file_line(old_dn=old_dn, new_dn=dn) self.move_obj(dn=old_dn, new_dn=v) changes_dic.update({k: [(MODIFY_REPLACE, [v])]}) if len(changes_dic) != 0: # 有修改的属性时 modify_res = self.conn.modify(dn=dn, changes=changes_dic) logging.info('更新对象: ' + dn + ' 更新内容: ' + str(changes_dic)) return self.conn.result def rename_obj(self, dn, newname): ''' @param newname{type}新的名字,User格式:"cn=新名字";OU格式:"OU=新名字" @return: 修改结果 @msg: 重命名对象 ''' res = self.conn.modify_dn(dn, newname) if res == True: return True else: return False def move_obj(self, dn, new_dn): ''' @param {type} @return: @msg: 移动对象到新OU ''' relative_dn, superou = new_dn.split(",", 1) res = self.conn.modify_dn(dn=dn, relative_dn=relative_dn, new_superior=superou) if res == True: return True else: return False def compare_attr(self, dn, attr, value): ''' @param {type} @return: @msg:比较员工指定的某个属性 ''' res = self.conn.compare(dn=dn, attribute=attr, value=value) return res def check_ou(self, ou, ou_list=None): ''' @param {type} @return: @msg: 递归函数 如何判断OU是修改了名字而不是新建的:当一个OU里面没有人就判断此OU被修改了名字,删除此OU; 不管是新建还是修改了名字,都会将人员转移到新的OU下面:需要新建OU则创建OU后再添加/转移人员 check_ou的作用是为人员的变动准备好OU ''' if ou_list is None: ou_list = [] self.conn.search(ou, OU_SEARCH_FILTER) # 判断OU存在性 while self.conn.result['result'] == 0: if ou_list: for ou in ou_list[::-1]: self.conn.add(ou, 'organizationalUnit') return True else: ou_list.append(ou) ou = ",".join(ou.split(",")[1:]) self.check_ou(ou, ou_list) # 递归判断 return True def scan_ou(self): '''扫描的时候,必须保证此OU为叶子节点,否则报notAllowedOnNonLeaf错误, 例如此次空OU——OU=开发部,OU=核心技术部,OU=RAN,OU=上海总部,DC=randolph,DC=com 的倒数第一、二层都是空OU,但是必须得先删除倒数第一层 因此在获取所有OU列表的位置get_ous就将获得的结果倒叙(用切片[::-1]) ''' res = self.get_ous(attr=['distinguishedName']) # 调用ps脚本,防止对象被意外删除× modify_right_res = self.del_ou_right(flag=0) for i, ou in enumerate(res): dn = ou['attributes']['distinguishedName'] # 判断dd下面是否有用户,没有用户的直接删除 self.conn.search(search_base=dn, search_filter=USER_SEARCH_FILTER) if not self.conn.entries: # 没有用户存在的空OU,可以进行清理 try: delete_res = self.conn.delete(dn=dn) if delete_res: logging.info('删除空的OU: ' + dn + ' 成功!') else: logging.error('删除操作处理结果' + str(self.conn.result)) except Exception as e: logging.error(e) else: logging.info("没有空OU,OU扫描完成!") # 防止对象被意外删除√ self.del_ou_right(flag=1) def disable_users(self, path): ''' @param {type} @return: @msg: 将AD域内的用户不在csv表格中的定义为离职员工 ''' result = ad.handle_excel(path) newest_list = [] # 全量员工列表 for person in result['person_list']: job_id, name, dn, email, tel, title, sam, cn = person[0:8] dd = str(dn).split(',', 1)[1] newest_list.append(name) # 查询AD域现有员工 res = self.get_users(attr=[ 'distinguishedName', 'name', 'cn', 'displayName', 'userAccountControl' ]) for i, ou in enumerate(res): ad_user_distinguishedName, ad_user_displayName, ad_user_cn, ad_user_userAccountControl = ou[ 'attributes']['distinguishedName'], ou['attributes'][ 'displayName'], ou['attributes']['cn'], ou['attributes'][ 'userAccountControl'] rela_dn = "cn=" + str(ad_user_cn) # 判断用户不在最新的员工表格中 或者 AD域中某用户为禁用用户 if ad_user_displayName not in newest_list or ad_user_userAccountControl in DISABLED_USER_FLAG: try: # 禁用用户 self.conn.modify( dn=ad_user_distinguishedName, changes={'userAccountControl': (2, [546])}) logging.info("在AD域中发现不在表格中用户,禁用用户:" + ad_user_distinguishedName) # 移动到离职组 判断OU存在性 self.conn.search(DISABLED_BASE_DN, OU_SEARCH_FILTER) # 判断OU存在性 if self.conn.entries == []: # 搜不到离职员工OU则需要创建此OU self.create_obj(dn=DISABLED_BASE_DN, type='ou') # 移动到离职组 self.conn.modify_dn(dn=ad_user_distinguishedName, relative_dn=rela_dn, new_superior=DISABLED_BASE_DN) logging.info('将禁用用户【' + ad_user_distinguishedName + '】转移到【' + DISABLED_BASE_DN + '】') except Exception as e: logging.error(e) def create_user_by_excel(self, path): ''' @param path{string} 用于新增用户的表格 @return: @msg: ''' res_dic = self.handle_excel(path) for person in res_dic['person_list']: user_info = person self.create_obj(info=user_info) def ad_update(self, path): '''AD域的初始化/更新——从表格文件元数据更新AD域: 判断用户是否在AD域中——不在则新增; 在则判断该用户各属性是否与表格中相同,有不同则修改; 完全相同的用户不用作处理; ''' # 准备表格文件 result = ad.handle_excel(path) ori_data = result['person_list'] try: self.del_ou_right(flag=0) # 防止对象被意外删除× with tqdm(iterable=ori_data, ncols=100, total=len(ori_data), desc='处理进度', unit='人') as tqdm_ori_data: # 封装进度条 for person in tqdm_ori_data: dn, cn = person[2], person[7] user_info = person dd = str(dn).split(',', 1)[1] # 根据cn判断用户是否已经存在 filter_phrase_by_cn = "(&(objectclass=person)(cn=" + cn + "))" search_by_cn = self.conn.search( search_base=ENABLED_BASE_DN, search_filter=filter_phrase_by_cn, attributes=['distinguishedName']) search_by_cn_json_list = json.loads( self.conn.response_to_json())['entries'] search_by_cn_res = self.conn.result if search_by_cn == False: # 根据cn搜索失败,查无此人则新增 self.create_obj(info=user_info) else: old_dn = search_by_cn_json_list[0][ 'dn'] # 部门改变的用户的现有部门,从表格拼接出来的是新的dn在user_info中带过去修改 self.update_obj(old_dn=old_dn, info=user_info) # break # 可测试一个例子 self.del_ou_right(flag=1) # 防止对象被意外删除√ except KeyboardInterrupt: tqdm_ori_data.close() raise tqdm_ori_data.close() def handle_pwd_expire(self, attr=None): ''' @param {type} @return: @msg: 处理密码过期 设置密码不过期 需要补全理论和测试 参考理论地址: https://stackoverflow.com/questions/18615958/ldap-pwdlastset-unable-to-change-without-error-showing ''' attr = ['pwdLastSet'] self.conn.search(search_base=ENABLED_BASE_DN, search_filter=USER_SEARCH_FILTER, attributes=attr) result = self.conn.response_to_json() res_list = json.loads(result)['entries'] for l in res_list: pwdLastSet, dn = l['attributes']['pwdLastSet'], l['dn'] modify_res = self.conn.modify(dn, {'pwdLastSet': (2, [-1])}) # pwdLastSet只能给-1 或 0 if modify_res: logging.info('密码不过期-修改用户: ' + dn) def update_pwd_file_line(self, old_dn=None, new_dn=None, new_pwd=None): ''' @param dn{string} @return: 修改结果 @msg: 当用户的dn或密码被程序更新,将会在这里更新对应部分的信息 采用临时文件替换源文件的方式,节省内存,但占硬盘 参考文章: https://www.cnblogs.com/wuzhengzheng/p/9692368.html ''' with open(PWD_PATH, mode='rt', encoding='utf-8') as file, \ open('TEMP.txt', mode='wt', encoding='utf-8') as temp_file: for line in file: if old_dn and new_dn: # dn被修改 if old_dn in line: line = line.replace(old_dn, new_dn) temp_file.write(line) else: temp_file.write(line) elif new_pwd and old_dn: # 密码被修改 if old_dn in line: # 需要正则匹配旧的密码 pattern = "PWD: (.+?)\\n" # 惰性匹配 local = re.findall(pattern, line) old_pwd = local[0] line = line.replace(old_pwd, new_pwd) temp_file.write(line) else: temp_file.write(line) os.remove(PWD_PATH) os.rename('TEMP.txt', PWD_PATH) def modify_pwd(self, cn): ''' @param cn{string} 姓名工号 戴东1325 @return: 修改结果 @msg: 修改密码 ''' # 根据cn判断用户是否已经存在 filter_phrase_by_cn = "(&(objectclass=person)(cn=" + cn + "))" search_by_cn = self.conn.search(search_base=ENABLED_BASE_DN, search_filter=filter_phrase_by_cn, attributes=['distinguishedName']) search_by_cn_json_list = json.loads( self.conn.response_to_json())['entries'] if search_by_cn: new_pwd = self.generate_pwd(8) old_pwd = '' dn = search_by_cn_json_list[0]['dn'] modify_password_res = self.conn.extend.microsoft.modify_password( dn, new_pwd, old_pwd) if modify_password_res: logging.info('更新了对象: ' + dn + ' 的密码') is_exist = os.path.exists(PWD_PATH) if not is_exist: # 校验密码文件存在性 info = 'DN: ' + dn + ' PWD: ' + new_pwd save_res = self.write2txt(PWD_PATH, info) # 将账户密码写入文件中 if save_res: logging.info('保存初始化账号密码成功!') else: logging.error('保存初始化账号密码失败: ' + info) else: # 若密码修改了需要将密码文件这个人的密码信息更新下 with open(PWD_PATH, mode='rt', encoding='utf-8') as file: if dn in file.read(): is_exist_pwd_record = True else: is_exist_pwd_record = False if is_exist_pwd_record: # 若发现此人信息在密码文件里则更新,否则需创建 self.update_pwd_file_line(old_dn=dn, new_pwd=new_pwd) else: info = 'DN: ' + dn + ' PWD: ' + new_pwd # 因为是修改密码,所以dn未修改 self.write2txt(PWD_PATH, info) else: logging.error('更新对象密码失败!: ' + dn) else: logging.error('查无此人!请检查待修改密码对象格式是否为【姓名工号】')
class OptLdap(): """ AD中的用户与组织单位操作 """ def __init__(self): """ 连接初始化 """ self.connect = Connection( # 配置服务器连接参数 server=AD_SERVER_POOL, auto_bind=True, authentication=NTLM, # 连接Windows AD 使用NTLM方式认证 user=SERVER_USER, password=SERVER_PASSWORD, ) ldap_logger.info("连接AD域服务器 %s", self.connect) self.leaved_base_dn = 'OU=LEAVED,DC=sh,DC=hupu,DC=com' # 离职账户的OU self.active_base_dn = 'OU=HUPU,DC=sh,DC=hupu,DC=com' # 在职账户的OU self.all_base_dn = 'DC=sh,DC=hupu,DC=com' # 所有用户的OU self.user_search_filter = '(objectclass=user)' # 只获取用户对象 self.ou_search_filter = '(objectclass=organizationalUnit)' # 只获取OU对象 self.attributes_ou = ['Name', 'ObjectGUID'] self.attributes_user = ['name', 'memberOf', 'sAMAccountName', 'badPwdCount', 'displayName', 'mail', 'userAccountControl', 'userPrincipalName', 'telephoneNumber'] def get_users(self, get_type='active'): """ 获取用户信息 """ if get_type == 'all': self.connect.search(search_base=self.all_base_dn, search_filter=self.user_search_filter, attributes=self.attributes_user) elif get_type == 'leaved': self.connect.search(search_base=self.leaved_base_dn, search_filter=self.user_search_filter, attributes=self.attributes_user) else: self.connect.search(search_base=self.active_base_dn, search_filter=self.user_search_filter, attributes=self.attributes_user) res = json.loads(self.connect.response_to_json())['entries'] ldap_logger.info("获取所有用户信息 %s - %s", get_type, self.connect.result) return res def get_obj_info(self, filter_key=None, filter_value=None, filter_all=None, attr=None): """ 根据自定义filter获取用户信息 """ if filter_all: search_filter = filter_all else: search_filter = "(" + filter_key + "=" + filter_value + ")" res = [] attr = attr if attr else ALL_ATTRIBUTES try: self.connect.search(search_base=self.all_base_dn, search_filter=search_filter, attributes=attr) res = json.loads(self.connect.response_to_json())['entries'] except exceptions.LDAPException as ept: ldap_logger.error("获取自定义用户信息失败 %s", ept) raise # finally: # self.connect.unbind() ldap_logger.info("获取自定义用户信息 %s - %s", search_filter, self.connect.result) return res def get_ous(self): """ 获取OU信息 """ self.connect.search(search_base=self.active_base_dn, search_filter=self.ou_search_filter, attributes=self.attributes_ou) res = json.loads(self.connect.response_to_json())['entries'] ldap_logger.info("获取所有OU信息 %s - %s", self.active_base_dn, self.connect.result) return res def del_obj(self, dn): # pylint: disable=invalid-name """ 删除用户 or 部门 :param dn: 'CN=张三,OU=IT组,OU=企业信息化部,OU=虎扑,DC=sh,DC=hupu,DC=com' or 'OU=IT组,OU=企业信息化部,OU=虎扑,DC=sh,DC=hupu,DC=com' :return True/False """ res = self.connect.delete(dn=dn) ldap_logger.info("用户信息 %s - %s", dn, res) return res, self.connect.result def create_obj(self, dn, obj_type, pwd="Abcd.1234", attr=None): # pylint: disable=invalid-name """ 新增用户 or OU :param DN: ou - "OU=IT组,OU=企业信息化部,OU=虎扑,DC=sh,DC=hupu,DC=com" or user - "CN=张三,OU=IT组,OU=企业信息化部,OU=虎扑,DC=sh,DC=hupu,DC=com" :param obj_type: user or ou :param attr: user - {"sAMAccountName": "zhangsan", "Sn": "张", "name":"张三", "UserPrincipalName": "*****@*****.**", "Mail": "*****@*****.**", "Displayname": "张三"} ou - {"name": "IT组"} :return : {"status": True/False, "msg": {}} """ res = False ldap_logger.info("创建AD域Object %s - %s_attr:%s", dn, obj_type, attr) object_class = {'user': ['top', 'person', 'organizationalPerson', 'user'], 'ou': ['top', 'organizationalUnit']} try: res = self.connect.add(dn=dn, object_class=object_class[obj_type], attributes=attr) ldap_logger.info("创建Object结果 %s - %s - %s", dn, self.connect.result, res) msg = self.connect.result if obj_type == 'user': # 如果是用户,需要设置密码、激活账号 self.connect.extend.microsoft.modify_password(dn, pwd) # 设置密码 self.connect.modify(dn, {'userAccountControl': [(MODIFY_REPLACE, 512)]}) # 设置账号状态,激活用户 except exceptions.LDAPException as ept: ldap_logger.error("Object信息 %s - %s - %s", dn, self.connect, ept) msg = "Ldap新增Object操作失败,详细信息查看Log" # finally: # self.connect.unbind() return res, msg def update_obj(self, dn, attr=None): # pylint: disable=invalid-name """ 更新user or OU 只允许OU更新name,user不能更新 ["name","sAMAccountName", "userPrincipalName", "displayname"] OU or USER都可以移动 :param dn: 需要修改的完整DN :param attr: 需要更新的属性值,字典形式 :return {"status: True/False, "msg": {'result': 0, 'description': 'success', 'dn': '', 'message': '', 'referrals': None, 'type': 'modDNResponse'}} """ changes_dic = {} ldap_logger.info("更新Object的信息 %s - %s", dn, attr) for k, v in attr.items(): # pylint: disable=invalid-name if not self.compare_attr(dn=dn, attr=k, value=v): ldap_logger.info("对比Object信息结果 %s - %s:%s - %s", dn, k, v, self.connect.result) if k == "name": # 修改name值只允许OU修改,不允许修改CN的name res = self._rename_obj(dn=dn, newname=attr['name']) if res['description'] == 'success': if dn[:2] == "OU": dn = "OU=%s,%s" % (attr["name"], dn.split(",", 1)[1]) else: return {"status": False, "msg": "不支持的DN " + dn} elif k == "DistinguishedName": res = self._move_object(dn=dn, new_dn=v) # 调用移动User or OU 的方法 if res['description'] == 'success': if dn[:2] == "CN": dn = "%s" % (attr["DistinguishedName"]) if dn[:2] == "OU": dn = "%s" % (attr["DistinguishedName"]) elif k in ["sAMAccountName", "userPrincipalName", "displayname"]: return {"status": False, "msg": "不支持的属性 " + k} else: changes_dic.update({k:[(MODIFY_REPLACE, [v])]}) self.connect.modify(dn=dn, changes=changes_dic) return {"status": True, "msg": self.connect.result} def _rename_obj(self, dn, newname): # pylint: disable=invalid-name """ OU or User 重命名方法 :param dn:需要修改的object的完整dn路径 :param newname: 新的名字 :return:返回中有:'description': 'success', 表示操作成功 {'result': 0, 'description': 'success', 'dn': '', 'message': '', 'referrals': None, 'type': 'modDNResponse'} """ cn_ou = dn.split("=", 1) newname = cn_ou[0] + "=" + newname res = self.connect.modify_dn(dn, newname) ldap_logger.info("Remove-Object-Info %s - %s - %s", dn, self.connect.result, res) return self.connect.result def _move_object(self, dn, new_dn): # pylint: disable=invalid-name """移动员工 or 部门到新部门""" relative_dn, superou = new_dn.split(",", 1) res = self.connect.modify_dn(dn=dn, relative_dn=relative_dn, new_superior=superou) ldap_logger.info("Move-Object-Info %s - %s - %s", dn, self.connect.result, res) return self.connect.result def leaved_user(self, dn): # pylint: disable=invalid-name """ 处理离职的用户,离职用户放置在离职OU里,账号禁用 """ res = self.connect.modify(dn, {'userAccountControl': [(MODIFY_REPLACE, 514)]}) # 设置账号状态,禁用账号 ldap_logger.info("Leaved-User %s - Disable - %s", dn, res) if res: new_dn = dn.split(",")[0] + "," + self.leaved_base_dn res = self._move_object(dn=dn, new_dn=new_dn) return res def compare_attr(self, dn, attr, value): # pylint: disable=invalid-name """比较员工指定的某个属性 """ try: res = self.connect.compare(dn=dn, attribute=attr, value=value) ldap_logger.info("Commpare-Object-Info %s - %s:%s - %s", dn, attr, value, res) except exceptions.LDAPException as ept: ldap_logger.error("Commpare-Object-Info-Expception %s - %s - %s", dn, self.connect, ept) res = False return res def reset_password(self, dn, new_pwd): # pylint: disable=invalid-name """ 重置密码, 不需要原密码 """ res = self.connect.extend.microsoft.modify_password(dn, new_pwd) ldap_logger.info("Reset-Password %s - %s", dn, self.connect.result) return res, self.connect.result
class LDAPLib: def __init__(self): self.connection = Connection( server=settings.LDAP_SERVER, user=settings.LDAP_USER, password=settings.LDAP_PASSWORD, auto_bind=True, ) self.user_base = ",".join(("ou=users", settings.LDAP_BASE_DN)) self.group_base = ",".join(("ou=groups", settings.LDAP_BASE_DN)) self.user_attributes = ["uid", "cn", "sn"] self.group_attributes = ["gidNumber", "cn"] def get_all_users(self): search_filter = "(cn=*)" result = self.connection.search(self.user_base, search_filter, attributes=self.user_attributes) if result: return self.connection.entries return [] def get_all_groups(self): search_filter = "(cn=*)" result = self.connection.search(self.group_base, search_filter, attributes=self.group_attributes) if result: return self.connection.entries return [] def search_user(self, query): search_filter = f"(uid={query})" result = self.connection.search(self.user_base, search_filter, attributes=self.user_attributes) if result: return self.connection.entries[0] def add_user(self, uid, first_name, last_name, email, password_hash): dn = ",".join((f"uid={uid}", self.user_base)) return self.connection.add( dn, ["inetOrgPerson", "top"], { "uid": uid, "cn": first_name, "sn": last_name, "mail": email, "userPassword": "******" + password_hash, }, ) def search_group(self, query): search_filter = f"(gidNumber={query})" result = self.connection.search(self.group_base, search_filter, attributes=self.group_attributes) if result: return self.connection.entries[0] def add_group(self, gid, name): dn = ",".join((f"cn={name}", self.group_base)) return self.connection.add(dn, ["posixGroup", "top"], { "gidNumber": gid, "cn": name }) def update_organization_unit(self, name): """ Make sure the organization unit exists in LDAP. """ search_filter = f"(ou={name})" result = self.connection.search(settings.LDAP_BASE_DN, search_filter, attributes=["ou"]) if not result: dn = ",".join((f"ou={name}", settings.LDAP_BASE_DN)) self.connection.add(dn, ["organizationalUnit", "top"], {"ou": name}) def delete_user(self, uid): dn = ",".join((f"uid={uid}", self.user_base)) return self.connection.delete(dn) def delete_group(self, cn): dn = ",".join((f"cn={cn}", self.group_base)) return self.connection.delete(dn) def check_password(self, uid, password_hash): dn = ",".join((f"uid={uid}", self.user_base)) return self.connection.compare(dn, "userPassword", "{CRYPT}" + password_hash) def change_password(self, uid, password_hash): dn = ",".join((f"uid={uid}", self.user_base)) changes = { "userPassword": [(MODIFY_REPLACE, ["{CRYPT}" + password_hash])] } self.connection.modify(dn, changes) def update_group_members(self, cn, members): dn = ",".join((f"cn={cn}", self.group_base)) if members: changes = {"memberUid": [(MODIFY_REPLACE, members)]} else: changes = {"memberUid": [(MODIFY_DELETE, [])]} self.connection.modify(dn, changes)
}) #print(conn.result) #print() #modify a UniPerson's attributes mail and userPassword conn.modify( 'cn=' + name + ',ou=people,dc=ldap,dc=secuis,dc=fun', { 'mail': [(MODIFY_REPLACE, [email])], 'userPassword': [(MODIFY_REPLACE, ['111111'])] }) #print(conn.result) #print() #compare Uniperson's attributes userPassword pwd = '123456' check = conn.compare('cn=' + name + ',ou=people,dc=ldap,dc=secuis,dc=fun', 'userPassword', pwd) #print ('compare with ', pwd) #print(check) #print() pwd = '111111' check = conn.compare('cn=' + name + ',ou=people,dc=ldap,dc=secuis,dc=fun', 'userPassword', pwd) #print ('compare with ', pwd) #print(check) #print() #look for all Uniperson and print them before deleting conn.search('ou=people,dc=ldap,dc=secuis,dc=fun', '(objectClass=UniPerson)', attributes=['*']) #print(conn.entries)
class AD(object): ''' AD用户操作 ''' def __init__(self, server, username, password): '''初始化''' self.conn = Connection( # 配置服务器连接参数 server=server, auto_bind=True, read_only=False, # 禁止修改数据:True user=username, # 管理员账户 password=password, ) self.active_base_dn = 'ou=people,dc=micl,dc=org' # 账户所在OU # self.search_filter = '(objectclass=user)' # 只获取【用户】对象 self.search_filter = '(objectclass=*)' # 只获取【用户】对象 self.user_search_filter = '(&(objectclass=*)(mail={}))' # 只获取【用户】对象 self.ou_search_filter = '(objectclass=organizationalUnit)' # 只获取【OU】对象 def password_generator(self): s = "abcdefghijklmnopqrstuvwxyz01234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#\ $%^&*()?" passlen = 8 p = "".join(random.sample(s, passlen)) return p def get_user_list_json(self): '''获取所有的用户 JSON''' self.conn.search(search_base=self.active_base_dn, search_filter=self.search_filter, attributes=ALL_ATTRIBUTES) res = self.conn.response_to_json() res = json.loads(res)['entries'] return res[1:] def get_user_list(self): '''获取所有的用户''' self.conn.search(search_base=self.active_base_dn, search_filter=self.search_filter, attributes=ALL_ATTRIBUTES) return self.conn.entries[1:] def get_username_list(self): users = self.get_user_list() usernames = [x.uid.value for x in users if x] return usernames def get_email_list(self): users = self.get_user_list() emails = [x.mail.value for x in users if x] return emails def generate_uid(self): users = self.get_user_list_json() uid = [] for user in users: uid.append(user['attributes']['uidNumber']) uid.sort() return uid[-1] + 1 def username_exists(self, username): usernames = self.get_username_list() if username in usernames: return True else: return False def email_exists(self, email): emails = self.get_email_list() if email in emails: return True else: return False def get_user(self, email): '''获取指定的用户''' search_base = 'cn={},{}'.format(email, self.active_base_dn) # search_filter = self.user_search_filter.format(email) result = self.conn.search(search_base=search_base, search_filter=self.search_filter, attributes=ALL_ATTRIBUTES) if result: return self.conn.entries[0] else: return result def modify_user_password(self, email): user = self.get_user(email) if not user: return False, EMAIL_DOES_NOT_EXISTS user_dn = user.entry_dn password = self.password_generator() username = user.uid.value first_name = user.givenName.value last_name = user.sn.value name = '{} {}'.format(first_name, last_name) hashed_password = hashed(HASHED_SALTED_SHA, password) changes = {'userPassword': [(MODIFY_REPLACE, [hashed_password])]} res = self.conn.modify(user_dn, changes=changes) r = send_email(username, password, name, email) if not r.success: return r.success, RESET_PASSWORD_FAIL if not res: return res, RESET_PASSWORD_FAIL else: return res, RESET_PASSWORD def create_user(self, username, email, first_name, last_name, mobile, attr=None): if self.username_exists(username): return False, USERNAME_EXISTS if self.email_exists(email): return False, EMAIL_EXISTS password = self.password_generator() hashed_password = hashed(HASHED_SALTED_SHA, password) dn = 'cn={},{}'.format(email, self.active_base_dn) object_class = [ 'person', 'posixAccount', 'top', 'inetOrgPerson', 'organizationalPerson' ] attr = { 'mail': [email], # 'sn': [last_name], 'sn': [last_name], 'givenName': [first_name], 'gidNumber': [2000], 'userPassword': [hashed_password], 'loginShell': ['/bin/bash'], 'uidNumber': [self.generate_uid()], 'homeDirectory': ['/home/{}'.format(username)], 'uid': [username], 'cn': [email] } res = self.conn.add(dn=dn, object_class=object_class, attributes=attr) name = '{} {}'.format(first_name, last_name) r = send_email(username, password, name, email) if not r.success: return r.success, CREATE_FAIL if res: return res, CREATE_SUCCESS else: return res, CREATE_FAIL def OU_get(self): '''获取所有的OU''' self.conn.search(search_base=self.active_base_dn, search_filter=self.ou_search_filter, attributes=ALL_ATTRIBUTES) res = self.conn.response_to_json() res = json.loads(res)['entries'] return res def create_obj(self, dn, type, attr=None): ''' 新建用户or 部门,User需要设置密码,激活账户 :param dn: dn = "ou=人事部3,ou=罗辑实验室,dc=adtest,dc=intra" # 创建的OU的完整路径 dn = "cn=张三,ou=人事部3,ou=罗辑实验室,dc=adtest,dc=intra" # 创建的User的完整路径 :param type:选项:ou or user :param attr = {#User 属性表,需要设置什么属性,增加对应的键值对 "SamAccountName": "zhangsan", # 账号 "EmployeeID":"1", # 员工编号 "Sn": "张", # 姓 "name": "张三", "telephoneNumber": "12345678933", "mobile": "12345678933", "UserPrincipalName":"*****@*****.**", "Mail":"*****@*****.**", "Displayname": "张三", "Manager":"CN=李四,OU=人事部,DC=adtest,DC=com",#需要使用用户的DN路径 } attr = {#OU属性表 'name':'人事部', 'managedBy':"CN=张三,OU=IT组,OU=罗辑实验室,DC=adtest,DC=intra", #部分负责人 } :return:True and success 是创建成功了 (True, {'result': 0, 'description': 'success', 'dn': '', 'message': '', 'referrals': None, 'type': 'addResponse'}) ''' object_class = { 'user': ['user', 'posixGroup', 'top'], 'ou': ['organizationalUnit', 'posixGroup', 'top'], } res = self.conn.add(dn=dn, object_class=object_class[type], attributes=attr) if type == "user": # 如果是用户时,我们需要给账户设置密码,并把账户激活 self.conn.extend.microsoft.modify_password(dn, "XXXXXXXXX") # 设置用户密码 self.conn.modify( dn, {'userAccountControl': [('MODIFY_REPLACE', 512)]}) # 激活用户 return res, self.conn.result def del_obj(self, DN): ''' 删除用户 or 部门 :param DN: :return:True ''' res = self.conn.delete(dn=DN) return res def update_obj(self, dn, attr): '''更新员工 or 部门属性 先比较每个属性值,是否和AD中的属性一致,不一样的记录,统一update 注意: 1. attr中dn属性写在最后 2. 如果name属性不一样的话,需要先变更名字(实际是变更原始dn为新name的DN), 后续继续操作update and move_object User 的 attr 照如下格式写: dn = "cn=test4,ou=IT组,dc=adtest,dc=com" #需要移动的User的原始路径 {#updateUser需要更新的属性表 "Sn": "李", # 姓 "telephoneNumber": "12345678944", "mobile": "12345678944", "Displayname": "张三3", "Manager":"CN=李四,OU=人事部,DC=adtest,DC=com",#需要使用用户的DN路径 'DistinguishedName':"cn=张三,ou=IT组,dc=adtest,dc=com" #用户需要移动部门时,提供此属性,否则不提供 } OU 的 attr 格式如下: dn = "ou=人事部,dc=adtest,dc=com" #更新前OU的原始路径 attr = { 'name':'人事部', 'managedBy':"CN=张三,OU=IT组,DC=adtest,DC=com", 'DistinguishedName': "ou=人事部,dc=adtest,dc=com" # 更新后的部门完整路径 } ''' changes_dic = {} for k, v in attr.items(): if not self.conn.compare(dn=dn, attribute=k, value=v): if k == "name": # 改过名字后,DN就变了,这里调用重命名的方法 res = self.__rename_obj(dn=dn, newname=attr['name']) if res['description'] == "success": if "CN" == dn[:2]: dn = "cn=%s,%s" % (attr["name"], dn.split(",", 1)[1]) if "OU" == dn[:2]: dn = "DN=%s,%s" % (attr["name"], dn.split(",", 1)[1]) # 如果属性里有“DistinguishedName”,表示需要移动User or OU if k == "DistinguishedName": self.__move_object(dn=dn, new_dn=v) # 调用移动User or OU 的方法 changes_dic.update({k: [(MODIFY_REPLACE, [v])]}) self.conn.modify(dn=dn, changes=changes_dic) return self.conn.result def __rename_obj(self, dn, newname): ''' OU or User 重命名方法 :param dn:需要修改的object的完整dn路径 :param newname: 新的名字,User格式:"cn=新名字";OU格式:"OU=新名字" :return:返回中有:'description': 'success', 表示操作成功 {'result': 0, 'description': 'success', 'dn': '', 'message': '', 'referrals': None, 'type': 'modDNResponse'} ''' self.conn.modify_dn(dn, newname) return self.conn.result def compare_attr(self, dn, attr, value): '''比较员工指定的某个属性 ''' res = self.conn.compare(dn=dn, attribute=attr, value=value) return res def __move_object(self, dn, new_dn): '''移动员工 or 部门到新部门''' relative_dn, superou = new_dn.split(",", 1) res = self.conn.modify_dn(dn=dn, relative_dn=relative_dn, new_superior=superou) return res def check_credentials(self, username, password): """ 用户认证接口 # """ users = self.get_user_list() dn = None for user in users: if user['uid'].value == username: dn = user.entry_dn server = Server('192.168.0.10') connection = Connection(server, user=dn, password=password) print("username:%s ;res: %s" % (username, connection.bind())) result = connection.bind() if result: authencated = 'isAuthenticated' else: authencated = 'isNotAuthenticated' connection.closed return result, authencated
class LDAPLib: def __init__(self): self.connection = Connection(server=settings.LDAP_SERVER, user=settings.LDAP_USER, password=settings.LDAP_PASSWORD, auto_bind=True) self.user_base = ','.join(('ou=users', settings.LDAP_BASE_DN)) self.group_base = ','.join(('ou=groups', settings.LDAP_BASE_DN)) self.user_attributes = ['uid', 'cn', 'sn'] self.group_attributes = ['gidNumber', 'cn'] def get_all_users(self): search_filter = '(cn=*)' result = self.connection.search(self.user_base, search_filter, attributes=self.user_attributes) if result: return self.connection.entries return [] def get_all_groups(self): search_filter = '(cn=*)' result = self.connection.search(self.group_base, search_filter, attributes=self.group_attributes) if result: return self.connection.entries return [] def search_user(self, query): search_filter = f'(uid={query})' result = self.connection.search(self.user_base, search_filter, attributes=self.user_attributes) if result: return self.connection.entries[0] def add_user(self, uid, first_name, last_name, email, password_hash): dn = ','.join((f'uid={uid}', self.user_base)) return self.connection.add( dn, ['inetOrgPerson', 'top'], { 'uid': uid, 'cn': first_name, 'sn': last_name, 'mail': email, 'userPassword': '******' + password_hash }) def search_group(self, query): search_filter = f'(gidNumber={query})' result = self.connection.search(self.group_base, search_filter, attributes=self.group_attributes) if result: return self.connection.entries[0] def add_group(self, gid, name): dn = ','.join((f'cn={name}', self.group_base)) return self.connection.add(dn, ['posixGroup', 'top'], { 'gidNumber': gid, 'cn': name, }) def update_organization_unit(self, name): """ Make sure the organization unit exists in LDAP. """ search_filter = f'(ou={name})' result = self.connection.search(settings.LDAP_BASE_DN, search_filter, attributes=['ou']) if not result: dn = ','.join((f'ou={name}', settings.LDAP_BASE_DN)) self.connection.add(dn, ['organizationalUnit', 'top'], {'ou': name}) def delete_user(self, uid): dn = ','.join((f'uid={uid}', self.user_base)) return self.connection.delete(dn) def delete_group(self, cn): dn = ','.join((f'cn={cn}', self.group_base)) return self.connection.delete(dn) def check_password(self, uid, password_hash): dn = ','.join((f'uid={uid}', self.user_base)) return self.connection.compare(dn, 'userPassword', '{CRYPT}' + password_hash) def change_password(self, uid, password_hash): dn = ','.join((f'uid={uid}', self.user_base)) changes = { 'userPassword': [(MODIFY_REPLACE, ['{CRYPT}' + password_hash])] } self.connection.modify(dn, changes) def update_group_members(self, cn, members): dn = ','.join((f'cn={cn}', self.group_base)) if members: changes = {'memberUid': [(MODIFY_REPLACE, members)]} else: changes = {'memberUid': [(MODIFY_DELETE, [])]} self.connection.modify(dn, changes)