### 2021/05/19 -- 更新:

+ 添加了企业微信支持,修改pwdselfservice/local_settings.py中的SCAN_CODE_TYPE = 'DING'或SCAN_CODE_TYPE = 'WEWORK',区分使用哪个应用扫码验证
+ 添加Reids缓存Token支持,如果不配置Redis则使用MemoryStorage缓存到内存中
This commit is contained in:
向乐🌌
2021-05-19 17:07:26 +08:00
parent 00d1d9a03c
commit 89b1c0de46
29 changed files with 753 additions and 440 deletions

View File

@@ -46,7 +46,7 @@ class AdOps(object):
self.authentication = authentication
self.auto_bind = auto_bind
server = Server(host='%s' % AD_HOST, use_ssl=self.use_ssl, port=port, get_info=ALL)
server = Server(host='%s' % AD_HOST, connect_timeout=1, use_ssl=self.use_ssl, port=port, get_info=ALL)
try:
self.conn = Connection(server, auto_bind=self.auto_bind, user=r'{}\{}'.format(self.domain, self.user), password=self.password,
authentication=self.authentication, raise_exceptions=True)
@@ -115,7 +115,7 @@ class AdOps(object):
def ad_get_user_dn_by_account(self, username):
"""
通过mail查询某个用户的完整DN
通过username查询某个用户的完整DN
:param username:
:return: DN
"""
@@ -179,7 +179,7 @@ class AdOps(object):
def ad_get_user_locked_status_by_account(self, username):
"""
通过mail获取某个用户账号是否被锁定
通过username获取某个用户账号是否被锁定
:param username:
:return: 如果结果是1601-01-01说明账号未锁定返回0
"""

View File

@@ -9,47 +9,31 @@ from urllib.parse import quote
import requests
from dingtalk.client import AppKeyClient
from pwdselfservice import cache_storage
from pwdselfservice.local_settings import DING_APP_KEY, DING_APP_SECRET, DING_CORP_ID, DING_URL, DING_MO_APP_ID, DING_MO_APP_SECRET
class DingDingOps(object):
def __init__(self, corp_id=DING_CORP_ID, app_key=DING_APP_KEY, app_secret=DING_APP_SECRET, mo_app_id=DING_MO_APP_ID, mo_app_secret=DING_MO_APP_SECRET):
def __init__(self, corp_id=DING_CORP_ID, app_key=DING_APP_KEY, app_secret=DING_APP_SECRET, mo_app_id=DING_MO_APP_ID, mo_app_secret=DING_MO_APP_SECRET, storage=cache_storage):
self.corp_id = corp_id
self.app_key = app_key
self.app_secret = app_secret
self.mo_app_id = mo_app_id
self.mo_app_secret = mo_app_secret
self.storage = storage
@property
def ding_client_connect(self):
def _client_connect(self):
"""
钉钉连接器
:return:
"""
return AppKeyClient(corp_id=self.corp_id, app_key=self.app_key, app_secret=self.app_secret)
@property
def ding_get_access_token(self):
def get_union_id_by_code(self, code):
"""
获取企业内部应用的access_token
:return:
"""
return self.ding_client_connect.access_token
def ding_get_dept_user_list_detail(self, dept_id, offset, size):
"""
获取部门中的用户列表详细清单
:param dept_id: 部门ID
:param offset: 偏移量(可理解为步进量)
:param size: 一次查询多少个
:return:
"""
return self.ding_client_connect.user.list(department_id=dept_id, offset=offset, size=size)
def ding_get_union_id_by_code(self, code):
"""
通过移动应用接入扫码返回的临时授权码获取用户的unionid
通过移动应用接入扫码返回的临时授权码使用临时授权码换取用户的unionid
:param code:
:return:
"""
@@ -68,57 +52,36 @@ class DingDingOps(object):
json=dict(tmp_auth_code=code),
)
resp = resp.json()
print(resp)
try:
if resp['errcode'] != 0:
return False, 'ding_get_union_id_by_code: %s' % str(resp)
return False, 'get_union_id_by_code: %s' % str(resp)
else:
return True, resp["user_info"]["unionid"]
except Exception:
return False, 'ding_get_union_id_by_code: %s' % str(resp)
return False, 'get_union_id_by_code: %s' % str(resp)
def ding_get_userid_by_union_id(self, union_id):
def get_user_id_by_code(self, code):
"""
通过unionid获取用户的userid
:param union_id: 用户在当前钉钉开放平台账号范围内的唯一标识
通过code获取用户的userid
:param id: 用户在当前钉钉开放平台账号范围内的唯一标识
:return:
"""
try:
return True, self.ding_client_connect.user.get_userid_by_unionid(union_id)['userid']
except Exception as e:
return False, 'ding_get_union_id_by_code: %s' % str(e)
_status, union_id = self.get_union_id_by_code(code)
if _status:
return True, self._client_connect.user.get_userid_by_unionid(union_id).get('userid')
else:
return False, 'get_user_id_by_code: %s' % str(union_id)
except (KeyError, IndexError) as k_error:
return False, 'ding_get_union_id_by_code: %s' % str(k_error)
@property
def ding_get_org_user_count(self):
"""
企业员工数量
only_active 是否包含未激活钉钉的人员数量
:return:
"""
return self.ding_client_connect.user.get_org_user_count('only_active')
def ding_get_userinfo_detail(self, user_id):
def get_user_detail_by_user_id(self, user_id):
"""
通过user_id 获取用户详细信息
user_id 用户ID
:return:
"""
try:
return True, self.ding_client_connect.user.get(user_id)
return True, self._client_connect.user.get(user_id)
except Exception as e:
return False, 'ding_get_union_id_by_code: %s' % str(e)
return False, 'get_user_detail_by_user_id: %s' % str(e)
except (KeyError, IndexError) as k_error:
return False, 'ding_get_union_id_by_code: %s' % str(k_error)
if __name__ == '__main__':
start = time.time()
d = DingDingOps().ding_client_connect
unicode = ''
# print(d.)
end = time.time()
print("running:" + str(round((end - start), 3)))
return False, 'get_user_detail_by_user_id: %s' % str(k_error)

View File

@@ -24,12 +24,22 @@ def format2username(account):
elif re.fullmatch(domain_compile, account):
return re.fullmatch(domain_compile, account).group(2)
else:
return account
return account.lower()
else:
raise NameError("输入的账号不能为空..")
def get_user_is_active(user_info):
try:
return True, user_info.get('active') or user_info.get('status')
except Exception as e:
return False, 'get_user_is_active: %s' % str(e)
except (KeyError, IndexError) as k_error:
return False, 'get_user_is_active: %s' % str(k_error)
if __name__ == '__main__':
user = 'aaa\jf.com'
user = 'jf.com\XiangLe'
username = format2username(user)
print(username)

23
utils/storage/__init__.py Normal file
View File

@@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals
class BaseStorage(object):
def get(self, key, default=None):
raise NotImplementedError()
def set(self, key, value, ttl=None):
raise NotImplementedError()
def delete(self, key):
raise NotImplementedError()
def __getitem__(self, key):
self.get(key)
def __setitem__(self, key, value):
self.set(key, value)
def __delitem__(self, key):
self.delete(key)

58
utils/storage/cache.py Normal file
View File

@@ -0,0 +1,58 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals
import inspect
from utils.storage import BaseStorage
def _is_cache_item(obj):
return isinstance(obj, CacheItem)
class CacheItem(object):
def __init__(self, cache=None, name=None):
self.cache = cache
self.name = name
def key_name(self, key):
if isinstance(key, (tuple, list)):
key = ':'.join(key)
k = '{0}:{1}'.format(self.cache.prefix, self.name)
if key is not None:
k = '{0}:{1}'.format(k, key)
return k
def get(self, key=None, default=None):
return self.cache.storage.get(self.key_name(key), default)
def set(self, key=None, value=None, ttl=None):
return self.cache.storage.set(self.key_name(key), value, ttl)
def delete(self, key=None):
return self.cache.storage.delete(self.key_name(key))
class BaseCache(object):
def __new__(cls, *args, **kwargs):
self = super(BaseCache, cls).__new__(cls)
api_endpoints = inspect.getmembers(self, _is_cache_item)
for name, api in api_endpoints:
api_cls = type(api)
api = api_cls(self, name)
setattr(self, name, api)
return self
def __init__(self, storage, prefix='client'):
assert isinstance(storage, BaseStorage)
self.storage = storage
self.prefix = prefix
class WeWorkCache(BaseCache):
access_token = CacheItem()

View File

@@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals
import json
from dingtalk.core.utils import to_text
from utils.storage import BaseStorage
class KvStorage(BaseStorage):
def __init__(self, kvdb, prefix='wework'):
for method_name in ('get', 'set', 'delete'):
assert hasattr(kvdb, method_name)
self.kvdb = kvdb
self.prefix = prefix
def key_name(self, key):
return '{0}:{1}'.format(self.prefix, key)
def get(self, key, default=None):
key = self.key_name(key)
value = self.kvdb.get(key)
if value is None:
return default
return json.loads(to_text(value))
def set(self, key, value, ttl=None):
if value is None:
return
key = self.key_name(key)
value = json.dumps(value)
self.kvdb.set(key, value, ttl)
def delete(self, key):
key = self.key_name(key)
self.kvdb.delete(key)

View File

@@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals
import time
from utils.storage import BaseStorage
class MemoryStorage(BaseStorage):
def __init__(self):
self._data = {}
def get(self, key, default=None):
ret = self._data.get(key, None)
if ret is None or len(ret) != 2:
return default
else:
value = ret[0]
expires_at = ret[1]
if expires_at is None or expires_at > time.time():
return value
else:
return default
def set(self, key, value, ttl=None):
if value is None:
return
self._data[key] = (value, int(time.time()) + ttl)
def delete(self, key):
self._data.pop(key, None)

View File

@@ -0,0 +1,113 @@
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import json
import requests
DEBUG = False
class ApiException(Exception):
def __init__(self, errCode, errMsg):
self.errCode = errCode
self.errMsg = errMsg
class AbstractApi(object):
def __init__(self):
return
def access_token(self):
raise NotImplementedError
def http_call(self, url_type, args=None):
short_url = url_type[0]
method = url_type[1]
response = {}
for retryCnt in range(0, 3):
if 'POST' == method:
url = self.__make_url(short_url)
response = self.__http_post(url, args)
elif 'GET' == method:
url = self.__make_url(short_url)
url = self.__append_args(url, args)
response = self.__http_get(url)
else:
raise ApiException(-1, "unknown method type")
# check if token expired
if self.__token_expired(response.get('errcode')):
self.__refresh_token(short_url)
retryCnt += 1
continue
else:
break
return self.__check_response(response)
@staticmethod
def __append_args(url, args):
if args is None:
return url
for key, value in args.items():
if '?' in url:
url += ('&' + key + '=' + value)
else:
url += ('?' + key + '=' + value)
return url
@staticmethod
def __make_url(short_url):
base = "https://qyapi.weixin.qq.com"
if short_url[0] == '/':
return base + short_url
else:
return base + '/' + short_url
def __append_token(self, url):
if 'ACCESS_TOKEN' in url:
return url.replace('ACCESS_TOKEN', self.access_token())
else:
return url
def __http_post(self, url, args):
real_url = self.__append_token(url)
if DEBUG is True:
print(real_url, args)
return requests.post(real_url, data=json.dumps(args, ensure_ascii=False).encode('utf-8')).json()
def __http_get(self, url):
real_url = self.__append_token(url)
if DEBUG is True:
print(real_url)
return requests.get(real_url).json()
def __post_file(self, url, media_file):
return requests.post(url, file=media_file).json()
@staticmethod
def __check_response(response):
errCode = response.get('errcode')
errMsg = response.get('errmsg')
if errCode == 0:
return response
else:
raise ApiException(errCode, errMsg)
@staticmethod
def __token_expired(errCode):
if errCode == 40014 or errCode == 42001 or errCode == 42007 or errCode == 42009:
return True
else:
return False
def __refresh_token(self, url):
if 'ACCESS_TOKEN' in url:
self.access_token()

140
utils/wework_ops.py Normal file
View File

@@ -0,0 +1,140 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# @FileName WEWORK_ops.py
# @Software:
# @Author: Leven Xiang
# @Mail: xiangle0109@outlook.com
# @Date 2021/5/18 16:55
from __future__ import absolute_import, unicode_literals
from pwdselfservice import cache_storage
from pwdselfservice.local_settings import *
from utils.storage.cache import WeWorkCache
from utils.wework_api.AbstractApi import *
CORP_API_TYPE = {
'GET_ACCESS_TOKEN': ['/cgi-bin/gettoken', 'GET'],
'USER_CREATE': ['/cgi-bin/user/create?access_token=ACCESS_TOKEN', 'POST'],
'USER_GET': ['/cgi-bin/user/get?access_token=ACCESS_TOKEN', 'GET'],
'USER_UPDATE': ['/cgi-bin/user/update?access_token=ACCESS_TOKEN', 'POST'],
'USER_DELETE': ['/cgi-bin/user/delete?access_token=ACCESS_TOKEN', 'GET'],
'USER_BATCH_DELETE': ['/cgi-bin/user/batchdelete?access_token=ACCESS_TOKEN', 'POST'],
'USER_SIMPLE_LIST': ['/cgi-bin/user/simplelist?access_token=ACCESS_TOKEN', 'GET'],
'USER_LIST': ['/cgi-bin/user/list?access_token=ACCESS_TOKEN', 'GET'],
'USERID_TO_OPENID': ['/cgi-bin/user/convert_to_openid?access_token=ACCESS_TOKEN', 'POST'],
'OPENID_TO_USERID': ['/cgi-bin/user/convert_to_userid?access_token=ACCESS_TOKEN', 'POST'],
'USER_AUTH_SUCCESS': ['/cgi-bin/user/authsucc?access_token=ACCESS_TOKEN', 'GET'],
'DEPARTMENT_CREATE': ['/cgi-bin/department/create?access_token=ACCESS_TOKEN', 'POST'],
'DEPARTMENT_UPDATE': ['/cgi-bin/department/update?access_token=ACCESS_TOKEN', 'POST'],
'DEPARTMENT_DELETE': ['/cgi-bin/department/delete?access_token=ACCESS_TOKEN', 'GET'],
'DEPARTMENT_LIST': ['/cgi-bin/department/list?access_token=ACCESS_TOKEN', 'GET'],
'TAG_CREATE': ['/cgi-bin/tag/create?access_token=ACCESS_TOKEN', 'POST'],
'TAG_UPDATE': ['/cgi-bin/tag/update?access_token=ACCESS_TOKEN', 'POST'],
'TAG_DELETE': ['/cgi-bin/tag/delete?access_token=ACCESS_TOKEN', 'GET'],
'TAG_GET_USER': ['/cgi-bin/tag/get?access_token=ACCESS_TOKEN', 'GET'],
'TAG_ADD_USER': ['/cgi-bin/tag/addtagusers?access_token=ACCESS_TOKEN', 'POST'],
'TAG_DELETE_USER': ['/cgi-bin/tag/deltagusers?access_token=ACCESS_TOKEN', 'POST'],
'TAG_GET_LIST': ['/cgi-bin/tag/list?access_token=ACCESS_TOKEN', 'GET'],
'BATCH_JOB_GET_RESULT': ['/cgi-bin/batch/getresult?access_token=ACCESS_TOKEN', 'GET'],
'BATCH_INVITE': ['/cgi-bin/batch/invite?access_token=ACCESS_TOKEN', 'POST'],
'AGENT_GET': ['/cgi-bin/agent/get?access_token=ACCESS_TOKEN', 'GET'],
'AGENT_SET': ['/cgi-bin/agent/set?access_token=ACCESS_TOKEN', 'POST'],
'AGENT_GET_LIST': ['/cgi-bin/agent/list?access_token=ACCESS_TOKEN', 'GET'],
'MENU_CREATE': ['/cgi-bin/menu/create?access_token=ACCESS_TOKEN', 'POST'],
'MENU_GET': ['/cgi-bin/menu/get?access_token=ACCESS_TOKEN', 'GET'],
'MENU_DELETE': ['/cgi-bin/menu/delete?access_token=ACCESS_TOKEN', 'GET'],
'MESSAGE_SEND': ['/cgi-bin/message/send?access_token=ACCESS_TOKEN', 'POST'],
'MESSAGE_REVOKE': ['/cgi-bin/message/revoke?access_token=ACCESS_TOKEN', 'POST'],
'MEDIA_GET': ['/cgi-bin/media/get?access_token=ACCESS_TOKEN', 'GET'],
'GET_USER_INFO_BY_CODE': ['/cgi-bin/user/getuserinfo?access_token=ACCESS_TOKEN', 'GET'],
'GET_USER_DETAIL': ['/cgi-bin/user/getuserdetail?access_token=ACCESS_TOKEN', 'POST'],
'GET_TICKET': ['/cgi-bin/ticket/get?access_token=ACCESS_TOKEN', 'GET'],
'GET_JSAPI_TICKET': ['/cgi-bin/get_jsapi_ticket?access_token=ACCESS_TOKEN', 'GET'],
'GET_CHECKIN_OPTION': ['/cgi-bin/checkin/getcheckinoption?access_token=ACCESS_TOKEN', 'POST'],
'GET_CHECKIN_DATA': ['/cgi-bin/checkin/getcheckindata?access_token=ACCESS_TOKEN', 'POST'],
'GET_APPROVAL_DATA': ['/cgi-bin/corp/getapprovaldata?access_token=ACCESS_TOKEN', 'POST'],
'GET_INVOICE_INFO': ['/cgi-bin/card/invoice/reimburse/getinvoiceinfo?access_token=ACCESS_TOKEN', 'POST'],
'UPDATE_INVOICE_STATUS':
['/cgi-bin/card/invoice/reimburse/updateinvoicestatus?access_token=ACCESS_TOKEN', 'POST'],
'BATCH_UPDATE_INVOICE_STATUS':
['/cgi-bin/card/invoice/reimburse/updatestatusbatch?access_token=ACCESS_TOKEN', 'POST'],
'BATCH_GET_INVOICE_INFO':
['/cgi-bin/card/invoice/reimburse/getinvoiceinfobatch?access_token=ACCESS_TOKEN', 'POST'],
'APP_CHAT_CREATE': ['/cgi-bin/appchat/create?access_token=ACCESS_TOKEN', 'POST'],
'APP_CHAT_GET': ['/cgi-bin/appchat/get?access_token=ACCESS_TOKEN', 'GET'],
'APP_CHAT_UPDATE': ['/cgi-bin/appchat/update?access_token=ACCESS_TOKEN', 'POST'],
'APP_CHAT_SEND': ['/cgi-bin/appchat/send?access_token=ACCESS_TOKEN', 'POST'],
'MINIPROGRAM_CODE_TO_SESSION_KEY': ['/cgi-bin/miniprogram/jscode2session?access_token=ACCESS_TOKEN', 'GET'],
}
class WeWorkOps(AbstractApi):
def __init__(self, corp_id=WEWORK_CORP_ID, agent_id=WEWORK_AGENT_ID, agent_secret=WEWORK_AGNET_SECRET, storage=cache_storage, prefix='wework'):
super().__init__()
self.corp_id = corp_id
self.agent_id = agent_id
self.agent_secret = agent_secret
self.storage = storage
self.cache = WeWorkCache(self.storage, "%s:%s" % (prefix, "corp_id:%s" % self.corp_id))
def access_token(self):
access_token = self.cache.access_token.get()
if access_token is None:
ret = self.get_access_token()
access_token = ret['access_token']
expires_in = ret.get('expires_in', 7200)
self.cache.access_token.set(value=access_token, ttl=expires_in)
return access_token
def get_access_token(self):
return self.http_call(
CORP_API_TYPE['GET_ACCESS_TOKEN'],
{
'corpid': self.corp_id,
'corpsecret': self.agent_secret,
})
def get_user_id_by_code(self, code):
try:
return True, self.http_call(
CORP_API_TYPE['GET_USER_INFO_BY_CODE'],
{
'code': code,
}).get('UserId')
except ApiException as e:
return False, "get_user_id_by_code: {}-{}" .format(e.errCode, e.errMsg)
except Exception as e:
return False, "get_user_id_by_code: {}".format(e)
def get_user_detail_by_user_id(self, user_id):
try:
return True, self.http_call(
CORP_API_TYPE['USER_GET'],
{
'userid': user_id,
})
except ApiException as e:
return False, "get_user_detail_by_user_id: {}-{}" .format(e.errCode, e.errMsg)
except Exception as e:
return False, "get_user_detail_by_user_id: {}".format(e)
if __name__ == '__main__':
wx = WeWorkOps()
print(wx.get_user_detail_by_user_id('XiangLe'))