### 本次升级、修复,请使用最新版:

+ 升级Python版本为3.8
+ 升级Django到3.2
+ 修复用户名中使用\被转义的问题
+ 重写了dingding模块,因为dingding开发者平台接口鉴权的一些变动,之前的一些接口不能再使用,本次重写。
+ 重写了ad模块,修改账号的一些判断逻辑。
+ 重写了用户账号的格式兼容,现在用户账号可以兼容:username、DOMAIN\username、username@abc.com这三种格式。
+ 优化了整体的代码逻辑,去掉一些冗余重复的代码。
This commit is contained in:
向乐🌌
2021-04-23 15:37:54 +08:00
parent d8ac7552a6
commit bc04829070
1222 changed files with 57072 additions and 984 deletions

View File

@@ -1,35 +0,0 @@
# 编译代码
FROM python:3.6.13-slim as stage-build
MAINTAINER Xiangle0109@outlook.com
ARG VERSION
ENV VERSION=1.0
WORKDIR /opt/password-self-service
ADD ./ad-password.tar.gz ./
ARG PIP_MIRROR=https://pypi.douban.com/simple
ENV PIP_MIRROR=$PIP_MIRROR
WORKDIR /opt/password-self-service
RUN sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list \
&& sed -i 's/security.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list \
&& apt update \
&& grep -v '^#' ./docker-src/deb_requirement | xargs apt -y install \
&& rm -rf /var/lib/apt/lists/* \
&& localedef -c -f UTF-8 -i zh_CN zh_CN.UTF-8 \
&& cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
RUN pip install --upgrade pip==20.2.4 setuptools==49.6.0 wheel==0.34.2 -i ${PIP_MIRROR} \
&& pip config set global.index-url ${PIP_MIRROR} \
&& pip install --no-cache-dir -r ./docker-src/requirement
VOLUME /opt/password-self-service/log
ENV LANG=zh_CN.UTF-8
EXPOSE 8070
EXPOSE 8080
ENTRYPOINT ["./entrypoint.sh"]

View File

@@ -138,30 +138,31 @@ fi
##install python3
##如果之前用此脚本安装过python3后续就不会再次安装。
if [[ -f "/usr/share/python-3.6.9/bin/python3" ]]
python_ver='3.8.9'
if [[ -f "/usr/share/python-${python_ver}/bin/python3" ]]
then
echo "己发现Python3将不会安装。"
else
if [[ -f "Python-3.6.9.tar.xz" ]]
if [[ -f "Python-${python_ver}.tar.xz" ]]
then
echo "将安装Python3.6.9"
tar xf Python-3.6.9.tar.xz
cd Python-3.6.9
sudo ./configure --prefix=/usr/share/python-3.6.9 && make && make install
echo "将安装Python${python_ver}"
tar xf Python-${python_ver}.tar.xz
cd Python-${python_ver}
sudo ./configure --prefix=/usr/share/python-${python_ver} && make && make install
else
echo "脚本目录下没有发现Python3.6.9.tar.xz将会下载python 3.6.9"
sudo wget https://www.python.org/ftp/python/3.6.9/Python-3.6.9.tar.xz
tar xf Python-3.6.9.tar.xz
cd Python-3.6.9
sudo ./configure --prefix=/usr/share/python-3.6.9 && make && make install
echo "脚本目录下没有发现Python${python_ver}.tar.xz将会下载python ${python_ver}"
sudo wget https://www.python.org/ftp/python/${python_ver}/Python-${python_ver}.tar.xz
tar xf Python-${python_ver}.tar.xz
cd Python-${python_ver}
sudo ./configure --prefix=/usr/share/python-${python_ver} && make && make install
fi
if [[ $? -eq 0 ]]
then
echo "创建python3和pip3的软件链接"
cd ${SHELL_FOLDER}
sudo ln -svf /usr/share/python-3.6.9/bin/python3 /usr/bin/python3
sudo ln -svf /usr/share/python-3.6.9/bin/pip3 /usr/bin/pip3
sudo ln -svf /usr/share/python-${python_ver}/bin/python3 /usr/bin/python3
sudo ln -svf /usr/share/python-${python_ver}/bin/pip3 /usr/bin/pip3
echo "======================================================================="
echo "Python3 安装成功!"
echo "======================================================================="

View File

View File

@@ -1,255 +0,0 @@
#!/bin/bash
echo -e "此脚本为Docker快速部署脚本"
##Check IP
function check_ip() {
local IP=$1
VALID_CHECK=$(echo $IP|awk -F. '$1<=255&&$2<=255&&$3<=255&&$4<=255{print "yes"}')
if echo $IP|grep -E "^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$" >/dev/null; then
if [[ $VALID_CHECK == "yes" ]]; then
return 0
else
return 1
fi
else
return 1
fi
}
##Check domain
function check_domain() {
local DOMAIN=$1
if echo $DOMAIN |grep -P "(?=^.{4,253}$)(^(?:[a-zA-Z0-9](?:(?:[a-zA-Z0-9\-]){0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$)" >/dev/null; then
return 0
else
return 1
fi
}
##Check Port
function check_port() {
local PORT=$1
VALID_CHECK=$(echo $PORT|awk '$1<=65535&&$1>=1{print "yes"}')
if echo $PORT |grep -E "^([1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]{1}|6553[0-5])$" >/dev/null; then
if [[ $VALID_CHECK == "yes" ]]; then
return 0
else
return 1
fi
else
return 1
fi
}
while :; do echo
echo "请确认你此台服务器是全新干净的,以防此脚本相关操作对正在运行的服务造成影响(不可逆)。"
read -p "请确认是否继续执行,输入 [y/n]: " ensure_yn
if [[ ! "${ensure_yn}" =~ ^[y,n]$ ]]; then
echo "输入有误,请输入 y 或 n"
else
break
fi
done
if [[ "${ensure_yn}" = n ]]; then
exit 0
fi
echo "======================================================================="
while :; do echo
read -p "请输入密码自助平台使用的本机IP: " PWD_SELF_SERVICE_IP
check_ip ${PWD_SELF_SERVICE_IP}
if [[ $? -ne 0 ]]; then
echo "---输入的IP地址格式有误请重新输入。"
else
break
fi
done
echo "======================================================================="
while :; do echo
read -p "请输入密码自助平台使用的端口: " PWD_SELF_SERVICE_PORT
check_port ${PWD_SELF_SERVICE_PORT}
if [[ $? -ne 0 ]]; then
echo "---输入的端口有误,请重新输入。"
else
break
fi
done
echo "======================================================================="
while :; do echo
read -p "请输入密码自助平台使用域名例如pwd.abc.com: " PWD_SELF_SERVICE_DOMAIN
check_domain ${PWD_SELF_SERVICE_DOMAIN}
if [[ $? -ne 0 ]]; then
echo "---输入的域名格式有误,请重新输入。"
else
break
fi
done
##当前脚本的绝对路径
SHELL_FOLDER=$(dirname $(readlink -f "$0"))
echo "关闭SELINUX"
sudo setenforce 0
sudo sed -i 's@SELINUX=*@SELINUX=disabled@g' /etc/selinux/config
echo "DONE....."
echo "关闭防火墙"
sudo systemctl disable firewalld
sudo systemctl stop firewalld
echo "DONE....."
echo "初始化编译环境----------"
sudo yum install gcc patch libffi-devel python-devel zlib-devel bzip2-devel openssl-devel ncurses-devel sqlite-devel readline-devel tk-devel gdbm-devel db4-devel libpcap-devel xz-devel wget psmisc -y
echo "======================================================================="
echo "初始化编译环境完成"
echo "======================================================================="
##Quick install nginx
echo "======================================================================="
echo "安装 Nginx"
sudo cat << EOF > /etc/yum.repos.d/nginx.repo
[nginx-stable]
name=nginx stable repo
baseurl=http://nginx.org/packages/centos/7/\$basearch/
gpgcheck=1
enabled=1
gpgkey=https://nginx.org/keys/nginx_signing.key
module_hotfixes=true
EOF
sudo yum makecache fast
sudo yum install nginx -y
if [[ $? -eq 0 ]]
then
sudo systemctl enable nginx
sudo systemctl start nginx
echo "======================================================================="
echo "nginx 安装成功!"
echo "======================================================================="
else
echo "======================================================================="
echo "nginx 安装失败!"
echo "======================================================================="
exit 1
fi
##install python3
##如果之前用此脚本安装过python3后续就不会再次安装。
if [[ -f "/usr/share/python-3.6.9/bin/python3" ]]
then
echo "己发现Python3将不会安装。"
else
if [[ -f "Python-3.6.9.tar.xz" ]]
then
echo "将安装Python3.6.9"
tar xf Python-3.6.9.tar.xz
cd Python-3.6.9
sudo ./configure --prefix=/usr/share/python-3.6.9 && make && make install
else
echo "脚本目录下没有发现Python3.6.9.tar.xz将会下载python 3.6.9"
sudo wget https://www.python.org/ftp/python/3.6.9/Python-3.6.9.tar.xz
tar xf Python-3.6.9.tar.xz
cd Python-3.6.9
sudo ./configure --prefix=/usr/share/python-3.6.9 && make && make install
fi
if [[ $? -eq 0 ]]
then
echo "创建python3和pip3的软件链接"
cd ${SHELL_FOLDER}
sudo ln -svf /usr/share/python-3.6.9/bin/python3 /usr/bin/python3
sudo ln -svf /usr/share/python-3.6.9/bin/pip3 /usr/bin/pip3
echo "======================================================================="
echo "Python3 安装成功!"
echo "======================================================================="
else
echo "======================================================================="
echo "Python3 安装失败!"
echo "======================================================================="
exit 1
fi
fi
##修改PIP源为国内
mkdir -p ~/.pip
cat << EOF > ~/.pip/pip.conf
[global]
index-url = https://pypi.tuna.tsinghua.edu.cn/simple
[install]
trusted-host=pypi.tuna.tsinghua.edu.cn
EOF
cd ${SHELL_FOLDER}
echo "====升级pip================"
/usr/bin/pip3 install --upgrade pip
/usr/bin/pip3 install -r requestment
if [[ $? -eq 0 ]]
then
echo "======================================================================="
echo "Pip3 requestment 安装成功!"
echo "======================================================================="
else
echo "======================================================================="
echo "Pip3 requestment 安装失败!"
echo "======================================================================="
exit 1
fi
##处理配置文件
echo "======================================================================="
echo "处理uwsgi.ini配置文件"
sed -i "s@PWD_SELF_SERVICE_HOME@${SHELL_FOLDER}@g" ${SHELL_FOLDER}/uwsgi.ini
sed -i "s@PWD_SELF_SERVICE_IP@${PWD_SELF_SERVICE_IP}@g" ${SHELL_FOLDER}/uwsgi.ini
sed -i "s@PWD_SELF_SERVICE_PORT@${PWD_SELF_SERVICE_PORT}@g" ${SHELL_FOLDER}/uwsgi.ini
echo "处理uwsgi.ini配置文件完成"
echo
echo "处理uwsgiserver启动脚本"
sed -i "s@PWD_SELF_SERVICE_HOME@${SHELL_FOLDER}@g" ${SHELL_FOLDER}/uwsgiserver
alias cp='cp'
cp -f ${SHELL_FOLDER}/uwsgiserver /etc/init.d/uwsgiserver
chmod +x /etc/init.d/uwsgiserver
chkconfig uwsgiserver on
echo "处理uwsgiserver启动脚本完成"
echo
sed -i "s@PWD_SELF_SERVICE_DOMAIN@${PWD_SELF_SERVICE_DOMAIN}@g" ${SHELL_FOLDER}/pwdselfservice/local_settings.py
##Nginx vhost配置
cat << EOF > /etc/nginx/conf.d/pwdselfservice.conf
server {
listen 80;
server_name ${PWD_SELF_SERVICE_DOMAIN} ${PWD_SELF_SERVICE_IP};
location / {
proxy_pass http://${PWD_SELF_SERVICE_IP}:${PWD_SELF_SERVICE_PORT};
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
}
access_log off;
}
EOF
rm -f /etc/nginx/conf.d/default.conf
systemctl restart nginx
echo
echo "======================================================================="
echo
echo "密码自助服务平台的访问地址是http://${PWD_SELF_SERVICE_DOMAIN}或http://${PWD_SELF_SERVICE_IP}"
echo "请确保以上域名能正常解析,否则使用域名无法访问。"
echo
echo "Uwsgi启动/etc/init.d/uwsgi start"
echo "Uwsgi停止/etc/init.d/uwsgi stop"
echo "Uwsgi重启/etc/init.d/uwsgi restart"
echo
echo
echo "文件${SHELL_FOLDER}/pwdselfservice/local_setting.py中必要参数需要你自行修改"
echo "此文件中有AD和钉钉的一些参数按自己企业的修改"
echo
echo "======================================================================="

View File

@@ -1,23 +0,0 @@
# common
gcc
cmake
curl
wget
vim
locales
iputils-ping
python3
nginx
# mysql-client
default-mysql-client
default-libmysqlclient-dev
openssl
libssl-dev
libldap2-dev
libsasl2-dev
libkrb5-dev
sqlite
sshpass

View File

@@ -1,28 +0,0 @@
[uwsgi]
http-socket = PWD_SELF_SERVICE_IP:PWD_SELF_SERVICE_PORT
chdir = PWD_SELF_SERVICE_HOME
module = pwdselfservice.wsgi:application
master = true
processes = 4
threads = 4
max-requests = 2000
chmod-socket = 755
vacuum = true
#设置缓冲
post-buffering = 4096
#设置静态文件
static-map = /static=PWD_SELF_SERVICE_HOME/static
#设置日志目录
daemonize = PWD_SELF_SERVICE_HOME/log/uwsgi.log

View File

@@ -1,50 +0,0 @@
#!/bin/bash
# Startup script for the uwsgi server
# chkconfig: - 85 15
# description: uwsgi server is Web Server
# HTML files and CGI.
# processname: uwsgiserver
INI="/opt/password-self-service/uwsgi.ini"
UWSGI="/usr/local/bin/uwsgi"
PSID=$(ps -ef | grep "password-self-service-uwsgi uWSGI master" | grep -v grep | awk '{print $2}')
if [ ! -n "$1" ]
then
content="Usages: $0 [start|stop|restart|status]"
echo -e "\033[31m $content \033[0m"
exit 0
fi
if [ $1 = start ]
then
if [[ `eval $PSID` -gt 4 ]]
then
content="uWsgi is Running!"
echo -e "\033[32m $content \033[0m"
exit 0
else
$UWSGI --ini $INI
content="Start uWsgi Service [OK]"
echo -e "\033[32m $content \033[0m"
fi
elif [ $1 = stop ];then
kill -9 $PSID > /dev/null 2>&1
content="Stop uWsgi Service [OK]"
echo -e "\033[32m $content \033[0m"
elif [ $1 = restart ];then
kill -9 $PSID > /dev/null 2>&1
echo "Pls wait...."
sleep 3s
$UWSGI --ini $INI
content="Restart uWsgi Service [OK]"
echo -e "\033[32m $content \033[0m"
elif [ $1 = status ];then
ps -ef | grep "password-self-service-uwsgi" | grep -v "grep"
else
content="Usages: $0 [start|stop|restart|status]"
echo -e "\033[31m $content \033[0m"
fi

33488
log/log.log

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,7 @@
#!/usr/bin/env python
import os
import sys
from utils.ad_ops import AdOps
if __name__ == '__main__':
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'pwdselfservice.settings')
@@ -12,4 +13,11 @@ if __name__ == '__main__':
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
try:
AdOps()
except Exception as e:
print(str(e))
print("未能连接到AD先决条件未满足Django不会运行..")
sys.exit(1)
execute_from_command_line(sys.argv)

View File

@@ -1,32 +1,42 @@
# AD配置修改为自己的
# AD主机可以是IP或主机域名例如可以是:abc.com或172.16.122.1
AD_HOST = '修改自己的'
# ########## AD配置修改为自己的
# AD主机可以是IP或主机域名例如可以是: abc.com或172.16.122.1
AD_HOST = r'修改自己的'
# 用于登录AD做用户信息验证的账号 需要有修改用户账号密码的权限。
# 账号格式使用DOMAIN\USERNAME例如abc\pwdadmin
AD_LOGIN_USER = '修改为自己的'
# AD域控的DOMAIN名例如abc、abc.com
AD_DOMAIN = r'修改成自己的'
# 用于登录AD做用户信息处理的账号需要有修改用户账号密码或信息的权限。
# AD账号例如pwdadmin
AD_LOGIN_USER = r'修改成自己的'
# 密码
AD_LOGIN_USER_PWD = '修改为自己的'
AD_LOGIN_USER_PWD = r'修改为自己的'
# BASE DN账号的查找DN路径例如'DC=abc,DC=com'可以指定到OU之下例如'OU=RD,DC=abc,DC=com'。
BASE_DN = '修改自己的'
BASE_DN = r'修改自己的'
# 是否启用SSL,
# 注意AD必须使用SSL才能修改密码这里被坑了N久...,自行部署下AD的证书服务并颁发CA证书重启服务器生效。具体教程百度一下有很多。
AD_USE_SSL = True
# 连接的端口如果启用SSL默认是636否则就是389
AD_CONN_PORT = 636
# ########## 钉钉
# 钉钉配置
# 钉钉接口地址,不可修改
DING_URL = "https://oapi.dingtalk.com/sns"
# 钉钉接口地址,不可修改
DING_URL = r'https://oapi.dingtalk.com'
# 钉钉企业ID修改为自己的
# 钉钉企业ID <CorpId>,修改为自己的
DING_CORP_ID = '修改为自己的'
# 钉钉E应用修改为自己的
DING_AGENT_ID = '修改为自己的'
DING_APP_KEY = '修改为自己的'
DING_APP_SECRET = '修改为自己的'
# 钉钉企业内部开发内部H5微应用或小程序用于读取企业内部用户信息
DING_AGENT_ID = r'修改为自己的'
DING_APP_KEY = r'修改为自己的'
DING_APP_SECRET = r'修改为自己的'
# 钉钉移动应用接入,修改为自己的
DING_SELF_APP_ID = '修改为自己的'
DING_SELF_APP_SECRET = '修改为自己的'
# 移动应用接入 主要为了实现通过扫码拿到用户的unioid
DING_MO_APP_ID = r'修改为自己的'
DING_MO_APP_SECRET = r'修改为自己的'
# 执行python3 ./resetpwd/utils/crypto.py 生成
# 可自行生成后替换
@@ -35,6 +45,5 @@ CRYPTO_KEY = b'dp8U9y7NAhCD3MoNwPzPBhBtTZ1uI_WWSdpNs6wUDgs='
# COOKIE 超时单位是秒,可不用修改
TMPID_COOKIE_AGE = 300
# 主页域名index.html中的钉钉跳转等需要指定域名。
# 主页域名钉钉跳转等需要指定域名格式pwd.abc.com。
HOME_URL = 'PWD_SELF_SERVICE_DOMAIN'

View File

@@ -15,15 +15,11 @@ import os
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/2.1/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'nxnm3#&2tat_c2i6%$y74a)t$(3irh^gpwaleoja1kdv30fmcm'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False
DEBUG = True
ALLOWED_HOSTS = ['*']
@@ -35,11 +31,12 @@ if not os.path.isdir(LOG_PATH):
LOGGING = {
'version': 1,
'disable_existing_loggers': False,#此选项开启表示禁用部分日志不建议设置为True
# 此选项开启表示禁用部分日志不建议设置为True
'disable_existing_loggers': False,
'formatters': {
'verbose': {
'format': '%(asctime)s %(levelname)s %(pathname)s %(module)s.%(funcName)s %(lineno)d: %(message)s'
#日志格式
# 日志格式
},
'simple': {
'format': '%(asctime)s %(levelname)s %(pathname)s %(module)s.%(funcName)s %(lineno)d: %(message)s'
@@ -47,7 +44,8 @@ LOGGING = {
},
'filters': {
'require_debug_true': {
'()': 'django.utils.log.RequireDebugTrue',#过滤器只有当setting的DEBUG = True时生效
# 过滤器只有当setting的DEBUG = True时生效
'()': 'django.utils.log.RequireDebugTrue',
},
},
'handlers': {
@@ -57,15 +55,18 @@ LOGGING = {
'class': 'logging.StreamHandler',
'formatter': 'verbose'
},
'file': {#重点配置部分
'file': {
'level': 'DEBUG',
'class': 'logging.FileHandler',
'filename': '%s/log.log' % LOG_PATH,#日志保存文件
'formatter': 'verbose'#日志格式,与上边的设置对应选择
# 日志保存文件
'filename': '%s/log.log' % LOG_PATH,
# 日志格式,与上边的设置对应选择
'formatter': 'verbose'
}
},
'loggers': {
'django': {#日志记录器
'django': {
# 日志记录器
'handlers': ['file'],
'level': 'DEBUG',
'propagate': True,
@@ -85,8 +86,6 @@ SESSION_EXPIRE_AT_BROWSER_CLOSE = True
# SESSION_COOKIE_HTTPONLY= True
# Application definition
INSTALLED_APPS = [
# 'django.contrib.admin',
'django.contrib.auth',
@@ -140,9 +139,6 @@ WSGI_APPLICATION = 'pwdselfservice.wsgi.application'
# }
# Password validation
# https://docs.djangoproject.com/en/2.1/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
@@ -158,10 +154,6 @@ AUTH_PASSWORD_VALIDATORS = [
},
]
# Internationalization
# https://docs.djangoproject.com/en/2.1/topics/i18n/
LANGUAGE_CODE = 'zh-hans'
TIME_ZONE = 'Asia/Shanghai'
@@ -170,15 +162,10 @@ USE_I18N = True
USE_L10N = True
USE_TZ = False
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.1/howto/static-files/
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
USE_TZ = True
STATIC_URL = '/static/'
STATIC_ROOT = 'static'
# STATIC_ROOT = 'static'
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'static'),

View File

@@ -4,9 +4,9 @@ import resetpwd.views
urlpatterns = {
path("favicon.ico", RedirectView.as_view(url='static/img/favicon.ico')),
path('', resetpwd.views.resetpwd_index, name='index'),
path('resetcheck', resetpwd.views.resetpwd_check_userinfo, name='resetcheck'),
path('resetpwd', resetpwd.views.resetpwd_reset, name='resetpwd'),
path('resetunlock', resetpwd.views.resetpwd_unlock, name='resetunlock'),
path('resetmsg', resetpwd.views.reset_msg, name='resetmsg'),
path('', resetpwd.views.index, name='index'),
path('callbackCheck', resetpwd.views.callback_check, name='callbackCheck'),
path('resetPassword', resetpwd.views.reset_pwd_by_ding_callback, name='resetPassword'),
path('unlockAccount', resetpwd.views.unlock_account, name='unlockAccount'),
path('messages', resetpwd.views.messages, name='messages'),
}

135
readme.md
View File

@@ -1,57 +1,61 @@
# 初学Django时碰到的一个需求因为公司中很多员工在修改密码之后有一些关联的客户端或网页中的旧密码没有更新导致密码在尝试多次之后账号被锁为了减少这种让人头疼的重置解锁密码的操蛋工作自己做了一个自助修改小平台。
## 水平有限,代码写得不好,但是能用,有需要的可以直接拿去用。
### 初学Django时碰到的一个需求因为公司中很多员工在修改密码之后有一些关联的客户端或网页中的旧密码没有更新导致密码在尝试多次之后账号被锁为了减少这种让人头疼的重置解锁密码的操蛋工作自己做了一个自助修改小平台。
### 水平有限,代码写得不好,但是能用,有需要的可以直接拿去用。
#### 场景说明:
因为本公司AD是早期已经在用用户的个人信息不是十分全面例如:用户手机号。
钉钉是后来才开始使用,钉钉默认是使用手机号登录。
这样就造成如果通过手机号来进行钉钉与AD之间的验证视乎行不通。
在这里我就使用了通过扫码后提取钉钉账号的邮箱信息再将邮箱在AD中进行比对来验证用户(邮箱)是否同时在企业的钉钉和企业AD中同时存在并账号状态是激活的。
钉钉是后来才开始使用,钉钉默认是使用手机号登录。
用户自行重置密码时如果通过手机号来进行钉钉与AD之间的验证行不通
### 新版本逻辑:
>用户扫码通过之后通过临时授权码提取用户的unionid再通过unionid判断用户在本企业中是否存在。如果存在提取用户钉钉账号的邮箱通过邮箱转成账号将账号拿到AD中进行比对来验证账号在AD中是否存在并账号状态是激活的。满足以上条件的账号就会视为可自行重置密码。
此处的配置可按自己的实际情况修改。
整个验证逻辑写在resetpwd/views.py
提示:
如果使用中提示无法连接到域控可以修改下resetpwd\utils\ad.py文件
```python
def __ad_connect():
"""
AD连接器
:return:
"""
username = str(AD_LOGIN_USER).lower()
server = Server(host=AD_HOST, use_ssl=True, port=636, get_info='ALL')
try:
conn = Connection(server, auto_bind=True, user=username, password=AD_LOGIN_USER_PWD, authentication='NTLM')
return conn
except Exception:
raise Exception('Server Error. Could not connect to Domain Controller')
### 提示:
```
AD必须使用SSL才能修改密码这里被坑了N久...
自行部署下AD的证书服务并颁发CA证书重启服务器生效。
具体教程百度一下,有很多。
```
把上面代码中的use_ssl=True改成use_ssl=False
### 本次升级、修复,请使用最新版:
+ 升级Python版本为3.8
+ 升级Django到3.2
+ 修复用户名中使用\被转义的问题
+ 重写了dingding模块因为dingding开发者平台接口鉴权的一些变动之前的一些接口不能再使用本次重写。
+ 重写了ad模块修改账号的一些判断逻辑。
+ 重写了用户账号的格式兼容现在用户账号可以兼容username、DOMAIN\username、username@abc.com这三种格式。
+ 优化了整体的代码逻辑,去掉一些冗余重复的代码。
## 线上环境需要的基础环境:
+ Python 3.8.9 (可自行下载源码包放到项目目录下,使用一键安装)
+ Nginx
+ Uwsgi
## 截图
![截图1](screenshot/Snipaste_2019-07-15_20-05-49.jpg)
![截图2](screenshot/Snipaste_2019-07-15_20-06-14.jpg)
## 线上环境需要的基础环境:
+ Python 3.6.x
* Nginx
* Uwsgi
## 钉钉必要条件:
#### E应用配置
* 在钉钉工作台中通过“自建应用”创建应用,选择“企业内部自主开发”在应用首页中获取应用的AgentId、AppKey、AppSecret。
#### 创建企业内部应用
* 在钉钉工作台中通过“自建应用”创建应用,选择“企业内部开发”,创建H5微应用或小程序在应用首页中获取应用的AgentId、AppKey、AppSecret。
* 应用需要权限:身份验证、消息通知、通讯录只读权限、手机号码信息、邮箱等个人信息、智能人事,范围是全部员工或自行选择
* 应用安全域名和IP一定要配置否则无法返回接口数据。
#### 移动接入应用
* 登录中开启扫码登录配置回调域名“https://pwd.abc.com/resetcheck”
其中pwd.abc.com请按自己实际域名来并记录相关的appId、appSecret。
参考截图配置
![截图3](screenshot/h5微应用.png)
![截图4](screenshot/h5微应用--开发管理.png)
![截图5](screenshot/h5微应用--权限管理.png)
#### 移动接入应用--登录权限:
>登录中开启扫码登录配置回调域名“https://pwd.abc.com/callbackCheck”
其中pwd.abc.com请按自己实际域名来并记录相关的appId、appSecret。
参考截图配置:
![截图6](screenshot/移动应用接入--登录.png)
# 使用脚本自动快速部署只适合Centos其它发行版本的Linux请自行修改相关命令。
## 我添加了一个快速自动部署脚本,可快速自动部署完成当前项目上线。
### 使用脚本自动快速部署只适合Centos其它发行版本的Linux请自行修改相关命令。
#### 我添加了一个快速自动部署脚本,可快速自动部署完成当前项目上线。
把整个项目目录上传到新的服务器上
```shell
chmod +x auto-install.sh
@@ -113,51 +117,60 @@ HOME_URL = 'PWD_SELF_SERVICE_DOMAIN'
修改pwdselfservice/local_settings.py中的参数按自己的实际参数修改
```` python
# AD配置修改为自己的
# AD主机可以是IP或主机域名例如可以是:abc.com或172.16.122.1
AD_HOST = '修改自己的'
# ########## AD配置修改为自己的
# AD主机可以是IP或主机域名例如可以是: abc.com或172.16.122.1
AD_HOST = r'修改自己的'
# 用于登录AD做用户信息验证的账号 需要有修改用户账号密码的权限。
# 账号格式使用DOMAIN\USERNAME例如abc\pwdadmin
AD_LOGIN_USER = '修改为自己的'
# AD域控的DOMAIN名例如abc、abc.com
AD_DOMAIN = r'修改成自己的'
# 用于登录AD做用户信息处理的账号需要有修改用户账号密码或信息的权限。
# AD账号例如pwdadmin
AD_LOGIN_USER = r'修改成自己的'
# 密码
AD_LOGIN_USER_PWD = '修改为自己的'
AD_LOGIN_USER_PWD = r'修改为自己的'
# BASE DN账号的查找DN路径例如'DC=abc,DC=com'可以指定到OU之下例如'OU=RD,DC=abc,DC=com'。
BASE_DN = '修改自己的'
BASE_DN = r'修改自己的'
# 是否启用SSL,
# 注意AD必须使用SSL才能修改密码这里被坑了N久...,自行部署下AD的证书服务并颁发CA证书重启服务器生效。具体教程百度一下有很多。
AD_USE_SSL = True
# 连接的端口如果启用SSL默认是636否则就是389
AD_CONN_PORT = 636
# ########## 钉钉
# 钉钉配置
# 钉钉接口地址,不可修改
DING_URL = "https://oapi.dingtalk.com/sns"
# 钉钉接口地址,不可修改
DING_URL = r'https://oapi.dingtalk.com'
# 钉钉企业ID修改为自己的
# 钉钉企业ID <CorpId>,修改为自己的
DING_CORP_ID = '修改为自己的'
# 钉钉E应用修改为自己的
DING_AGENT_ID = '修改为自己的'
DING_APP_KEY = '修改为自己的'
DING_APP_SECRET = '修改为自己的'
# 钉钉企业内部开发内部H5微应用或小程序用于读取企业内部用户信息
DING_AGENT_ID = r'修改为自己的'
DING_APP_KEY = r'修改为自己的'
DING_APP_SECRET = r'修改为自己的'
# 钉钉移动应用接入,修改为自己的
DING_SELF_APP_ID = '修改为自己的'
DING_SELF_APP_SECRET = '修改为自己的'
# 移动应用接入 主要为了实现通过扫码拿到用户的unioid
DING_MO_APP_ID = r'修改为自己的'
DING_MO_APP_SECRET = r'修改为自己的'
# Crypty key 通过generate_key生成可不用修改
# 执行python3 ./resetpwd/utils/crypto.py 生成
# 可自行生成后替换
CRYPTO_KEY = b'dp8U9y7NAhCD3MoNwPzPBhBtTZ1uI_WWSdpNs6wUDgs='
# COOKIE 超时单位是秒,可不用修改
TMPID_COOKIE_AGE = 300
# 主页域名index.html中的钉钉跳转等需要指定域名。
# 主页域名钉钉跳转等需要指定域名格式pwd.abc.com。
HOME_URL = 'PWD_SELF_SERVICE_DOMAIN'
````
### 自行安装完python3之后使用python3目录下的pip3进行安装依赖
### 我自行安装的Python路径为/usr/local/python3
# 手动部署
#### 自行安装完python3之后使用python3目录下的pip3进行安装依赖
#### 我自行安装的Python路径为/usr/local/python3
项目目录下的requestment文件里记录了所依赖的相关python模块安装方法
* /usr/local/python3/bin/pip3 install -r requestment

View File

@@ -1,7 +1,6 @@
Django==2.1.8
dingtalk-sdk>=1.2.2
pycrypto>=2.6
cryptography
ldap3
Django==3.2
dingtalk-sdk==1.3.8
cryptography==3.4.7
ldap3==2.9
requests
uwsgi

View File

@@ -1,3 +0,0 @@
from django.contrib import admin
# Register your models here.

View File

@@ -17,7 +17,7 @@ class CheckForm(c_forms.Form):
)
old_password = c_fields.CharField(error_messages={'required': '确认密码不能为空'})
ensure_password = c_fields.CharField(error_messages={'required': '确认密码不能为空'})
user_email = c_fields.CharField(error_messages={'required': '邮箱不能为空', 'invalid': '邮箱格式错误'})
username = c_fields.CharField(error_messages={'required': '账号不能为空', 'invalid': '账号格式错误'})
def clean(self):
pwd0 = self.cleaned_data.get('old_password')

View File

@@ -1,4 +0,0 @@
from django.db import models
from django import forms
from django.contrib import auth

View File

@@ -1,144 +0,0 @@
from ldap3 import *
from pwdselfservice.local_settings import *
def __ad_connect():
"""
AD连接器
:return:
"""
username = str(AD_LOGIN_USER).lower()
server = Server(host=AD_HOST, use_ssl=True, port=636, get_info='ALL')
try:
conn = Connection(server, auto_bind=True, user=username, password=AD_LOGIN_USER_PWD, authentication='NTLM')
return conn
except Exception:
raise Exception('Server Error. Could not connect to Domain Controller')
def ad_ensure_user_by_sam(username):
"""
通过sAMAccountName查询某个用户是否在AD中
:param username: 除去@domain.com 的部分
:return: True or False
"""
conn = __ad_connect()
base_dn = BASE_DN
condition = '(&(objectclass=person)(mail=' + username + '))'
attributes = ['sAMAccountName']
result = conn.search(base_dn, condition, attributes=attributes)
conn.unbind()
return result
def ad_ensure_user_by_mail(user_mail_addr):
"""
通过mail查询某个用户是否在AD中
:param user_mail_addr:
:return: True or False
"""
conn = __ad_connect()
base_dn = BASE_DN
condition = '(&(objectclass=person)(mail=' + user_mail_addr + '))'
attributes = ['mail']
result = conn.search(base_dn, condition, attributes=attributes)
conn.unbind()
return result
def ad_get_user_displayname_by_mail(user_mail_addr):
"""
通过mail查询某个用户的显示名
:param user_mail_addr:
:return: user_displayname
"""
conn = __ad_connect()
conn.search(BASE_DN, '(&(objectclass=person)(mail=' + user_mail_addr + '))', attributes=[
'displayName'])
user_displayname = conn.entries[0]['displayName']
conn.unbind()
return user_displayname
def ad_get_user_dn_by_mail(user_mail_addr):
"""
通过mail查询某个用户的完整DN
:param user_mail_addr:
:return: DN
"""
conn = __ad_connect()
conn.search(BASE_DN,
'(&(objectclass=person)(mail=' + user_mail_addr + '))', attributes=['distinguishedName'])
user_dn = conn.entries[0]['distinguishedName']
conn.unbind()
return user_dn
def ad_get_user_status_by_mail(user_mail_addr):
"""
通过mail查询某个用户的账号状态
:param user_mail_addr:
:return: user_account_control code
"""
conn = __ad_connect()
conn.search(BASE_DN,
'(&(objectclass=person)(mail=' + user_mail_addr + '))', attributes=['userAccountControl'])
user_account_control = conn.entries[0]['userAccountControl']
conn.unbind()
return user_account_control
def ad_unlock_user_by_mail(user_mail_addr):
"""
通过mail解锁某个用户
:param user_mail_addr:
:return:
"""
conn = __ad_connect()
user_dn = ad_get_user_dn_by_mail(user_mail_addr)
result = conn.extend.microsoft.unlock_account(user="%s" % user_dn)
conn.unbind()
return result
def ad_reset_user_pwd_by_mail(user_mail_addr, new_password):
"""
通过mail重置某个用户的密码
:param user_mail_addr:
:return:
"""
conn = __ad_connect()
user_dn = ad_get_user_dn_by_mail(user_mail_addr)
result = conn.extend.microsoft.modify_password(user="%s" % user_dn, new_password="%s" % new_password)
conn.unbind()
return result
def ad_modify_user_pwd_by_mail(user_mail_addr, old_password, new_password):
"""
通过mail修改某个用户的密码
:param user_mail_addr:
:return:
"""
conn = __ad_connect()
user_dn = ad_get_user_dn_by_mail(user_mail_addr)
result = conn.extend.microsoft.modify_password(user="%s" % user_dn, new_password="%s" % new_password,
old_password="%s" % old_password)
conn.unbind()
return result
def ad_get_user_locked_status_by_mail(user_mail_addr):
"""
通过mail获取某个用户账号是否被锁定
:param user_mail_addr:
:return: 如果结果是1601-01-01说明账号未锁定返回0
"""
conn = __ad_connect()
conn.search(BASE_DN, '(&(objectclass=person)(mail=' + user_mail_addr + '))', attributes=['lockoutTime'])
locked_status = conn.entries[0]['lockoutTime']
conn.unbind()
if '1601-01-01' in str(locked_status):
return 0
else:
return locked_status

View File

@@ -1,34 +0,0 @@
from django.forms import fields as c_fields
from django import forms as c_forms
from django.core.exceptions import ValidationError
class CheckForm(c_forms.Form):
new_password = c_fields.RegexField(
'(?=.*[0-9])(?=.*[a-zA-Z])(?=.*[^a-zA-Z0-9]).{8,30}',
# 密码必须同时包含大写、小写、数字和特殊字符其中三项且至少8位
strip=True,
min_length=8,
max_length=30,
error_messages={'required': '新密码不能为空.',
'invalid': '密码必须包含数字,字母、特殊字符',
'min_length': "密码长度不能小于8个字符",
'max_length': "密码长度不能大于30个字符"}
)
old_password = c_fields.CharField(error_messages={'required': '确认密码不能为空'})
ensure_password = c_fields.CharField(error_messages={'required': '确认密码不能为空'})
user_email = c_fields.CharField(error_messages={'required': '邮箱不能为空', 'invalid': '邮箱格式错误'})
def clean(self):
pwd0 = self.cleaned_data.get('old_password')
pwd1 = self.cleaned_data.get('new_password')
pwd2 = self.cleaned_data.get('ensure_password')
if pwd1 == pwd2:
pass
elif pwd0 == pwd1:
# 这里异常模块导入要放在函数里面,放到文件开头有时会报错,找不到
from django.core.exceptions import ValidationError
raise ValidationError('新旧密码不能一样')
else:
from django.core.exceptions import ValidationError
raise ValidationError('新密码和确认密码输入不一致')

View File

@@ -1,30 +0,0 @@
from django.shortcuts import render, reverse, HttpResponsePermanentRedirect, redirect
from django.http import *
from django.contrib import messages
from dingtalk import *
from resetpwd.models import *
from .crypto import Crypto
from resetpwd.utils.ad import ad_get_user_locked_status_by_mail, ad_unlock_user_by_mail, ad_reset_user_pwd_by_mail, \
ad_get_user_status_by_mail, ad_ensure_user_by_mail, ad_modify_user_pwd_by_mail
from .dingding import ding_get_userinfo_detail, ding_get_userid_by_unionid, ding_get_userinfo_by_code, \
ding_get_persistent_code, ding_get_access_token
from pwdselfservice.local_settings import *
from resetpwd.form import *
class CustomPasswortValidator(object):
def __init__(self, min_length=1, max_length=30):
self.min_length = min_length
def validate(self, password):
special_characters = "[~\!@#\$%\^&\*\(\)_\+{}\":;'\[\]]"
if not any(char.isdigit() for char in password):
raise ValidationError(_('Password must contain at least %(min_length)d digit.') % {'min_length': self.min_length})
if not any(char.isalpha() for char in password):
raise ValidationError(_('Password must contain at least %(min_length)d letter.') % {'min_length': self.min_length})
if not any(char in special_characters for char in password):
raise ValidationError(_('Password must contain at least %(min_length)d special character.') % {'min_length': self.min_length})
def get_help_text(self):
return ""

View File

@@ -1,129 +1,89 @@
from django.shortcuts import render
from django.http import *
from resetpwd.utils.crypto import Crypto
from resetpwd.utils.ad import ad_get_user_locked_status_by_mail, ad_unlock_user_by_mail, ad_reset_user_pwd_by_mail, \
ad_get_user_status_by_mail, ad_ensure_user_by_mail, ad_modify_user_pwd_by_mail
from resetpwd.utils.dingding import ding_get_userinfo_detail, ding_get_userid_by_unionid, \
ding_get_persistent_code, ding_get_access_token
from pwdselfservice.local_settings import *
from .form import CheckForm
import logging
import sys
msg_template = 'msg.html'
from django.http import *
from django.shortcuts import render
from utils.ad_ops import *
from utils.crypto import Crypto
from utils.dingding_ops import *
from utils.format_username import format2username
from .form import CheckForm
msg_template = 'messages.html'
logger = logging.getLogger('django')
ad_ops = AdOps()
ding_ops = DingDingOps()
def resetpwd_index(request):
def index(request):
"""
用户修改密码
用户自行修改密码
:param request:
:return:
"""
home_url = '%s://%s' % (request.scheme, HOME_URL)
app_id = DING_SELF_APP_ID
app_id = DING_MO_APP_ID
if request.method == 'GET':
return render(request, 'index.html', locals())
else:
logger.error('[异常] 请求方法:%s,请求路径:%s' % (request.method, request.path))
if request.method == 'POST':
# 对前端提交的数据进行二次验证,防止恶意提交简单密码或改账号。
# 对前端提交的数据进行二次验证,防止恶意提交简单密码或改账号。
check_form = CheckForm(request.POST)
if check_form.is_valid():
form_obj = check_form.cleaned_data
user_email = form_obj.get("user_email")
username = form_obj.get("username")
old_password = form_obj.get("old_password")
new_password = form_obj.get("new_password")
else:
msg = check_form.as_p().errors
logger.error('[异常] 请求方法:%s,请求路径:%s,错误信息:%s' % (request.method, request.path, msg))
_msg = check_form.as_p().errors
logger.error('[异常] 请求方法:%s,请求路径:%s,错误信息:%s' % (request.method, request.path, _msg))
context = {
'msg': msg,
'msg': _msg,
'button_click': "window.location.href='%s'" % home_url,
'button_display': "返回主页"
}
return render(request, msg_template, context)
if user_email and old_password and new_password:
try:
# 判断账号是否被锁定
if ad_get_user_locked_status_by_mail(user_mail_addr=user_email) is not 0:
context = {
'msg': "此账号己被锁定,请先解锁账号。",
'button_click': "window.history.back()",
'button_display': "返回"
}
return render(request, msg_template, context)
# 514 66050是AD中账号被禁用的特定代码这个可以在微软官网查到。
if ad_get_user_status_by_mail(user_mail_addr=user_email) == 514 or ad_get_user_status_by_mail(
user_mail_addr=user_email) == 66050:
context = {
'msg': "此账号状态为己禁用请联系HR确认账号是否正确。",
'button_click': "window.location.href='%s'" % home_url,
'button_display': "返回主页"
}
return render(request, msg_template, context)
try:
result = ad_modify_user_pwd_by_mail(user_mail_addr=user_email, old_password=old_password,
new_password=new_password)
if result:
context = {
'msg': "密码己修改成功,请妥善保管密码。你可直接关闭此页面!",
'button_click': "window.location.href='%s'" % home_url,
'button_display': "返回主页"
}
return render(request, msg_template, context)
else:
context = {
'msg': "密码未修改成功,请确认旧密码是否正确。",
'button_click': "window.history.back()",
'button_display': "返回"
}
return render(request, msg_template, context)
except IndexError:
context = {
'msg': "请确认邮箱账号[%s]是否正确未能在Active Directory中检索到相关信息。" % user_email,
'button_click': "window.location.href='%s'" % home_url,
'button_display': "返回主页"
}
return render(request, msg_template, context)
except Exception as e:
context = {
'msg': "出现未预期的错误[%s],请与管理员联系~" % str(e),
'button_click': "window.location.href='%s'" % home_url,
'button_display': "返回主页"
}
return render(request, msg_template, context)
except IndexError:
context = {
'msg': "请确认邮箱账号[%s]是否正确未能在Active Directory中检索到相关信息。" % user_email,
'button_click': "window.location.href='%s'" % home_url,
'button_display': "返回主页"
}
return render(request, msg_template, context)
except Exception as e:
context = {
'msg': "出现未预期的错误[%s],请与管理员联系~" % str(e),
'button_click': "window.history.back()",
'button_display': "返回"
}
return render(request, msg_template, context)
else:
# 格式化用户名
username = format2username(username)
# 检测账号状态
auth_status, auth_result = ad_ops.ad_auth_user(username=username, password=old_password)
if not auth_status:
context = {
'msg': "用户名、旧密码、新密码参数不正确,请重新确认后输入。",
'msg': str(auth_result),
'button_click': "window.history.back()",
'button_display': "返回"
}
return render(request, msg_template, context)
# 514 66050是AD中账号被禁用的特定代码这个可以在微软官网查到。
# 可能不是太准确
if ad_ops.ad_get_user_status_by_account(username) == 514 or ad_ops.ad_get_user_status_by_account(username) == 66050:
context = {
'msg': "此账号状态为己禁用请联系HR确认账号是否正确。",
'button_click': "window.location.href='%s'" % home_url,
'button_display': "返回主页"
}
return render(request, msg_template, context)
reset_status, reset_result = ad_ops.ad_reset_user_pwd_by_account(username=username, new_password=new_password)
if reset_status:
context = {
'msg': "密码己修改成功,新密码稍后生效,请妥善保管。您可直接关闭此页面!",
'button_click': "window.location.href='%s'" % home_url,
'button_display': "返回主页"
}
return render(request, msg_template, context)
else:
context = {
'msg': "密码未修改成功,原因:{}" .format(reset_result),
'button_click': "window.history.back()",
'button_display': "返回"
}
return render(request, msg_template, context)
else:
context = {
'msg': "请从主页进行修改密码操作或扫码验证用户信息。",
@@ -133,10 +93,9 @@ def resetpwd_index(request):
return render(request, msg_template, context)
def resetpwd_check_userinfo(request):
def callback_check(request):
"""
钉钉扫码回调数据对用户在AD中进行验证
扫码之后从钉钉中取出用户的unionid
钉钉扫码回调数据之后将用户账号在AD中进行验证如果通过则返回钉钉中取出用户的union_id
:param request:
:return:
"""
@@ -147,45 +106,48 @@ def resetpwd_check_userinfo(request):
else:
logger.error('[异常] 请求方法:%s,请求路径:%s未能拿到CODE。' % (request.method, request.path))
try:
unionid = ding_get_persistent_code(code, ding_get_access_token())
# 判断 unionid 在本企业钉钉中是否存在
if not unionid:
logger.error('[异常] 请求方法:%s,请求路径:%s未能拿到unionid。' % (request.method, request.path))
union_status, union_id = ding_ops.ding_get_union_id_by_code(code)
# 判断 union_id 在本企业钉钉中是否存在
if not union_status:
logger.error('[异常] 请求方法:%s,请求路径:%s未能拿到union_id。' % (request.method, request.path))
context = {
'msg': '未能在钉钉企业通讯录中检索到相关信息,请确认当前登录钉钉的账号已在企业中注册!',
'msg': '未能在企业钉钉中检索到用户信息,错误信息:{}' .format(union_id),
'button_click': "window.location.href='%s'" % home_url,
'button_display': "返回主页"
}
return render(request, msg_template, context)
ding_user_info = ding_get_userinfo_detail(ding_get_userid_by_unionid(unionid))
try:
# 钉钉中此账号是否可用
if ding_user_info['active']:
crypto = Crypto(CRYPTO_KEY)
# 对unionid进行加密因为unionid基本上固定不变的为了防止unionid泄露而导致重复使用进行加密后再传回。
unionid_cryto = crypto.encrypt(unionid)
# 配置cookie通过cookie把加密后的用户unionid传到重置密码页面并重定向到重置密码页面。
set_cookie = HttpResponseRedirect('resetpwd')
set_cookie.set_cookie('tmpid', unionid_cryto, expires=TMPID_COOKIE_AGE)
return set_cookie
else:
context = {
'msg': '邮箱是[%s]的用户在钉钉中未激活或可能己离职' % ding_user_info['email'],
'button_click': "window.location.href='%s'" % home_url,
'button_display': "返回主页"
}
return render(request, msg_template, context)
except IndexError:
userid_status, user_result = ding_ops.ding_get_userid_by_union_id(union_id)
if not userid_status:
context = {
'msg': "用户不存在或己离职",
'msg': '获取钉钉userid失败错误信息{}'.format(user_result),
'button_click': "window.location.href='%s'" % home_url,
'button_display': "返回主页"
}
return render(request, msg_template, context)
detail_status, ding_user_info = ding_ops.ding_get_userinfo_detail(user_result)
if not detail_status:
context = {
'msg': '获取钉钉用户信息失败,错误信息:{}'.format(ding_user_info),
'button_click': "window.location.href='%s'" % home_url,
'button_display': "返回主页"
}
return render(request, msg_template, context)
# 钉钉中此账号是否可用
if ding_user_info['active']:
crypto = Crypto(CRYPTO_KEY)
# 对union_id进行加密因为union_id基本上固定不变的为了防止union_id泄露而导致重复使用进行加密后再传回。
union_id_cryto = crypto.encrypt(union_id)
# 配置cookie通过cookie把加密后的用户union_id传到重置密码页面并重定向到重置密码页面。
set_cookie = HttpResponseRedirect('resetPassword')
set_cookie.set_cookie('tmpid', union_id_cryto, expires=TMPID_COOKIE_AGE)
return set_cookie
else:
context = {
'msg': '[%s]在钉钉中未激活或可能己离职' % format2username(ding_user_info['name']),
'button_click': "window.location.href='%s'" % home_url,
'button_display': "返回主页"
}
return render(request, msg_template, context)
except Exception as e:
logger.error('[异常] %s' % str(e))
except KeyError:
context = {
'msg': "错误钉钉临时Code己失效请从主页重新扫码。",
@@ -205,21 +167,22 @@ def resetpwd_check_userinfo(request):
return render(request, msg_template, context)
def resetpwd_reset(request):
def reset_pwd_by_ding_callback(request):
"""
钉钉扫码并验证信息之后,在重置密码页面将用户邮箱进行绑定
钉钉扫码并验证信息通过之后,在重置密码页面将用户账号进行绑定
:param request:
:return:
"""
global unionid_crypto
global union_id_crypto
home_url = '%s://%s' % (request.scheme, HOME_URL)
# 从cookie中提取unionid并解密然后对当前unionid的用户进行重置密码
# 从cookie中提取union_id并解密然后对当前union_id的用户进行重置密码
if request.method == 'GET':
try:
unionid_crypto = request.COOKIES.get('tmpid')
union_id_crypto = request.COOKIES.get('tmpid')
except Exception as e:
union_id_crypto = None
logger.error('[异常] %s' % str(e))
if not unionid_crypto:
if not union_id_crypto:
logger.error('[异常] 请求方法:%s,请求路径:%s未能拿到CODE或CODE己超时。' % (request.method, request.path))
context = {
'msg': "会话己超时,请重新扫码验证用户信息。",
@@ -229,20 +192,35 @@ def resetpwd_reset(request):
return render(request, msg_template, context)
# 解密
crypto = Crypto(CRYPTO_KEY)
unionid = crypto.decrypt(unionid_crypto)
# 通过unionid在钉钉中拿到用户的邮箱
user_email = ding_get_userinfo_detail(ding_get_userid_by_unionid(unionid))['email']
# 如果邮箱在钉钉中能提取,则提交到前端绑定
if user_email:
union_id = crypto.decrypt(union_id_crypto)
# 通过union_id在钉钉中拿到用户的邮箱并格式化为username
userid_status, user_result = ding_ops.ding_get_userid_by_union_id(union_id)
if not userid_status:
context = {
'user_email': user_email,
'msg': '获取钉钉userid失败错误信息{}'.format(user_result),
'button_click': "window.location.href='%s'" % home_url,
'button_display': "返回主页"
}
return render(request, 'resetpwd.html', context)
return render(request, msg_template, context)
detail_status, ding_user_info = ding_ops.ding_get_userinfo_detail(user_result)
if not detail_status:
context = {
'msg': '获取钉钉用户信息失败,错误信息:{}'.format(ding_user_info),
'button_click': "window.location.href='%s'" % home_url,
'button_display': "返回主页"
}
return render(request, msg_template, context)
username = format2username(ding_user_info['email'])
# 如果邮箱在钉钉中能提取到,则格式化之后,提取出账号提交到前端绑定
if username:
context = {
'username': username,
}
return render(request, 'resetPassword.html', context)
# 否则就是钉钉中此用户未配置邮箱,返回相关提示
else:
context = {
'msg': "%s 您好企业钉钉中未能找到您账号的邮箱配置请联系HR完善信息。" % ding_get_userinfo_detail(ding_get_userid_by_unionid(
unionid))['name'],
'msg': "%s您好企业钉钉中未能找到您账号的邮箱配置请联系HR完善信息。" % ding_user_info['name'],
'button_click': "window.location.href='%s'" % home_url,
'button_display': "返回主页"
}
@@ -250,10 +228,14 @@ def resetpwd_reset(request):
# 重置密码页面,输入新密码后点击提交
elif request.method == 'POST':
new_password = request.POST.get('new_password').strip()
unionid_crypto = request.COOKIES.get('tmpid')
# 对cookie中的unionid进行超时验证,如果页面超时就不再做处理。
if not unionid_crypto:
_new_password = request.POST.get('new_password').strip()
try:
union_id_crypto = request.COOKIES.get('tmpid')
except Exception as e:
union_id_crypto = None
logger.error('[异常] %s' % str(e))
if not union_id_crypto:
logger.error('[异常] 请求方法:%s,请求路径:%s未能拿到CODE或CODE己超时。' % (request.method, request.path))
context = {
'msg': "会话己超时,请重新扫码验证用户信息。",
'button_click': "window.location.href='%s'" % home_url,
@@ -261,17 +243,32 @@ def resetpwd_reset(request):
}
return render(request, msg_template, context)
crypto = Crypto(CRYPTO_KEY)
unionid = crypto.decrypt(unionid_crypto)
user_email = ding_get_userinfo_detail(ding_get_userid_by_unionid(unionid))['email']
if ad_ensure_user_by_mail(user_mail_addr=user_email) is False:
union_id = crypto.decrypt(union_id_crypto)
userid_status, user_result = ding_ops.ding_get_userid_by_union_id(union_id)
if not userid_status:
context = {
'msg': "账号[%s]在AD中不存在请确认当前钉钉扫码账号绑定的邮箱是否和您正在使用的邮箱一致或者该邮箱账号己被禁用\n猜测:您的邮箱是否是带有数字或其它字母区分?" % user_email,
'msg': '获取钉钉userid失败错误信息{}'.format(user_result),
'button_click': "window.location.href='%s'" % home_url,
'button_display': "返回主页"
}
return render(request, msg_template, context)
if ad_get_user_status_by_mail(user_mail_addr=user_email) == 514 or ad_get_user_status_by_mail(
user_mail_addr=user_email) == 66050:
detail_status, ding_user_info = ding_ops.ding_get_userinfo_detail(user_result)
if not detail_status:
context = {
'msg': '获取钉钉用户信息失败,错误信息:{}'.format(ding_user_info),
'button_click': "window.location.href='%s'" % home_url,
'button_display': "返回主页"
}
return render(request, msg_template, context)
username = format2username(ding_user_info['email'])
if ad_ops.ad_ensure_user_by_account(username) is False:
context = {
'msg': "账号[%s]在AD中不存在请确认当前钉钉扫码账号绑定的邮箱是否和您正在使用的邮箱一致或者该账号己被禁用\n猜测:您的账号或邮箱是否是带有数字或其它字母区分?" % username,
'button_click': "window.location.href='%s'" % home_url,
'button_display': "返回主页"
}
return render(request, msg_template, context)
if ad_ops.ad_get_user_status_by_account(username) == 514 or ad_ops.ad_get_user_status_by_account(username) == 66050:
context = {
'msg': "此账号状态为己禁用请联系HR确认账号是否正确。",
'button_click': "window.location.href='%s'" % home_url,
@@ -279,34 +276,20 @@ def resetpwd_reset(request):
}
return render(request, msg_template, context)
try:
result = ad_reset_user_pwd_by_mail(user_mail_addr=user_email, new_password=new_password)
if result:
# 重置密码并执行一次解锁,防止重置后账号还是锁定状态。
ad_unlock_user_by_mail(user_email)
reset_status, result = ad_ops.ad_reset_user_pwd_by_account(username=username, new_password=_new_password)
if reset_status:
# 重置密码并执行一次解锁,防止重置后账号还是锁定状态。
unlock_status, result = ad_ops.ad_unlock_user_by_account(username)
if unlock_status:
context = {
'msg': "密码己重置成功,请妥善保管。你可以点击返回主页或直接关闭此页面!",
'button_click': "window.location.href='%s'" % home_url,
'button_display': "返回主页"
}
return render(request, msg_template, context)
else:
context = {
'msg': "密码未重置成功确认密码是否满足AD的复杂性要求。",
'button_click': "window.location.href='%s'" % home_url,
'button_display': "返回主页"
}
return render(request, msg_template, context)
except IndexError:
else:
context = {
'msg': "请确认邮箱账号[%s]是否正确未能在AD中检索到相关信息。" % user_email,
'button_click': "window.location.href='%s'" % home_url,
'button_display': "返回主页"
}
return render(request, msg_template, context)
except Exception as e:
context = {
'msg': "出现未预期的错误[%s],请与管理员联系~" % str(e),
'msg': "密码未重置成功,错误信息:{}" .format(result),
'button_click': "window.location.href='%s'" % home_url,
'button_display': "返回主页"
}
@@ -320,7 +303,7 @@ def resetpwd_reset(request):
return render(request, msg_template, context)
def resetpwd_unlock(request):
def unlock_account(request):
"""
解锁账号
:param request:
@@ -328,8 +311,8 @@ def resetpwd_unlock(request):
"""
home_url = '%s://%s' % (request.scheme, HOME_URL)
if request.method == 'GET':
unionid_crypto = request.COOKIES.get('tmpid')
if not unionid_crypto:
_union_id_crypto = request.COOKIES.get('tmpid')
if not _union_id_crypto:
context = {
'msg': "会话己超时,请重新扫码验证用户信息。",
'button_click': "window.location.href='%s'" % home_url,
@@ -337,16 +320,32 @@ def resetpwd_unlock(request):
}
return render(request, msg_template, context)
crypto = Crypto(CRYPTO_KEY)
unionid = crypto.decrypt(unionid_crypto)
user_email = ding_get_userinfo_detail(ding_get_userid_by_unionid(unionid))['email']
union_id = crypto.decrypt(_union_id_crypto)
userid_status, user_result = ding_ops.ding_get_userid_by_union_id(union_id)
if not userid_status:
context = {
'msg': '获取钉钉userid失败错误信息{}'.format(user_result),
'button_click': "window.location.href='%s'" % home_url,
'button_display': "返回主页"
}
return render(request, msg_template, context)
detail_status, ding_user_info = ding_ops.ding_get_userinfo_detail(user_result)
if not detail_status:
context = {
'msg': '获取钉钉用户信息失败,错误信息:{}'.format(ding_user_info),
'button_click': "window.location.href='%s'" % home_url,
'button_display': "返回主页"
}
return render(request, msg_template, context)
username = format2username(ding_user_info['email'])
context = {
'user_email': user_email,
'username': username,
}
return render(request, 'resetpwd.html', context)
return render(request, 'resetPassword.html', context)
elif request.method == 'POST':
unionid_crypto = request.COOKIES.get('tmpid')
if not unionid_crypto:
_union_id_crypto = request.COOKIES.get('tmpid')
if not _union_id_crypto:
context = {
'msg': "会话己超时,请重新扫码验证用户信息。",
'button_click': "window.location.href='%s'" % home_url,
@@ -354,43 +353,44 @@ def resetpwd_unlock(request):
}
return render(request, msg_template, context)
crypto = Crypto(CRYPTO_KEY)
unionid = crypto.decrypt(unionid_crypto)
user_email = ding_get_userinfo_detail(ding_get_userid_by_unionid(unionid))['email']
if ad_ensure_user_by_mail(user_mail_addr=user_email) is False:
union_id = crypto.decrypt(_union_id_crypto)
userid_status, user_result = ding_ops.ding_get_userid_by_union_id(union_id)
if not userid_status:
context = {
'msg': "账号[%s]在AD中未能正确检索到请确认当前钉钉扫码账号绑定的邮箱是否和您正在使用的邮箱一致或者该邮箱账号己被禁用\n猜测:您的邮箱是否是带有数字或其它字母区分?" %
user_email,
'msg': '获取钉钉userid失败错误信息{}'.format(user_result),
'button_click': "window.location.href='%s'" % home_url,
'button_display': "返回主页"
}
return render(request, msg_template, context)
detail_status, ding_user_info = ding_ops.ding_get_userinfo_detail(user_result)
if not detail_status:
context = {
'msg': '获取钉钉用户信息失败,错误信息:{}'.format(ding_user_info),
'button_click': "window.location.href='%s'" % home_url,
'button_display': "返回主页"
}
return render(request, msg_template, context)
username = format2username(ding_user_info['email'])
if ad_ops.ad_ensure_user_by_account(username=username) is False:
context = {
'msg': "账号[%s]在AD中未能正确检索到请确认当前钉钉扫码账号绑定的邮箱是否和您正在使用的邮箱一致或者该账号己被禁用\n猜测:您的账号或邮箱是否是带有数字或其它字母区分?" %
username,
'button_click': "window.location.href='%s'" % home_url,
'button_display': "返回主页"
}
return render(request, msg_template, context)
else:
try:
result = ad_unlock_user_by_mail(user_email)
if result:
context = {
'msg': "账号己解锁成功。你可以点击返回主页或直接关闭此页面!",
'button_click': "window.location.href='%s'" % home_url,
'button_display': "返回主页"
}
return render(request, msg_template, context)
else:
context = {
'msg': "账号未能解锁请联系管理员确认该账号在AD的是否己禁用。",
'button_click': "window.location.href='%s'" % home_url,
'button_display': "返回主页"
}
return render(request, msg_template, context)
except IndexError:
unlock_status, result = ad_ops.ad_unlock_user_by_account(username)
if unlock_status:
context = {
'msg': "请确认邮箱账号[%s]是否正确未能在AD中检索到相关信息。" % user_email,
'msg': "账号己解锁成功。你可以点击返回主页或直接关闭此页面!",
'button_click': "window.location.href='%s'" % home_url,
'button_display': "返回主页"
}
return render(request, msg_template, context)
except Exception as e:
else:
context = {
'msg': "出现未预期的错误[%s],请与管理员联系~" % str(e),
'msg': "账号未能解锁,错误信息:{}" .format(result),
'button_click': "window.location.href='%s'" % home_url,
'button_display': "返回主页"
}
@@ -404,12 +404,12 @@ def resetpwd_unlock(request):
return render(request, msg_template, context)
def reset_msg(request):
msg = request.GET.get('msg')
def messages(request):
_msg = request.GET.get('msg')
button_click = request.GET.get('button_click')
button_display = request.GET.get('button_display')
context = {
'msg': msg,
'msg': _msg,
'button_click': button_click,
'button_display': button_display
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 KiB

BIN
screenshot/h5微应用.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

View File

@@ -22,13 +22,14 @@
// (?=.*[0-9])(?=.*[a-zA-Z]).{8,30} 大小写字母+数字
regex_mail = new RegExp('^\\w+((-\\w+)|(\\.\\w+))*\\@[A-Za-z0-9]+((\\.|-)[A-Za-z0-9]+)*\\.[A-Za-z0-9]+$')
regex_pwd = new RegExp('(?=.*[0-9])(?=.*[a-zA-Z])(?=.*[^a-zA-Z0-9]).{8,30}');
if ($.trim($('#user_email').val()) === '') {
alert('请输入邮箱账号');
return false;
} else if (!regex_mail.test($.trim($('#user_email').val()))) {
alert('请输入正确的邮箱账号。\n');
return false;
} else if ($.trim($('#old_password').val()) === '') {
//if ($.trim($('#user_email').val()) === '') {
// alert('请输入邮箱账号');
// return false;
//} else if (!regex_mail.test($.trim($('#user_email').val()))) {
// alert('请输入正确的邮箱账号。\n');
// return false;
//} else
if ($.trim($('#old_password').val()) === '') {
alert('请输入旧密码');
return false;
} else if ($.trim($('#new_password').val()) === '') {
@@ -57,13 +58,14 @@
$('#btn_reset').click(function () {
let regex_mail = new RegExp('^\\w+((-\\w+)|(\\.\\w+))*\\@[A-Za-z0-9]+((\\.|-)[A-Za-z0-9]+)*\\.[A-Za-z0-9]+$')
let regex_pwd = new RegExp('(?=.*[0-9])(?=.*[a-zA-Z])(?=.*[^a-zA-Z0-9]).{8,30}');
if ($.trim($('#user_email').val()) === '') {
alert('请输入邮箱账号');
return false;
} else if (!regex_mail.test($.trim($('#user_email').val()))) {
alert('请输入正确的邮箱账号。\n');
return false;
} else if ($.trim($('#new_password').val()) === '') {
//if ($.trim($('#user_email').val()) === '') {
//alert('请输入邮箱账号');
// return false;
//} else if (!regex_mail.test($.trim($('#user_email').val()))) {
// alert('请输入正确的邮箱账号。\n');
// return false;
//} else
if ($.trim($('#new_password').val()) === '') {
alert('请输入密码');
return false;
} else if ($.trim($('#ensure_password').val()) === '') {

View File

@@ -1,4 +1,4 @@
{% load staticfiles %}
{% load static %}
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>密码自助服务</title>
@@ -9,7 +9,7 @@
</head>
<body style="overflow: hidden">
<form name="resetcheck" method="post" action="resetpwd" autocomplete="off">
<form name="callbackCheck" method="post" action="resetPassword" autocomplete="off">
{% csrf_token %}
</form>
</body></html>

View File

@@ -1,11 +1,11 @@
{% load staticfiles %}
{% load static %}
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>密码自助服务</title>
<link type="text/css" rel="stylesheet" href="{% static 'css/login.css' %}">
<script type="text/javascript" src="{% static 'js/jquery-1.8.3.min.js' %}"></script>
<script type="text/javascript" src="{% static 'js/check.js' %}"></script>
<script src="//g.alicdn.com/dingding/dinglogin/0.0.5/ddLogin.js"></script>
<script src="https://g.alicdn.com/dingding/dinglogin/0.0.5/ddLogin.js"></script>
</head>
<body>
<div class="pagewrap">
@@ -15,8 +15,8 @@
</div>
<div class="content">
<div class="con_left" >
<div style="margin: 0 auto; width:100%; height: 200px; line-height: 200px;" align="center" >
<p style="margin: 0 auto; color: #fdfdfe; font-size: 36px; width:100%; ">「域账号/邮箱」<small>密码自助平台</small></p>
<div style="margin: 0 auto; width:100%; height: 200px; line-height: 200px;" align="center">
<p style="margin: 0 auto; color: #fdfdfe; font-size: 36px; width:100%; ">「域账号邮箱」<small>密码自助平台</small></p>
</div>
<div style="margin: 0 auto; width:400px; height: 240px;">
<p style="margin: 0 auto; color: #fdfdfe; font-size: 16px; width:100%;
@@ -36,7 +36,7 @@
{% csrf_token %}
<div class="user">
<div><span class="user-icon"></span>
<input type="text" id="user_email" name="user_email" placeholder=" 输入邮箱(例如user@abc.com)" value="">
<input type="text" id="username" name="username" placeholder="格式abc\lisi、lisi、lisi@abc.com" value="">
</div>
<div><span class="mima-icon"></span>
<input type="password" id="old_password" name="old_password"
@@ -59,12 +59,16 @@
<div style="margin-top: -30px" class="erweima">
<div style="width: 300px; height: 300px; margin: 0 auto" id="ding_code"></div>
<script type="text/javascript">
// 构造钉钉登录
// 扫描之后需要跳转的域名填写自己的修改密码的域名地址http或https
var home_url = "{{ home_url }}";
// 钉钉移动应用接入ID
var appid = "{{ app_id }}";
var url = encodeURIComponent(home_url + '/resetcheck');
var goto = encodeURIComponent('https://oapi.dingtalk.com/connect/oauth2/sns_authorize?appid=' + appid + '&response_type=code&scope=snsapi_login&state=STATE&redirect_uri=' + url);
var app_id = "{{ app_id }}";
var redirect_url = encodeURIComponent(home_url + '/callbackCheck');
var goto = encodeURIComponent('https://oapi.dingtalk.com/connect/qrconnect?appid='
+ app_id
+ '&response_type=code&scope=snsapi_login&state=STATE&redirect_uri='
+ redirect_url);
var obj = DDLogin({
id: "ding_code",
goto: goto,
@@ -73,15 +77,20 @@
height: "300"
});
// 获取loginTmpCode
const hanndleMessage = function (event) {
let origin = event.origin;
var hanndleMessage = function (event) {
var origin = event.origin;
console.log("origin", event.origin)
//判断是否来自ddLogin扫码事件。
if (origin === "https://login.dingtalk.com") {
let loginTmpCode = event.data;
var loginTmpCode = event.data;
console.log("loginTmpCode", loginTmpCode);
if (loginTmpCode) {
location.href = "https://oapi.dingtalk.com/connect/oauth2/sns_authorize?appid=" + appid + "&response_type=code&scope=snsapi_login&state=STATE&redirect_uri=" + url + "&loginTmpCode=" + loginTmpCode;
//拿到loginTmpCode后就可以在这里构造跳转链接进行跳转了
location.href = 'https://oapi.dingtalk.com/connect/oauth2/sns_authorize?appid='
+ app_id
+ '&response_type=code&scope=snsapi_login&state=STATE&redirect_uri='
+ redirect_url
+ '&loginTmpCode=' + loginTmpCode;
}
}
};

View File

@@ -1,6 +1,6 @@
{% load staticfiles %}
{% load static %}
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<html xmlns="http://www.w3.org/1999/xhtml" lang="zh"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>密码自助服务</title>
<link type="text/css" rel="stylesheet" href="{% static 'css/login.css' %}">
<script type="text/javascript" src="{% static 'js/jquery-1.8.3.min.js' %}"></script>
@@ -12,9 +12,8 @@
<div class="header"></div>
<div class="content">
<div class="con_left" >
<div style="margin: 0 auto; width:100%; height: 200px; line-height: 200px;" align="center" >
<p style="margin: 0 auto; color: #fdfdfe; font-size: 36px; width:100%; ">「域账号/邮箱」
<small>密码自助平台</small></p>
<div style="margin: 0 auto; width:100%; height: 200px; line-height: 200px;" align="center">
<p style="margin: 0 auto; color: #fdfdfe; font-size: 36px; width:100%; ">「域账号邮箱」<small>密码自助平台</small></p>
</div>
<div style="margin: 0 auto; width:400px; height: 240px;">
<p style="margin: 0 auto; color: #fdfdfe; font-size: 16px; width:100%;

View File

@@ -1,4 +1,4 @@
{% load staticfiles %}
{% load static %}
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>密码自助服务</title>
@@ -14,7 +14,7 @@
<div class="content">
<div class="con_left" >
<div style="margin: 0 auto; width:100%; height: 200px; line-height: 200px;" align="center" >
<p style="margin: 0 auto; color: #fdfdfe; font-size: 36px; width:100%; ">「域账号/邮箱」
<p style="margin: 0 auto; color: #fdfdfe; font-size: 36px; width:100%; ">「域账号邮箱」
<small>密码自助平台</small></p>
</div>
<div style="margin: 0 auto; width:400px; height: 240px;">
@@ -35,7 +35,7 @@
{% csrf_token %}
<div class="user">
<div><span class="user-icon"></span>
<input type="text" id="user_email" name="user_email" readonly placeholder="{{ user_email }}" value="{{ user_email }}">
<input type="text" id="username" name="username" readonly placeholder="{{ username }}" value="{{ username }}">
</div>
<div><span class="mima-icon"></span>
<input type="password" id="new_password" name="new_password"
@@ -55,7 +55,7 @@
{% csrf_token %}
<div class="user" style="height: 168px">
<div><span class="user-icon"></span>
<input type="text" id="user_email" name="user_email" readonly placeholder="{{ user_email }}" value="{{ user_email }}">
<input type="text" id="username" name="username" readonly placeholder="{{ username }}" value="{{ username }}">
</div>
<span class="msgs"></span>

213
utils/ad_ops.py Normal file
View File

@@ -0,0 +1,213 @@
from ldap3 import *
from ldap3.core.exceptions import LDAPInvalidCredentialsResult, LDAPOperationResult
from ldap3.core.results import *
from ldap3.utils.dn import safe_dn
from pwdselfservice.local_settings import *
"""
根据以下网站的说明:
https://docs.microsoft.com/zh-cn/troubleshoot/windows/win32/change-windows-active-directory-user-password
密码存储在 unicodePwd 属性中的用户对象的 AD 和 LDS 数据库中。 此属性可以在受限条件下写入,但无法读取。 只能修改属性;无法在对象创建时或由搜索查询时添加它。
为了修改此属性,客户端必须具有到服务器的 128 位传输层安全性 (TLS) /Secure Socket Layer (SSL) 连接。
使用 SSP 创建的会话密钥(使用 NTLM 或 Kerberos的加密会话也可接受只要达到最小密钥长度。
若要使用 TLS/SSL 实现此连接:
服务器必须拥有 128 位 RSA 连接的服务器证书。
客户端必须信任生成服务器证书 (CA) 证书颁发机构。
客户端和服务器都必须能够进行 128 位加密。
unicodePwd 属性的语法为 octet-string;但是,目录服务预期八进制字符串将包含 UNICODE 字符串 (,因为属性的名称指示) 。
这意味着在 LDAP 中传递的此属性的任何值都必须是 BER 编码的 UNICODE 字符串 (基本编码规则) 八进制字符串。
此外UNICODE 字符串必须以引号开头和结尾,这些引号不是所需密码的一部分。
可通过两种方法修改 unicodePwd 属性。 第一种操作类似于正常的 用户更改密码 操作。
在这种情况下,修改请求必须同时包含删除和添加操作。 删除操作必须包含当前密码,并包含其周围的引号。
添加操作必须包含所需的新密码,其周围必须有引号。
修改此属性的第二种方法类似于管理员重置用户密码。 为此,客户端必须以具有修改其他用户密码的足够权限的用户进行绑定。
此修改请求应包含单个替换操作,其中包含用引号括起的新所需密码。 如果客户端具有足够的权限,则无论旧密码是什么,此密码都将变为新密码。
"""
class AdOps(object):
def __init__(self, auto_bind=True, use_ssl=AD_USE_SSL, port=AD_CONN_PORT, domain=AD_DOMAIN, user=AD_LOGIN_USER, password=AD_LOGIN_USER_PWD,
authentication=NTLM):
"""
AD连接器 authentication [SIMPLE, ANONYMOUS, SASL, NTLM]
:return:
"""
self.use_ssl = use_ssl
self.port = port
self.domain = domain
self.user = user
self.password = password
self.authentication = authentication
self.auto_bind = auto_bind
server = Server(host='%s' % AD_HOST, 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)
except LDAPOperationResult as e:
raise LDAPOperationResult("LDAPOperationResult: " + str(e))
except Exception:
raise Exception('出现错误无法连接到AD控制器。')
def ad_auth_user(self, username, password):
"""
验证账号
:param username:
:param password:
:return: True or False
"""
try:
server = Server(host='%s' % AD_HOST, use_ssl=self.use_ssl, port=self.port, get_info=ALL)
c_auth = Connection(server=server, user=r'{}\{}'.format(self.domain, username), password=password, auto_bind=True, raise_exceptions=True)
c_auth.unbind()
return True, '旧密码验证通过。'
except LDAPInvalidCredentialsResult as e:
if '52e' in e.message:
return False, u'账号或旧密码不正确!'
elif '775' in e.message:
return False, u'账号已锁定,请自行扫码解锁!'
elif '533' in e.message:
return False, u'账号已禁用!'
elif '525' in e.message:
return False, u'账号不存在!'
elif '532' in e.message:
return False, u'密码己过期!'
elif '701' in e.message:
return False, u'账号己过期!'
elif '773' in e.message:
# 如果仅仅使用普通凭据来绑定ldap用途请返回False, 让用户通过其他途径修改密码后再来验证登陆
# return False, '用户登陆前必须修改密码!'
# 设置该账号下次登陆不需要更改密码,再验证一次
self.conn.search(search_base=BASE_DN, search_filter='(sAMAccountName={}))'.format(username), attributes=['pwdLastSet'])
self.conn.modify(self.conn.entries[0].entry_dn, {'pwdLastSet': [(MODIFY_REPLACE, ['-1'])]})
return self.ad_auth_user(username, password)
else:
return False, u'旧密码认证失败,请确认账号的旧密码是否正确或使用重置密码功能。'
def ad_ensure_user_by_account(self, username):
"""
通过username查询某个用户是否在AD中
:param username:
:return: True or False
"""
base_dn = BASE_DN
condition = '(&(objectclass=user)(sAMAccountName={}))'.format(username)
attributes = ['sAMAccountName']
return self.conn.search(base_dn, condition, attributes=attributes)
def ad_get_user_displayname_by_account(self, username):
"""
通过username查询某个用户的显示名
:param username:
:return: user_displayname
"""
self.conn.search(BASE_DN, '(&(objectclass=user)(sAMAccountName={}))'.format(username), attributes=['name'])
try:
return True, self.conn.entries[0]['name']
except Exception as e:
return False, str(e)
def ad_get_user_dn_by_account(self, username):
"""
通过mail查询某个用户的完整DN
:param username:
:return: DN
"""
self.conn.search(BASE_DN, '(&(objectclass=user)(sAMAccountName={}))'.format(username), attributes=['distinguishedName'])
return str(self.conn.entries[0]['distinguishedName'])
def ad_get_user_status_by_account(self, username):
"""
通过username查询某个用户的账号状态
:param username:
:return: user_account_control code
"""
self.conn.search(BASE_DN, '(&(objectclass=user)(sAMAccountName={}))'.format(username), attributes=['userAccountControl'])
return self.conn.entries[0]['userAccountControl']
def ad_unlock_user_by_account(self, username):
"""
通过username解锁某个用户
:param username:
:return:
"""
user_dn = self.ad_get_user_dn_by_account(username)
try:
return True, self.conn.extend.microsoft.unlock_account(user='%s' % user_dn)
except Exception as e:
return False, str(e)
def ad_reset_user_pwd_by_account(self, username, new_password):
"""
重置某个用户的密码
:param username:
:return:
"""
user_dn = self.ad_get_user_dn_by_account(username)
if self.conn.check_names:
user_dn = safe_dn(user_dn)
encoded_new_password = ('"%s"' % new_password).encode('utf-16-le')
result = self.conn.modify(user_dn,
{'unicodePwd': [(MODIFY_REPLACE, [encoded_new_password])]},
)
if not self.conn.strategy.sync:
_, result = self.conn.get_response(result)
else:
if self.conn.strategy.thread_safe:
_, result, _, _ = result
else:
result = self.conn.result
# change successful, returns True
if result['result'] == RESULT_SUCCESS:
return True, '密码己修改成功,请妥善保管!'
# change was not successful, raises exception if raise_exception = True in connection or returns the operation result, error code is in result['result']
if self.conn.raise_exceptions:
from ldap3.core.exceptions import LDAPOperationResult
_msg = LDAPOperationResult(result=result['result'], description=result['description'], dn=result['dn'], message=result['message'],
response_type=result['type'])
return False, _msg
return False, result['result']
def ad_get_user_locked_status_by_account(self, username):
"""
通过mail获取某个用户账号是否被锁定
:param username:
:return: 如果结果是1601-01-01说明账号未锁定返回0
"""
self.conn.search(BASE_DN, '(&(objectclass=user)(sAMAccountName={}))'.format(username), attributes=['lockoutTime'])
locked_status = self.conn.entries[0]['lockoutTime']
if '1601-01-01' in str(locked_status):
return 0
else:
return locked_status
if __name__ == '__main__':
# server = Server(host='%s' % AD_HOST, use_ssl=AD_USE_SSL, port=AD_CONN_PORT, get_info=ALL)
# conn = Connection(server, auto_bind=True, user=str(AD_LOGIN_USER).lower(), password=AD_LOGIN_USER_PWD, authentication=SIMPLE)
# # conn.bind()
# # conn.search(BASE_DN, '(&(objectclass=user)(sAMAccountName=xiangle))', attributes=['name'])
# # print(conn.entries[0])
# print(conn.result)
# conn = _ad_connect()
user = 'zhangsan'
old_password = 'K2dhhuT1Zf11111cnJ1ollC3y'
# old_password = 'L1qyrmZDUFeYW1OIualjlNhr4'
new_password = 'K2dhhuT1Zf11111cnJ1ollC3y'
ad_ops = AdOps()
# ad_ops = AdOps(user=user, password=old_password)
status, msg = ad_ops.ad_auth_user(username=user, password=old_password)
print(msg)
if status:
res = ad_ops.ad_reset_user_pwd_by_account(user, new_password)
print(res)

114
utils/dingding_ops.py Normal file
View File

@@ -0,0 +1,114 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals
import base64
import hmac
import time
from hashlib import sha256
from urllib.parse import quote
import requests
from dingtalk.client import AppKeyClient
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):
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
@property
def ding_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):
"""
获取企业内部应用的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
:param code:
:return:
"""
# token = self.ding_get_access_token
time_stamp = int(round(time.time() * 1000))
# 时间戳
# 通过appSecret计算出来的签名值该参数值在HTTP请求参数中需要urlEncode(因为签名中可能包含特殊字符+)。
signature = quote(base64.b64encode(hmac.new(
self.mo_app_secret.encode('utf-8'),
str(time_stamp).encode('utf-8'),
digestmod=sha256).digest()).decode("utf-8"))
# accessKey 是 登录开发者后台,选择应用开发 > 移动接入应用 > 登录所看到应用的appId。
url = '{}/sns/getuserinfo_bycode?accessKey={}&signature={}&timestamp={}'.format(DING_URL, self.mo_app_id, signature, time_stamp)
resp = requests.post(
url=url,
json=dict(tmp_auth_code=code),
)
resp = resp.json()
try:
return True, resp["user_info"]["unionid"]
except Exception as e:
return False, 'ding_get_union_id_by_code: {}' .format(e)
def ding_get_userid_by_union_id(self, union_id):
"""
通过unionid获取用户的userid
:param union_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_userid_by_union_id: {}' .format(e)
@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):
"""
通过user_id 获取用户详细信息
user_id 用户ID
:return:
"""
try:
return True, self.ding_client_connect.user.get(user_id)
except Exception as e:
return False, 'ding_get_userinfo_detail: {}' .format(e)
if __name__ == '__main__':
start = time.time()
d = DingDingOps()
print(d.ding_get_access_token)
# print(d.user.getuserinfo('2ecebee187863a8ea2863a7a2fa17b49'))
end = time.time()
print("running:" + str(round((end - start), 3)))

View File

@@ -0,0 +1,21 @@
"""
Created on 2018-9-17
@author: xiaoxuan.lp
"""
class appinfo(object):
def __init__(self, appkey, secret):
self.appkey = appkey
self.secret = secret
def getDefaultAppInfo():
pass
def setDefaultAppInfo(appkey, secret):
default = appinfo(appkey, secret)
global getDefaultAppInfo
getDefaultAppInfo = lambda: default

View File

@@ -0,0 +1,2 @@
from api.rest import *
from api.base import FileItem

View File

@@ -0,0 +1,332 @@
# -*- coding: utf-8 -*-
"""
Created on 2018-9-17
@author: xiaoxuan.lp
"""
try:
import http.client
except ImportError:
import http.client as httplib
import base64
import hashlib
import hmac
import itertools
import json
import mimetypes
import time
import urllib.error
import urllib.parse
import urllib.request
'''
定义一些系统变量
'''
SYSTEM_GENERATE_VERSION = "taobao-sdk-python-dynamicVersionNo"
P_APPKEY = "app_key"
P_API = "method"
P_ACCESS_TOKEN = "access_token"
P_VERSION = "v"
P_FORMAT = "format"
P_TIMESTAMP = "timestamp"
P_SIGN = "sign"
P_SIGN_METHOD = "sign_method"
P_PARTNER_ID = "partner_id"
P_CODE = 'errcode'
P_MSG = 'errmsg'
def sign(secret, parameters):
# ===========================================================================
# '''签名方法
# @param secret: 签名需要的密钥
# @param parameters: 支持字典和string两种
# '''
# ===========================================================================
# 如果parameters 是字典类的话
if hasattr(parameters, "items"):
keys = list(parameters.keys())
keys.sort()
parameters = "%s%s%s" % (secret,
str().join('%s%s' % (key, parameters[key]) for key in keys),
secret)
sign = hashlib.md5(parameters.encode("utf-8")).hexdigest().upper()
return sign
def mixStr(pstr):
if isinstance(pstr, str):
return pstr
elif isinstance(pstr, str):
return pstr.encode('utf-8')
else:
return str(pstr)
class FileItem(object):
def __init__(self, filename=None, content=None):
self.filename = filename
self.content = content
class MultiPartForm(object):
"""Accumulate the data to be used when posting a form."""
def __init__(self):
self.form_fields = []
self.files = []
self.boundary = "PYTHON_SDK_BOUNDARY"
return
def get_content_type(self):
return 'multipart/form-data;charset=UTF-8; boundary=%s' % self.boundary
def add_field(self, name, value):
"""Add a simple field to the form data."""
self.form_fields.append((name, str(value)))
return
def add_file(self, fieldname, filename, fileHandle, mimetype=None):
"""Add a file to be uploaded."""
body = fileHandle.read()
if mimetype is None:
mimetype = mimetypes.guess_type(filename)[0] or 'application/octet-stream'
self.files.append((mixStr(fieldname), mixStr(filename), mixStr(mimetype), mixStr(body)))
return
def __str__(self):
"""Return a string representing the form data, including attached files."""
# Build a list of lists, each containing "lines" of the
# request. Each part is separated by a boundary string.
# Once the list is built, return a string where each
# line is separated by '\r\n'.
parts = []
part_boundary = '--' + self.boundary
# Add the form fields
parts.extend(
[part_boundary,
'Content-Disposition: form-data; name="%s"' % name,
'Content-Type: text/plain; charset=UTF-8',
'',
value,
]
for name, value in self.form_fields
)
# Add the files to upload
parts.extend(
[part_boundary,
'Content-Disposition: form-data; name="%s"; filename="%s"' % (field_name, filename),
'Content-Type: %s' % content_type,
'Content-Transfer-Encoding: binary',
'',
body,
]
for field_name, filename, content_type, body in self.files
)
# Flatten the list and add closing boundary marker,
# then return CR+LF separated data
flattened = list(itertools.chain(*parts))
flattened.append('--' + self.boundary + '--')
flattened.append('')
return '\r\n'.join(flattened)
class TopException(Exception):
# ===========================================================================
# 业务异常类
# ===========================================================================
def __init__(self):
self.errcode = None
self.errmsg = None
self.application_host = None
self.service_host = None
def __str__(self, *args, **kwargs):
sb = "errcode=" + mixStr(self.errcode) + \
" errmsg=" + mixStr(self.errmsg) + \
" application_host=" + mixStr(self.application_host) + \
" service_host=" + mixStr(self.service_host)
return sb
class RequestException(Exception):
# ===========================================================================
# 请求连接异常类
# ===========================================================================
pass
class RestApi(object):
# ===========================================================================
# Rest api的基类
# ===========================================================================
def __init__(self, url=None):
# =======================================================================
# 初始化基类
# Args @param domain: 请求的域名或者ip
# @param port: 请求的端口
# =======================================================================
if url is None:
raise RequestException("domain must not be empty.")
if url.find('http://') >= 0:
self.__port = 80
pathUrl = url.replace('http://', '')
elif url.find('https://') >= 0:
self.__port = 443
pathUrl = url.replace('https://', '')
else:
raise RequestException("http protocol is not validate.")
index = pathUrl.find('/')
if index > 0:
self.__domain = pathUrl[0:index]
self.__path = pathUrl[index:]
else:
self.__domain = pathUrl
self.__path = ''
# print("domain:" + self.__domain + ",path:" + self.__path + ",port:" + str(self.__port))
def get_request_header(self):
return {
'Content-type': 'application/json;charset=UTF-8',
"Cache-Control": "no-cache",
"Connection": "Keep-Alive",
}
def getHttpMethod(self):
return "GET"
def getapiname(self):
return ""
def getMultipartParas(self):
return []
def getTranslateParas(self):
return {}
def _check_requst(self):
pass
def getResponse(self, authrize='', accessKey='', accessSecret='', suiteTicket='', corpId='', timeout=30):
# =======================================================================
# 获取response结果
# =======================================================================
if self.__port == 443:
connection = http.client.HTTPSConnection(self.__domain, self.__port, None, None, timeout)
else:
connection = http.client.HTTPConnection(self.__domain, self.__port, timeout)
sys_parameters = {
P_PARTNER_ID: SYSTEM_GENERATE_VERSION,
}
if authrize is not None:
sys_parameters[P_ACCESS_TOKEN] = authrize
application_parameter = self.getApplicationParameters()
sign_parameter = sys_parameters.copy()
sign_parameter.update(application_parameter)
header = self.get_request_header()
if self.getMultipartParas():
form = MultiPartForm()
for key, value in list(application_parameter.items()):
form.add_field(key, value)
for key in self.getMultipartParas():
fileitem = getattr(self, key)
if fileitem and isinstance(fileitem, FileItem):
form.add_file(key, fileitem.filename, fileitem.content)
body = str(form)
header['Content-type'] = form.get_content_type()
else:
body = urllib.parse.urlencode(application_parameter)
if accessKey != '':
timestamp = str(int(round(time.time()))) + '000'
print(("timestamp:" + timestamp))
canonicalString = self.getCanonicalStringForIsv(timestamp, suiteTicket)
print(("canonicalString:" + canonicalString))
print(("accessSecret:" + accessSecret))
signature = self.computeSignature(accessSecret, canonicalString)
print(("signature:" + signature))
ps = {}
ps["accessKey"] = accessKey
ps["signature"] = signature
ps["timestamp"] = timestamp
if suiteTicket != '':
ps["suiteTicket"] = suiteTicket
if corpId != '':
ps["corpId"] = corpId
queryStr = urllib.parse.urlencode(ps)
if self.__path.find("?") > 0:
fullPath = self.__path + "&" + queryStr
else:
fullPath = self.__path + "?" + queryStr
print(("fullPath:" + fullPath))
else:
if self.__path.find("?") > 0:
fullPath = (self.__path + "&access_token=" + str(authrize)) if len(str(authrize)) > 0 else self.__path
else:
fullPath = (self.__path + "?access_token=" + str(authrize)) if len(str(authrize)) > 0 else self.__path
if self.getHttpMethod() == "GET":
if fullPath.find("?") > 0:
fullPath = fullPath + "&" + body
else:
fullPath = fullPath + "?" + body
connection.request(self.getHttpMethod(), fullPath, headers=header)
else:
if self.getMultipartParas():
body = body
else:
body = json.dumps(application_parameter)
connection.request(self.getHttpMethod(), fullPath, body=body, headers=header)
response = connection.getresponse()
if response.status != 200:
raise RequestException('invalid http status ' + str(response.status) + ',detail body:' + str(response.read()))
result = response.read()
# print("result:" + result)
jsonobj = json.loads(result)
if P_CODE in jsonobj and jsonobj[P_CODE] != 0:
error = TopException()
error.errcode = jsonobj[P_CODE]
error.errmsg = jsonobj[P_MSG]
error.application_host = response.getheader("Application-Host", "")
error.service_host = response.getheader("Location-Host", "")
raise error
return jsonobj
def getCanonicalStringForIsv(self, timestamp, suiteTicket):
if suiteTicket != '':
return timestamp + '\n' + suiteTicket
else:
return timestamp
def computeSignature(self, secret, canonicalString):
message = canonicalString.encode(encoding="utf-8")
sec = secret.encode(encoding="utf-8")
return str(base64.b64encode(hmac.new(sec, message, digestmod=hashlib.sha256).digest()))
def getApplicationParameters(self):
application_parameter = {}
for key, value in self.__dict__.items():
if not key.startswith("__") and not key in self.getMultipartParas() and not key.startswith("_RestApi__") and value is not None:
if key.startswith("_"):
application_parameter[key[1:]] = value
else:
application_parameter[key] = value
# 查询翻译字典来规避一些关键字属性
translate_parameter = self.getTranslateParas()
for key, value in application_parameter.items():
if key in translate_parameter:
application_parameter[translate_parameter[key]] = application_parameter[key]
del application_parameter[key]
return application_parameter

View File

@@ -0,0 +1,17 @@
"""
Created by auto_sdk on 2019.07.03
"""
from api.base import RestApi
class CcoserviceServicegroupAddmemberRequest(RestApi):
def __init__(self, url=None):
RestApi.__init__(self, url)
self.open_group_id = None
self.userid = None
def getHttpMethod(self):
return 'POST'
def getapiname(self):
return 'dingtalk.ccoservice.servicegroup.addmember'

View File

@@ -0,0 +1,16 @@
"""
Created by auto_sdk on 2019.07.03
"""
from api.base import RestApi
class CcoserviceServicegroupGetRequest(RestApi):
def __init__(self, url=None):
RestApi.__init__(self, url)
self.open_group_id = None
def getHttpMethod(self):
return 'POST'
def getapiname(self):
return 'dingtalk.ccoservice.servicegroup.get'

View File

@@ -0,0 +1,15 @@
"""
Created by auto_sdk on 2019.07.03
"""
from api.base import RestApi
class CorpBlazersGetbinddataRequest(RestApi):
def __init__(self, url=None):
RestApi.__init__(self, url)
def getHttpMethod(self):
return 'POST'
def getapiname(self):
return 'dingtalk.corp.blazers.getbinddata'

View File

@@ -0,0 +1,15 @@
"""
Created by auto_sdk on 2019.07.03
"""
from api.base import RestApi
class CorpBlazersGetbizidRequest(RestApi):
def __init__(self, url=None):
RestApi.__init__(self, url)
def getHttpMethod(self):
return 'POST'
def getapiname(self):
return 'dingtalk.corp.blazers.getbizid'

View File

@@ -0,0 +1,16 @@
"""
Created by auto_sdk on 2018.07.25
"""
from api.base import RestApi
class CorpBlazersRemovemappingRequest(RestApi):
def __init__(self, url=None):
RestApi.__init__(self, url)
self.biz_id = None
def getHttpMethod(self):
return 'POST'
def getapiname(self):
return 'dingtalk.corp.blazers.removemapping'

View File

@@ -0,0 +1,15 @@
"""
Created by auto_sdk on 2019.07.03
"""
from api.base import RestApi
class CorpBlazersUnbindRequest(RestApi):
def __init__(self, url=None):
RestApi.__init__(self, url)
def getHttpMethod(self):
return 'POST'
def getapiname(self):
return 'dingtalk.corp.blazers.unbind'

View File

@@ -0,0 +1,16 @@
"""
Created by auto_sdk on 2019.07.03
"""
from api.base import RestApi
class CorpCalendarCreateRequest(RestApi):
def __init__(self, url=None):
RestApi.__init__(self, url)
self.create_vo = None
def getHttpMethod(self):
return 'POST'
def getapiname(self):
return 'dingtalk.corp.calendar.create'

View File

@@ -0,0 +1,19 @@
"""
Created by auto_sdk on 2020.09.18
"""
from api.base import RestApi
class CorpChatbotAddchatbotinstanceRequest(RestApi):
def __init__(self, url=None):
RestApi.__init__(self, url)
self.chatbot_id = None
self.icon_media_id = None
self.name = None
self.open_conversation_id = None
def getHttpMethod(self):
return 'POST'
def getapiname(self):
return 'dingtalk.corp.chatbot.addchatbotinstance'

View File

@@ -0,0 +1,16 @@
"""
Created by auto_sdk on 2018.07.25
"""
from api.base import RestApi
class CorpChatbotCreateorgbotRequest(RestApi):
def __init__(self, url=None):
RestApi.__init__(self, url)
self.create_chat_bot_model = None
def getHttpMethod(self):
return 'POST'
def getapiname(self):
return 'dingtalk.corp.chatbot.createorgbot'

View File

@@ -0,0 +1,16 @@
"""
Created by auto_sdk on 2020.08.17
"""
from api.base import RestApi
class CorpChatbotInstallRequest(RestApi):
def __init__(self, url=None):
RestApi.__init__(self, url)
self.chatbot_vo = None
def getHttpMethod(self):
return 'POST'
def getapiname(self):
return 'dingtalk.corp.chatbot.install'

View File

@@ -0,0 +1,16 @@
"""
Created by auto_sdk on 2020.09.18
"""
from api.base import RestApi
class CorpChatbotListbychatbotidsRequest(RestApi):
def __init__(self, url=None):
RestApi.__init__(self, url)
self.chatbot_ids = None
def getHttpMethod(self):
return 'POST'
def getapiname(self):
return 'dingtalk.corp.chatbot.listbychatbotids'

View File

@@ -0,0 +1,17 @@
"""
Created by auto_sdk on 2018.07.25
"""
from api.base import RestApi
class CorpChatbotListorgbotRequest(RestApi):
def __init__(self, url=None):
RestApi.__init__(self, url)
self.agent_id = None
self.type = None
def getHttpMethod(self):
return 'POST'
def getapiname(self):
return 'dingtalk.corp.chatbot.listorgbot'

View File

@@ -0,0 +1,17 @@
"""
Created by auto_sdk on 2020.09.18
"""
from api.base import RestApi
class CorpChatbotListorgbotbytypeandbottypeRequest(RestApi):
def __init__(self, url=None):
RestApi.__init__(self, url)
self.bot_type = None
self.type = None
def getHttpMethod(self):
return 'POST'
def getapiname(self):
return 'dingtalk.corp.chatbot.listorgbotbytypeandbottype'

View File

@@ -0,0 +1,22 @@
"""
Created by auto_sdk on 2019.07.03
"""
from api.base import RestApi
class CorpChatbotUpdatebychatbotidRequest(RestApi):
def __init__(self, url=None):
RestApi.__init__(self, url)
self.breif = None
self.chatbot_id = None
self.description = None
self.icon = None
self.name = None
self.preview_media_id = None
self.update_type = None
def getHttpMethod(self):
return 'POST'
def getapiname(self):
return 'dingtalk.corp.chatbot.updatebychatbotid'

View File

@@ -0,0 +1,18 @@
"""
Created by auto_sdk on 2018.07.25
"""
from api.base import RestApi
class CorpChatbotUpdateorgbotRequest(RestApi):
def __init__(self, url=None):
RestApi.__init__(self, url)
self.chatbot_id = None
self.icon = None
self.name = None
def getHttpMethod(self):
return 'POST'
def getapiname(self):
return 'dingtalk.corp.chatbot.updateorgbot'

View File

@@ -0,0 +1,16 @@
"""
Created by auto_sdk on 2020.09.21
"""
from api.base import RestApi
class CorpConversationCorpconversionGetconversationRequest(RestApi):
def __init__(self, url=None):
RestApi.__init__(self, url)
self.open_conversation_id = None
def getHttpMethod(self):
return 'POST'
def getapiname(self):
return 'dingtalk.corp.conversation.corpconversion.getconversation'

View File

@@ -0,0 +1,18 @@
"""
Created by auto_sdk on 2020.09.21
"""
from api.base import RestApi
class CorpConversationCorpconversionListmemberRequest(RestApi):
def __init__(self, url=None):
RestApi.__init__(self, url)
self.count = None
self.offset = None
self.open_conversation_id = None
def getHttpMethod(self):
return 'POST'
def getapiname(self):
return 'dingtalk.corp.conversation.corpconversion.listmember'

View File

@@ -0,0 +1,17 @@
"""
Created by auto_sdk on 2019.07.03
"""
from api.base import RestApi
class CorpDeptgroupSyncuserRequest(RestApi):
def __init__(self, url=None):
RestApi.__init__(self, url)
self.dept_id = None
self.userid = None
def getHttpMethod(self):
return 'POST'
def getapiname(self):
return 'dingtalk.corp.deptgroup.syncuser'

View File

@@ -0,0 +1,17 @@
"""
Created by auto_sdk on 2019.07.03
"""
from api.base import RestApi
class CorpDeviceManageGetRequest(RestApi):
def __init__(self, url=None):
RestApi.__init__(self, url)
self.device_id = None
self.device_service_id = None
def getHttpMethod(self):
return 'POST'
def getapiname(self):
return 'dingtalk.corp.device.manage.get'

View File

@@ -0,0 +1,16 @@
"""
Created by auto_sdk on 2019.07.03
"""
from api.base import RestApi
class CorpDeviceManageHasbinddeviceRequest(RestApi):
def __init__(self, url=None):
RestApi.__init__(self, url)
self.device_service_id = None
def getHttpMethod(self):
return 'POST'
def getapiname(self):
return 'dingtalk.corp.device.manage.hasbinddevice'

View File

@@ -0,0 +1,18 @@
"""
Created by auto_sdk on 2019.08.14
"""
from api.base import RestApi
class CorpDeviceManageQuerylistRequest(RestApi):
def __init__(self, url=None):
RestApi.__init__(self, url)
self.cursor = None
self.device_service_id = None
self.size = None
def getHttpMethod(self):
return 'POST'
def getapiname(self):
return 'dingtalk.corp.device.manage.querylist'

View File

@@ -0,0 +1,17 @@
"""
Created by auto_sdk on 2018.07.25
"""
from api.base import RestApi
class CorpDeviceManageUnbindRequest(RestApi):
def __init__(self, url=None):
RestApi.__init__(self, url)
self.device_id = None
self.device_service_id = None
def getHttpMethod(self):
return 'POST'
def getapiname(self):
return 'dingtalk.corp.device.manage.unbind'

View File

@@ -0,0 +1,18 @@
"""
Created by auto_sdk on 2019.07.03
"""
from api.base import RestApi
class CorpDeviceNickUpdateRequest(RestApi):
def __init__(self, url=None):
RestApi.__init__(self, url)
self.device_id = None
self.device_service_id = None
self.new_nick = None
def getHttpMethod(self):
return 'POST'
def getapiname(self):
return 'dingtalk.corp.device.nick.update'

View File

@@ -0,0 +1,21 @@
"""
Created by auto_sdk on 2019.07.03
"""
from api.base import RestApi
class CorpDingCreateRequest(RestApi):
def __init__(self, url=None):
RestApi.__init__(self, url)
self.attachment = None
self.creator_userid = None
self.receiver_userids = None
self.remind_time = None
self.remind_type = None
self.text_content = None
def getHttpMethod(self):
return 'POST'
def getapiname(self):
return 'dingtalk.corp.ding.create'

View File

@@ -0,0 +1,19 @@
"""
Created by auto_sdk on 2019.07.03
"""
from api.base import RestApi
class CorpDingReceiverstatusListRequest(RestApi):
def __init__(self, url=None):
RestApi.__init__(self, url)
self.confirmed_status = None
self.ding_id = None
self.page_no = None
self.page_size = None
def getHttpMethod(self):
return 'POST'
def getapiname(self):
return 'dingtalk.corp.ding.receiverstatus.list'

View File

@@ -0,0 +1,16 @@
"""
Created by auto_sdk on 2018.07.25
"""
from api.base import RestApi
class CorpDingTaskCreateRequest(RestApi):
def __init__(self, url=None):
RestApi.__init__(self, url)
self.task_send_v_o = None
def getHttpMethod(self):
return 'POST'
def getapiname(self):
return 'dingtalk.corp.ding.task.create'

View File

@@ -0,0 +1,18 @@
"""
Created by auto_sdk on 2019.07.03
"""
from api.base import RestApi
class CorpEmpSearchRequest(RestApi):
def __init__(self, url=None):
RestApi.__init__(self, url)
self.keyword = None
self.offset = None
self.size = None
def getHttpMethod(self):
return 'POST'
def getapiname(self):
return 'dingtalk.corp.emp.search'

View File

@@ -0,0 +1,15 @@
"""
Created by auto_sdk on 2018.07.25
"""
from api.base import RestApi
class CorpEncryptionKeyListRequest(RestApi):
def __init__(self, url=None):
RestApi.__init__(self, url)
def getHttpMethod(self):
return 'POST'
def getapiname(self):
return 'dingtalk.corp.encryption.key.list'

View File

@@ -0,0 +1,16 @@
"""
Created by auto_sdk on 2019.07.03
"""
from api.base import RestApi
class CorpExtAddRequest(RestApi):
def __init__(self, url=None):
RestApi.__init__(self, url)
self.contact = None
def getHttpMethod(self):
return 'POST'
def getapiname(self):
return 'dingtalk.corp.ext.add'

View File

@@ -0,0 +1,17 @@
"""
Created by auto_sdk on 2019.07.03
"""
from api.base import RestApi
class CorpExtListRequest(RestApi):
def __init__(self, url=None):
RestApi.__init__(self, url)
self.offset = None
self.size = None
def getHttpMethod(self):
return 'POST'
def getapiname(self):
return 'dingtalk.corp.ext.list'

View File

@@ -0,0 +1,17 @@
"""
Created by auto_sdk on 2019.07.03
"""
from api.base import RestApi
class CorpExtListlabelgroupsRequest(RestApi):
def __init__(self, url=None):
RestApi.__init__(self, url)
self.offset = None
self.size = None
def getHttpMethod(self):
return 'POST'
def getapiname(self):
return 'dingtalk.corp.ext.listlabelgroups'

View File

@@ -0,0 +1,16 @@
"""
Created by auto_sdk on 2019.07.03
"""
from api.base import RestApi
class CorpExtUpdateRequest(RestApi):
def __init__(self, url=None):
RestApi.__init__(self, url)
self.contact = None
def getHttpMethod(self):
return 'POST'
def getapiname(self):
return 'dingtalk.corp.ext.update'

View File

@@ -0,0 +1,16 @@
"""
Created by auto_sdk on 2019.07.03
"""
from api.base import RestApi
class CorpExtcontactCreateRequest(RestApi):
def __init__(self, url=None):
RestApi.__init__(self, url)
self.contact = None
def getHttpMethod(self):
return 'POST'
def getapiname(self):
return 'dingtalk.corp.extcontact.create'

View File

@@ -0,0 +1,16 @@
"""
Created by auto_sdk on 2018.07.25
"""
from api.base import RestApi
class CorpExtcontactDeleteRequest(RestApi):
def __init__(self, url=None):
RestApi.__init__(self, url)
self.userid = None
def getHttpMethod(self):
return 'POST'
def getapiname(self):
return 'dingtalk.corp.extcontact.delete'

View File

@@ -0,0 +1,16 @@
"""
Created by auto_sdk on 2019.07.03
"""
from api.base import RestApi
class CorpExtcontactGetRequest(RestApi):
def __init__(self, url=None):
RestApi.__init__(self, url)
self.user_id = None
def getHttpMethod(self):
return 'POST'
def getapiname(self):
return 'dingtalk.corp.extcontact.get'

View File

@@ -0,0 +1,17 @@
"""
Created by auto_sdk on 2019.07.03
"""
from api.base import RestApi
class CorpExtcontactListRequest(RestApi):
def __init__(self, url=None):
RestApi.__init__(self, url)
self.offset = None
self.size = None
def getHttpMethod(self):
return 'POST'
def getapiname(self):
return 'dingtalk.corp.extcontact.list'

View File

@@ -0,0 +1,17 @@
"""
Created by auto_sdk on 2019.07.03
"""
from api.base import RestApi
class CorpExtcontactListlabelgroupsRequest(RestApi):
def __init__(self, url=None):
RestApi.__init__(self, url)
self.offset = None
self.size = None
def getHttpMethod(self):
return 'POST'
def getapiname(self):
return 'dingtalk.corp.extcontact.listlabelgroups'

View File

@@ -0,0 +1,16 @@
"""
Created by auto_sdk on 2018.07.25
"""
from api.base import RestApi
class CorpExtcontactUpdateRequest(RestApi):
def __init__(self, url=None):
RestApi.__init__(self, url)
self.contact = None
def getHttpMethod(self):
return 'POST'
def getapiname(self):
return 'dingtalk.corp.extcontact.update'

View File

@@ -0,0 +1,16 @@
"""
Created by auto_sdk on 2019.07.03
"""
from api.base import RestApi
class CorpHealthStepinfoGetuserstatusRequest(RestApi):
def __init__(self, url=None):
RestApi.__init__(self, url)
self.userid = None
def getHttpMethod(self):
return 'POST'
def getapiname(self):
return 'dingtalk.corp.health.stepinfo.getuserstatus'

View File

@@ -0,0 +1,18 @@
"""
Created by auto_sdk on 2019.07.03
"""
from api.base import RestApi
class CorpHealthStepinfoListRequest(RestApi):
def __init__(self, url=None):
RestApi.__init__(self, url)
self.object_id = None
self.stat_dates = None
self.type = None
def getHttpMethod(self):
return 'POST'
def getapiname(self):
return 'dingtalk.corp.health.stepinfo.list'

View File

@@ -0,0 +1,17 @@
"""
Created by auto_sdk on 2019.07.03
"""
from api.base import RestApi
class CorpHealthStepinfoListbyuseridRequest(RestApi):
def __init__(self, url=None):
RestApi.__init__(self, url)
self.stat_date = None
self.userids = None
def getHttpMethod(self):
return 'POST'
def getapiname(self):
return 'dingtalk.corp.health.stepinfo.listbyuserid'

View File

@@ -0,0 +1,23 @@
"""
Created by auto_sdk on 2018.07.25
"""
from api.base import RestApi
class CorpHrmEmployeeAddresumerecordRequest(RestApi):
def __init__(self, url=None):
RestApi.__init__(self, url)
self.content = None
self.k_v_content = None
self.pc_url = None
self.phone_url = None
self.record_time_stamp = None
self.title = None
self.userid = None
self.web_url = None
def getHttpMethod(self):
return 'POST'
def getapiname(self):
return 'dingtalk.corp.hrm.employee.addresumerecord'

View File

@@ -0,0 +1,17 @@
"""
Created by auto_sdk on 2019.07.03
"""
from api.base import RestApi
class CorpHrmEmployeeDelemployeedismissionandhandoverRequest(RestApi):
def __init__(self, url=None):
RestApi.__init__(self, url)
self.dismission_info_with_hand_over = None
self.op_userid = None
def getHttpMethod(self):
return 'POST'
def getapiname(self):
return 'dingtalk.corp.hrm.employee.delemployeedismissionandhandover'

View File

@@ -0,0 +1,16 @@
"""
Created by auto_sdk on 2019.07.03
"""
from api.base import RestApi
class CorpHrmEmployeeGetRequest(RestApi):
def __init__(self, url=None):
RestApi.__init__(self, url)
self.userid = None
def getHttpMethod(self):
return 'POST'
def getapiname(self):
return 'dingtalk.corp.hrm.employee.get'

View File

@@ -0,0 +1,18 @@
"""
Created by auto_sdk on 2019.07.03
"""
from api.base import RestApi
class CorpHrmEmployeeGetdismissionlistRequest(RestApi):
def __init__(self, url=None):
RestApi.__init__(self, url)
self.current = None
self.op_userid = None
self.page_size = None
def getHttpMethod(self):
return 'POST'
def getapiname(self):
return 'dingtalk.corp.hrm.employee.getdismissionlist'

View File

@@ -0,0 +1,17 @@
"""
Created by auto_sdk on 2019.07.03
"""
from api.base import RestApi
class CorpHrmEmployeeModjobinfoRequest(RestApi):
def __init__(self, url=None):
RestApi.__init__(self, url)
self.hrm_api_job_model = None
self.op_userid = None
def getHttpMethod(self):
return 'POST'
def getapiname(self):
return 'dingtalk.corp.hrm.employee.modjobinfo'

View File

@@ -0,0 +1,17 @@
"""
Created by auto_sdk on 2019.07.03
"""
from api.base import RestApi
class CorpHrmEmployeeSetuserworkdataRequest(RestApi):
def __init__(self, url=None):
RestApi.__init__(self, url)
self.hrm_api_user_data_model = None
self.op_userid = None
def getHttpMethod(self):
return 'POST'
def getapiname(self):
return 'dingtalk.corp.hrm.employee.setuserworkdata'

View File

@@ -0,0 +1,15 @@
"""
Created by auto_sdk on 2018.07.25
"""
from api.base import RestApi
class CorpInvoiceGettitleRequest(RestApi):
def __init__(self, url=None):
RestApi.__init__(self, url)
def getHttpMethod(self):
return 'POST'
def getapiname(self):
return 'dingtalk.corp.invoice.gettitle'

View File

@@ -0,0 +1,15 @@
"""
Created by auto_sdk on 2019.07.03
"""
from api.base import RestApi
class CorpLivenessGetRequest(RestApi):
def __init__(self, url=None):
RestApi.__init__(self, url)
def getHttpMethod(self):
return 'POST'
def getapiname(self):
return 'dingtalk.corp.liveness.get'

View File

@@ -0,0 +1,21 @@
"""
Created by auto_sdk on 2019.07.03
"""
from api.base import RestApi
class CorpMessageCorpconversationAsyncsendRequest(RestApi):
def __init__(self, url=None):
RestApi.__init__(self, url)
self.agent_id = None
self.dept_id_list = None
self.msgcontent = None
self.msgtype = None
self.to_all_user = None
self.userid_list = None
def getHttpMethod(self):
return 'POST'
def getapiname(self):
return 'dingtalk.corp.message.corpconversation.asyncsend'

View File

@@ -0,0 +1,22 @@
"""
Created by auto_sdk on 2019.07.03
"""
from api.base import RestApi
class CorpMessageCorpconversationAsyncsendbycodeRequest(RestApi):
def __init__(self, url=None):
RestApi.__init__(self, url)
self.agent_id = None
self.code = None
self.dept_id_list = None
self.msgcontent = None
self.msgtype = None
self.to_all_user = None
self.user_id_list = None
def getHttpMethod(self):
return 'POST'
def getapiname(self):
return 'dingtalk.corp.message.corpconversation.asyncsendbycode'

View File

@@ -0,0 +1,17 @@
"""
Created by auto_sdk on 2019.07.03
"""
from api.base import RestApi
class CorpMessageCorpconversationGetsendprogressRequest(RestApi):
def __init__(self, url=None):
RestApi.__init__(self, url)
self.agent_id = None
self.task_id = None
def getHttpMethod(self):
return 'POST'
def getapiname(self):
return 'dingtalk.corp.message.corpconversation.getsendprogress'

View File

@@ -0,0 +1,17 @@
"""
Created by auto_sdk on 2019.07.03
"""
from api.base import RestApi
class CorpMessageCorpconversationGetsendresultRequest(RestApi):
def __init__(self, url=None):
RestApi.__init__(self, url)
self.agent_id = None
self.task_id = None
def getHttpMethod(self):
return 'POST'
def getapiname(self):
return 'dingtalk.corp.message.corpconversation.getsendresult'

View File

@@ -0,0 +1,20 @@
"""
Created by auto_sdk on 2018.07.25
"""
from api.base import RestApi
class CorpMessageCorpconversationSendmockRequest(RestApi):
def __init__(self, url=None):
RestApi.__init__(self, url)
self.message = None
self.message_type = None
self.microapp_agent_id = None
self.to_party = None
self.to_user = None
def getHttpMethod(self):
return 'POST'
def getapiname(self):
return 'dingtalk.corp.message.corpconversation.sendmock'

View File

@@ -0,0 +1,21 @@
"""
Created by auto_sdk on 2019.07.03
"""
from api.base import RestApi
class CorpReportListRequest(RestApi):
def __init__(self, url=None):
RestApi.__init__(self, url)
self.cursor = None
self.end_time = None
self.size = None
self.start_time = None
self.template_name = None
self.userid = None
def getHttpMethod(self):
return 'POST'
def getapiname(self):
return 'dingtalk.corp.report.list'

View File

@@ -0,0 +1,17 @@
"""
Created by auto_sdk on 2019.07.03
"""
from api.base import RestApi
class CorpRoleAddrolesforempsRequest(RestApi):
def __init__(self, url=None):
RestApi.__init__(self, url)
self.rolelid_list = None
self.userid_list = None
def getHttpMethod(self):
return 'POST'
def getapiname(self):
return 'dingtalk.corp.role.addrolesforemps'

View File

@@ -0,0 +1,16 @@
"""
Created by auto_sdk on 2019.07.03
"""
from api.base import RestApi
class CorpRoleDeleteroleRequest(RestApi):
def __init__(self, url=None):
RestApi.__init__(self, url)
self.role_id = None
def getHttpMethod(self):
return 'POST'
def getapiname(self):
return 'dingtalk.corp.role.deleterole'

View File

@@ -0,0 +1,16 @@
"""
Created by auto_sdk on 2019.07.03
"""
from api.base import RestApi
class CorpRoleGetrolegroupRequest(RestApi):
def __init__(self, url=None):
RestApi.__init__(self, url)
self.group_id = None
def getHttpMethod(self):
return 'POST'
def getapiname(self):
return 'dingtalk.corp.role.getrolegroup'

View File

@@ -0,0 +1,17 @@
"""
Created by auto_sdk on 2019.07.03
"""
from api.base import RestApi
class CorpRoleListRequest(RestApi):
def __init__(self, url=None):
RestApi.__init__(self, url)
self.offset = None
self.size = None
def getHttpMethod(self):
return 'POST'
def getapiname(self):
return 'dingtalk.corp.role.list'

View File

@@ -0,0 +1,17 @@
"""
Created by auto_sdk on 2019.07.03
"""
from api.base import RestApi
class CorpRoleRemoverolesforempsRequest(RestApi):
def __init__(self, url=None):
RestApi.__init__(self, url)
self.roleid_list = None
self.userid_list = None
def getHttpMethod(self):
return 'POST'
def getapiname(self):
return 'dingtalk.corp.role.removerolesforemps'

View File

@@ -0,0 +1,18 @@
"""
Created by auto_sdk on 2019.07.03
"""
from api.base import RestApi
class CorpRoleSimplelistRequest(RestApi):
def __init__(self, url=None):
RestApi.__init__(self, url)
self.offset = None
self.role_id = None
self.size = None
def getHttpMethod(self):
return 'POST'
def getapiname(self):
return 'dingtalk.corp.role.simplelist'

View File

@@ -0,0 +1,18 @@
"""
Created by auto_sdk on 2019.07.03
"""
from api.base import RestApi
class CorpSearchCorpcontactBaseinfoRequest(RestApi):
def __init__(self, url=None):
RestApi.__init__(self, url)
self.offset = None
self.query = None
self.size = None
def getHttpMethod(self):
return 'POST'
def getapiname(self):
return 'dingtalk.corp.search.corpcontact.baseinfo'

Some files were not shown because too many files have changed in this diff Show More