mirror of https://github.com/jumpserver/jumpserver
perf: Support WeCom DingTalk FeiShu Lark Slack attribute mapping
parent
9825f9fbd2
commit
8d83c953d3
|
@ -2,16 +2,17 @@ import base64
|
||||||
import hmac
|
import hmac
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
from common.sdk.im.mixin import BaseRequest
|
from common.sdk.im.mixin import BaseRequest
|
||||||
from common.sdk.im.utils import digest, as_request
|
from common.sdk.im.utils import digest, as_request
|
||||||
from common.utils import get_logger
|
from common.utils import get_logger
|
||||||
from users.utils import construct_user_email
|
from users.utils import construct_user_email, flatten_dict, map_attributes
|
||||||
|
|
||||||
logger = get_logger(__file__)
|
logger = get_logger(__file__)
|
||||||
|
|
||||||
|
|
||||||
def sign(secret, data):
|
def sign(secret, data):
|
||||||
|
|
||||||
digest = hmac.HMAC(
|
digest = hmac.HMAC(
|
||||||
key=secret.encode('utf8'),
|
key=secret.encode('utf8'),
|
||||||
msg=data.encode('utf8'),
|
msg=data.encode('utf8'),
|
||||||
|
@ -115,6 +116,8 @@ class DingTalkRequests(BaseRequest):
|
||||||
|
|
||||||
|
|
||||||
class DingTalk:
|
class DingTalk:
|
||||||
|
attributes = settings.DINGTALK_RENAME_ATTRIBUTES
|
||||||
|
|
||||||
def __init__(self, appid, appsecret, agentid, timeout=None):
|
def __init__(self, appid, appsecret, agentid, timeout=None):
|
||||||
self._appid = appid or ''
|
self._appid = appid or ''
|
||||||
self._appsecret = appsecret or ''
|
self._appsecret = appsecret or ''
|
||||||
|
@ -206,17 +209,24 @@ class DingTalk:
|
||||||
data = self._request.post(URL.GET_SEND_MSG_PROGRESS, json=body, with_token=True)
|
data = self._request.post(URL.GET_SEND_MSG_PROGRESS, json=body, with_token=True)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def get_user_detail(self, user_id, **kwargs):
|
@staticmethod
|
||||||
# https://open.dingtalk.com/document/orgapp/query-user-details
|
def default_user_detail(data):
|
||||||
body = {'userid': user_id}
|
username = data['user_id']
|
||||||
data = self._request.post(
|
|
||||||
URL.GET_USER_INFO_BY_USER_ID, json=body, with_token=True
|
|
||||||
)
|
|
||||||
data = data['result']
|
|
||||||
username = user_id
|
|
||||||
name = data.get('name', username)
|
name = data.get('name', username)
|
||||||
email = data.get('email') or data.get('org_email')
|
email = data.get('email') or data.get('org_email')
|
||||||
email = construct_user_email(username, email)
|
email = construct_user_email(username, email)
|
||||||
return {
|
return {
|
||||||
'username': username, 'name': name, 'email': email
|
'username': username, 'name': name, 'email': email
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def get_user_detail(self, user_id, **kwargs):
|
||||||
|
# https://open.dingtalk.com/document/orgapp/query-user-details
|
||||||
|
data = self._request.post(
|
||||||
|
URL.GET_USER_INFO_BY_USER_ID, json={'userid': user_id}, with_token=True
|
||||||
|
)
|
||||||
|
data = data['result']
|
||||||
|
data['user_id'] = user_id
|
||||||
|
info = flatten_dict(data)
|
||||||
|
default_detail = self.default_user_detail(data)
|
||||||
|
detail = map_attributes(default_detail, info, self.attributes)
|
||||||
|
return detail
|
||||||
|
|
|
@ -6,7 +6,7 @@ from rest_framework.exceptions import APIException
|
||||||
from common.sdk.im.mixin import RequestMixin, BaseRequest
|
from common.sdk.im.mixin import RequestMixin, BaseRequest
|
||||||
from common.sdk.im.utils import digest
|
from common.sdk.im.utils import digest
|
||||||
from common.utils.common import get_logger
|
from common.utils.common import get_logger
|
||||||
from users.utils import construct_user_email
|
from users.utils import construct_user_email, flatten_dict, map_attributes
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
@ -84,7 +84,7 @@ class FeiShu(RequestMixin):
|
||||||
非业务数据导致的错误直接抛异常,说明是系统配置错误,业务代码不用理会
|
非业务数据导致的错误直接抛异常,说明是系统配置错误,业务代码不用理会
|
||||||
"""
|
"""
|
||||||
requests_cls = FeishuRequests
|
requests_cls = FeishuRequests
|
||||||
attributes = settings.LARK_RENAME_ATTRIBUTES
|
attributes = settings.FEISHU_RENAME_ATTRIBUTES
|
||||||
|
|
||||||
def __init__(self, app_id, app_secret, timeout=None):
|
def __init__(self, app_id, app_secret, timeout=None):
|
||||||
self._app_id = app_id or ''
|
self._app_id = app_id or ''
|
||||||
|
@ -151,11 +151,7 @@ class FeiShu(RequestMixin):
|
||||||
# get_user_id_by_code 已经返回个人信息,这里直接解析
|
# get_user_id_by_code 已经返回个人信息,这里直接解析
|
||||||
data = kwargs['other_info']
|
data = kwargs['other_info']
|
||||||
data['user_id'] = user_id
|
data['user_id'] = user_id
|
||||||
detail = self.default_user_detail(data)
|
info = flatten_dict(data)
|
||||||
|
default_detail = self.default_user_detail(data)
|
||||||
for local_name, remote_name in self.attributes.items():
|
detail = map_attributes(default_detail, info, self.attributes)
|
||||||
value = data.get(remote_name)
|
|
||||||
if not value:
|
|
||||||
continue
|
|
||||||
detail[local_name] = value
|
|
||||||
return detail
|
return detail
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import mistune
|
import mistune
|
||||||
import requests
|
import requests
|
||||||
|
from django.conf import settings
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework.exceptions import APIException
|
from rest_framework.exceptions import APIException
|
||||||
|
|
||||||
from common.utils.common import get_logger
|
from common.utils.common import get_logger
|
||||||
from jumpserver.utils import get_current_request
|
from jumpserver.utils import get_current_request
|
||||||
from users.utils import construct_user_email
|
from users.utils import construct_user_email, flatten_dict, map_attributes
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
@ -93,6 +94,8 @@ class SlackRequests:
|
||||||
|
|
||||||
|
|
||||||
class Slack:
|
class Slack:
|
||||||
|
attributes = settings.SLACK_RENAME_ATTRIBUTES
|
||||||
|
|
||||||
def __init__(self, client_id=None, client_secret=None, bot_token=None, **kwargs):
|
def __init__(self, client_id=None, client_secret=None, bot_token=None, **kwargs):
|
||||||
self._client = SlackRequests(
|
self._client = SlackRequests(
|
||||||
client_id=client_id, client_secret=client_secret, bot_token=bot_token
|
client_id=client_id, client_secret=client_secret, bot_token=bot_token
|
||||||
|
@ -138,13 +141,22 @@ class Slack:
|
||||||
logger.exception(e)
|
logger.exception(e)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_user_detail(user_id, **kwargs):
|
def default_user_detail(data):
|
||||||
# get_user_id_by_code 已经返回个人信息,这里直接解析
|
username = data['user_id']
|
||||||
user_info = kwargs['other_info']
|
username = data.get('name', username)
|
||||||
username = user_info.get('name') or user_id
|
name = data.get('real_name', username)
|
||||||
name = user_info.get('real_name', username)
|
email = data.get('profile.email')
|
||||||
email = user_info.get('profile', {}).get('email')
|
|
||||||
email = construct_user_email(username, email)
|
email = construct_user_email(username, email)
|
||||||
return {
|
return {
|
||||||
'username': username, 'name': name, 'email': email
|
'username': username, 'name': name, 'email': email
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def get_user_detail(self, user_id, **kwargs):
|
||||||
|
# https://api.slack.com/methods/users.info
|
||||||
|
# get_user_id_by_code 已经返回个人信息,这里直接解析
|
||||||
|
data = kwargs['other_info']
|
||||||
|
data['user_id'] = user_id
|
||||||
|
info = flatten_dict(data)
|
||||||
|
default_detail = self.default_user_detail(data)
|
||||||
|
detail = map_attributes(default_detail, info, self.attributes)
|
||||||
|
return detail
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
from typing import Iterable, AnyStr
|
from typing import Iterable, AnyStr
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework.exceptions import APIException
|
from rest_framework.exceptions import APIException
|
||||||
|
|
||||||
from common.sdk.im.mixin import RequestMixin, BaseRequest
|
from common.sdk.im.mixin import RequestMixin, BaseRequest
|
||||||
from common.sdk.im.utils import digest, update_values
|
from common.sdk.im.utils import digest, update_values
|
||||||
from common.utils.common import get_logger
|
from common.utils.common import get_logger
|
||||||
from users.utils import construct_user_email
|
from users.utils import construct_user_email, flatten_dict, map_attributes
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
@ -79,6 +80,7 @@ class WeCom(RequestMixin):
|
||||||
"""
|
"""
|
||||||
非业务数据导致的错误直接抛异常,说明是系统配置错误,业务代码不用理会
|
非业务数据导致的错误直接抛异常,说明是系统配置错误,业务代码不用理会
|
||||||
"""
|
"""
|
||||||
|
attributes = settings.WECOM_RENAME_ATTRIBUTES
|
||||||
|
|
||||||
def __init__(self, corpid, corpsecret, agentid, timeout=None):
|
def __init__(self, corpid, corpsecret, agentid, timeout=None):
|
||||||
self._corpid = corpid or ''
|
self._corpid = corpid or ''
|
||||||
|
@ -173,10 +175,8 @@ class WeCom(RequestMixin):
|
||||||
logger.error(f'WeCom response 200 but get field from json error: fields=UserId|OpenId')
|
logger.error(f'WeCom response 200 but get field from json error: fields=UserId|OpenId')
|
||||||
raise WeComError
|
raise WeComError
|
||||||
|
|
||||||
def get_user_detail(self, user_id, **kwargs):
|
@staticmethod
|
||||||
# https://open.work.weixin.qq.com/api/doc/90000/90135/90196
|
def default_user_detail(data):
|
||||||
params = {'userid': user_id}
|
|
||||||
data = self._requests.get(URL.GET_USER_DETAIL, params)
|
|
||||||
username = data.get('userid')
|
username = data.get('userid')
|
||||||
name = data.get('name', username)
|
name = data.get('name', username)
|
||||||
email = data.get('email') or data.get('biz_mail')
|
email = data.get('email') or data.get('biz_mail')
|
||||||
|
@ -184,3 +184,11 @@ class WeCom(RequestMixin):
|
||||||
return {
|
return {
|
||||||
'username': username, 'name': name, 'email': email
|
'username': username, 'name': name, 'email': email
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def get_user_detail(self, user_id, **kwargs):
|
||||||
|
# https://open.work.weixin.qq.com/api/doc/90000/90135/90196
|
||||||
|
data = self._requests.get(URL.GET_USER_DETAIL, {'userid': user_id})
|
||||||
|
info = flatten_dict(data)
|
||||||
|
default_detail = self.default_user_detail(data)
|
||||||
|
detail = map_attributes(default_detail, info, self.attributes)
|
||||||
|
return detail
|
||||||
|
|
|
@ -397,12 +397,22 @@ class Config(dict):
|
||||||
'WECOM_CORPID': '',
|
'WECOM_CORPID': '',
|
||||||
'WECOM_AGENTID': '',
|
'WECOM_AGENTID': '',
|
||||||
'WECOM_SECRET': '',
|
'WECOM_SECRET': '',
|
||||||
|
'WECOM_RENAME_ATTRIBUTES': {
|
||||||
|
'name': 'name',
|
||||||
|
'username': 'userid',
|
||||||
|
'email': 'email'
|
||||||
|
},
|
||||||
|
|
||||||
# 钉钉
|
# 钉钉
|
||||||
'AUTH_DINGTALK': False,
|
'AUTH_DINGTALK': False,
|
||||||
'DINGTALK_AGENTID': '',
|
'DINGTALK_AGENTID': '',
|
||||||
'DINGTALK_APPKEY': '',
|
'DINGTALK_APPKEY': '',
|
||||||
'DINGTALK_APPSECRET': '',
|
'DINGTALK_APPSECRET': '',
|
||||||
|
'DINGTALK_RENAME_ATTRIBUTES': {
|
||||||
|
'name': 'name',
|
||||||
|
'username': 'user_id',
|
||||||
|
'email': 'email'
|
||||||
|
},
|
||||||
|
|
||||||
# 飞书
|
# 飞书
|
||||||
'AUTH_FEISHU': False,
|
'AUTH_FEISHU': False,
|
||||||
|
@ -429,6 +439,11 @@ class Config(dict):
|
||||||
'SLACK_CLIENT_ID': '',
|
'SLACK_CLIENT_ID': '',
|
||||||
'SLACK_CLIENT_SECRET': '',
|
'SLACK_CLIENT_SECRET': '',
|
||||||
'SLACK_BOT_TOKEN': '',
|
'SLACK_BOT_TOKEN': '',
|
||||||
|
'SLACK_RENAME_ATTRIBUTES': {
|
||||||
|
'name': 'real_name',
|
||||||
|
'username': 'name',
|
||||||
|
'email': 'profile.email'
|
||||||
|
},
|
||||||
|
|
||||||
'LOGIN_REDIRECT_TO_BACKEND': '', # 'OPENID / CAS / SAML2
|
'LOGIN_REDIRECT_TO_BACKEND': '', # 'OPENID / CAS / SAML2
|
||||||
'LOGIN_REDIRECT_MSG_ENABLED': True,
|
'LOGIN_REDIRECT_MSG_ENABLED': True,
|
||||||
|
|
|
@ -130,12 +130,14 @@ AUTH_WECOM = CONFIG.AUTH_WECOM
|
||||||
WECOM_CORPID = CONFIG.WECOM_CORPID
|
WECOM_CORPID = CONFIG.WECOM_CORPID
|
||||||
WECOM_AGENTID = CONFIG.WECOM_AGENTID
|
WECOM_AGENTID = CONFIG.WECOM_AGENTID
|
||||||
WECOM_SECRET = CONFIG.WECOM_SECRET
|
WECOM_SECRET = CONFIG.WECOM_SECRET
|
||||||
|
WECOM_RENAME_ATTRIBUTES = CONFIG.WECOM_RENAME_ATTRIBUTES
|
||||||
|
|
||||||
# DingDing auth
|
# DingDing auth
|
||||||
AUTH_DINGTALK = CONFIG.AUTH_DINGTALK
|
AUTH_DINGTALK = CONFIG.AUTH_DINGTALK
|
||||||
DINGTALK_AGENTID = CONFIG.DINGTALK_AGENTID
|
DINGTALK_AGENTID = CONFIG.DINGTALK_AGENTID
|
||||||
DINGTALK_APPKEY = CONFIG.DINGTALK_APPKEY
|
DINGTALK_APPKEY = CONFIG.DINGTALK_APPKEY
|
||||||
DINGTALK_APPSECRET = CONFIG.DINGTALK_APPSECRET
|
DINGTALK_APPSECRET = CONFIG.DINGTALK_APPSECRET
|
||||||
|
DINGTALK_RENAME_ATTRIBUTES = CONFIG.DINGTALK_RENAME_ATTRIBUTES
|
||||||
|
|
||||||
# FeiShu auth
|
# FeiShu auth
|
||||||
AUTH_FEISHU = CONFIG.AUTH_FEISHU
|
AUTH_FEISHU = CONFIG.AUTH_FEISHU
|
||||||
|
@ -153,6 +155,7 @@ AUTH_SLACK = CONFIG.AUTH_SLACK
|
||||||
SLACK_CLIENT_ID = CONFIG.SLACK_CLIENT_ID
|
SLACK_CLIENT_ID = CONFIG.SLACK_CLIENT_ID
|
||||||
SLACK_CLIENT_SECRET = CONFIG.SLACK_CLIENT_SECRET
|
SLACK_CLIENT_SECRET = CONFIG.SLACK_CLIENT_SECRET
|
||||||
SLACK_BOT_TOKEN = CONFIG.SLACK_BOT_TOKEN
|
SLACK_BOT_TOKEN = CONFIG.SLACK_BOT_TOKEN
|
||||||
|
SLACK_RENAME_ATTRIBUTES = CONFIG.SLACK_RENAME_ATTRIBUTES
|
||||||
|
|
||||||
# Saml2 auth
|
# Saml2 auth
|
||||||
AUTH_SAML2 = CONFIG.AUTH_SAML2
|
AUTH_SAML2 = CONFIG.AUTH_SAML2
|
||||||
|
|
|
@ -13,3 +13,10 @@ class DingTalkSettingSerializer(serializers.Serializer):
|
||||||
DINGTALK_APPKEY = serializers.CharField(max_length=256, required=True, label='App Key')
|
DINGTALK_APPKEY = serializers.CharField(max_length=256, required=True, label='App Key')
|
||||||
DINGTALK_APPSECRET = EncryptedField(max_length=256, required=False, label='App Secret')
|
DINGTALK_APPSECRET = EncryptedField(max_length=256, required=False, label='App Secret')
|
||||||
AUTH_DINGTALK = serializers.BooleanField(default=False, label=_('Dingtalk'))
|
AUTH_DINGTALK = serializers.BooleanField(default=False, label=_('Dingtalk'))
|
||||||
|
DINGTALK_RENAME_ATTRIBUTES = serializers.JSONField(
|
||||||
|
required=False, label=_('User attribute'),
|
||||||
|
help_text=_(
|
||||||
|
'User attribute mapping, where the `key` is the JumpServer user attribute name and the '
|
||||||
|
'`value` is the DingTalk service user attribute name'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
|
@ -15,7 +15,7 @@ class FeiShuSettingSerializer(serializers.Serializer):
|
||||||
FEISHU_RENAME_ATTRIBUTES = serializers.JSONField(
|
FEISHU_RENAME_ATTRIBUTES = serializers.JSONField(
|
||||||
required=False, label=_('User attribute'),
|
required=False, label=_('User attribute'),
|
||||||
help_text=_(
|
help_text=_(
|
||||||
"User attribute mapping, where the `key` is the CAS service user attribute name "
|
'User attribute mapping, where the `key` is the JumpServer user attribute name and the '
|
||||||
"and the `value` is the JumpServer user attribute name"
|
'`value` is the FeiShu service user attribute name'
|
||||||
)
|
)
|
||||||
)
|
)
|
|
@ -15,7 +15,7 @@ class LarkSettingSerializer(serializers.Serializer):
|
||||||
LARK_RENAME_ATTRIBUTES = serializers.JSONField(
|
LARK_RENAME_ATTRIBUTES = serializers.JSONField(
|
||||||
required=False, label=_('User attribute'),
|
required=False, label=_('User attribute'),
|
||||||
help_text=_(
|
help_text=_(
|
||||||
"User attribute mapping, where the `key` is the CAS service user attribute name "
|
'User attribute mapping, where the `key` is the JumpServer user attribute name and the '
|
||||||
"and the `value` is the JumpServer user attribute name"
|
'`value` is the Lark service user attribute name'
|
||||||
)
|
)
|
||||||
)
|
)
|
|
@ -13,3 +13,10 @@ class SlackSettingSerializer(serializers.Serializer):
|
||||||
SLACK_CLIENT_ID = serializers.CharField(max_length=256, required=True, label='Client ID')
|
SLACK_CLIENT_ID = serializers.CharField(max_length=256, required=True, label='Client ID')
|
||||||
SLACK_CLIENT_SECRET = EncryptedField(max_length=256, required=False, label='Client Secret')
|
SLACK_CLIENT_SECRET = EncryptedField(max_length=256, required=False, label='Client Secret')
|
||||||
SLACK_BOT_TOKEN = EncryptedField(max_length=256, required=False, label='Client bot Token')
|
SLACK_BOT_TOKEN = EncryptedField(max_length=256, required=False, label='Client bot Token')
|
||||||
|
SLACK_RENAME_ATTRIBUTES = serializers.JSONField(
|
||||||
|
required=False, label=_('User attribute'),
|
||||||
|
help_text=_(
|
||||||
|
'User attribute mapping, where the `key` is the JumpServer user attribute name and the '
|
||||||
|
'`value` is the Slack service user attribute name'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
|
@ -13,3 +13,10 @@ class WeComSettingSerializer(serializers.Serializer):
|
||||||
WECOM_AGENTID = serializers.CharField(max_length=256, required=True, label='App Agent ID')
|
WECOM_AGENTID = serializers.CharField(max_length=256, required=True, label='App Agent ID')
|
||||||
WECOM_SECRET = EncryptedField(max_length=256, required=False, label='App Secret')
|
WECOM_SECRET = EncryptedField(max_length=256, required=False, label='App Secret')
|
||||||
AUTH_WECOM = serializers.BooleanField(default=False, label=_('WeCom'))
|
AUTH_WECOM = serializers.BooleanField(default=False, label=_('WeCom'))
|
||||||
|
WECOM_RENAME_ATTRIBUTES = serializers.JSONField(
|
||||||
|
required=False, label=_('User attribute'),
|
||||||
|
help_text=_(
|
||||||
|
'User attribute mapping, where the `key` is the JumpServer user attribute name and the '
|
||||||
|
'`value` is the WeCom service user attribute name'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
|
@ -245,6 +245,26 @@ def construct_user_email(username, email, email_suffix=''):
|
||||||
return email or default
|
return email or default
|
||||||
|
|
||||||
|
|
||||||
|
def flatten_dict(d, parent_key='', sep='.'):
|
||||||
|
items = {}
|
||||||
|
for k, v in d.items():
|
||||||
|
new_key = f"{parent_key}{sep}{k}" if parent_key else k
|
||||||
|
if isinstance(v, dict):
|
||||||
|
items.update(flatten_dict(v, new_key, sep=sep))
|
||||||
|
else:
|
||||||
|
items[new_key] = v
|
||||||
|
return items
|
||||||
|
|
||||||
|
|
||||||
|
def map_attributes(default_profile, profile, attributes):
|
||||||
|
detail = default_profile
|
||||||
|
for local_name, remote_name in attributes.items():
|
||||||
|
value = profile.get(remote_name)
|
||||||
|
if value:
|
||||||
|
detail[local_name] = value
|
||||||
|
return detail
|
||||||
|
|
||||||
|
|
||||||
def get_current_org_members():
|
def get_current_org_members():
|
||||||
from orgs.utils import current_org
|
from orgs.utils import current_org
|
||||||
return current_org.get_members()
|
return current_org.get_members()
|
||||||
|
|
Loading…
Reference in New Issue