mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-02 07:27:04 +00:00
[social auth & notification] Add wechat work notification
This commit is contained in:
@@ -20,3 +20,4 @@ gunicorn==19.8.1
|
|||||||
django-webpack-loader==0.6.0
|
django-webpack-loader==0.6.0
|
||||||
git+git://github.com/haiwen/python-cas.git@ffc49235fd7cc32c4fdda5acfa3707e1405881df#egg=python_cas
|
git+git://github.com/haiwen/python-cas.git@ffc49235fd7cc32c4fdda5acfa3707e1405881df#egg=python_cas
|
||||||
futures==3.2.0
|
futures==3.2.0
|
||||||
|
social-auth-core==1.7.0
|
||||||
|
164
seahub/notifications/management/commands/send_wxwork_notices.py
Normal file
164
seahub/notifications/management/commands/send_wxwork_notices.py
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
# Copyright (c) 2012-2016 Seafile Ltd.
|
||||||
|
# encoding: utf-8
|
||||||
|
from datetime import datetime
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
from django.utils import translation
|
||||||
|
from django.utils.translation import ungettext
|
||||||
|
from social_django.models import UserSocialAuth
|
||||||
|
from weworkapi import CorpApi
|
||||||
|
|
||||||
|
from seahub.base.models import CommandsLastCheck
|
||||||
|
from seahub.notifications.models import UserNotification
|
||||||
|
from seahub.profile.models import Profile
|
||||||
|
from seahub.utils import get_site_scheme_and_netloc, get_site_name
|
||||||
|
|
||||||
|
# Get an instance of a logger
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
########## Utility Functions ##########
|
||||||
|
def wrap_div(s):
|
||||||
|
"""
|
||||||
|
Replace <a ..>xx</a> to xx and wrap content with <div></div>.
|
||||||
|
"""
|
||||||
|
patt = '<a.*?>(.+?)</a>'
|
||||||
|
|
||||||
|
def repl(matchobj):
|
||||||
|
return matchobj.group(1)
|
||||||
|
|
||||||
|
return '<div class="highlight">' + re.sub(patt, repl, s) + '</div>'
|
||||||
|
|
||||||
|
class CommandLogMixin(object):
|
||||||
|
def println(self, msg):
|
||||||
|
self.stdout.write('[%s] %s\n' % (str(datetime.now()), msg))
|
||||||
|
|
||||||
|
def log_error(self, msg):
|
||||||
|
logger.error(msg)
|
||||||
|
self.println(msg)
|
||||||
|
|
||||||
|
def log_info(self, msg):
|
||||||
|
logger.info(msg)
|
||||||
|
self.println(msg)
|
||||||
|
|
||||||
|
def log_debug(self, msg):
|
||||||
|
logger.debug(msg)
|
||||||
|
self.println(msg)
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
|
||||||
|
class Command(BaseCommand, CommandLogMixin):
|
||||||
|
help = 'Send WeChat Work msg to user if he/she has unseen notices every '
|
||||||
|
'period of time.'
|
||||||
|
label = "notifications_send_wxwork_notices"
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
self.log_debug('Start sending WeChat Work msg...')
|
||||||
|
self.api = CorpApi.CorpApi(settings.SOCIAL_AUTH_WEIXIN_WORK_KEY,
|
||||||
|
settings.SOCIAL_AUTH_WEIXIN_WORK_SECRET)
|
||||||
|
|
||||||
|
self.do_action()
|
||||||
|
self.log_debug('Finish sending WeChat Work msg.\n')
|
||||||
|
|
||||||
|
def send_wx_msg(self, uid, title, content, detail_url):
|
||||||
|
try:
|
||||||
|
self.log_info('Send wechat msg to user: %s, msg: %s' % (uid, content))
|
||||||
|
response = self.api.httpCall(
|
||||||
|
CorpApi.CORP_API_TYPE['MESSAGE_SEND'],
|
||||||
|
{
|
||||||
|
"touser": uid,
|
||||||
|
"agentid": settings.SOCIAL_AUTH_WEIXIN_WORK_AGENTID,
|
||||||
|
'msgtype': 'textcard',
|
||||||
|
# 'climsgid': 'climsgidclimsgid_d',
|
||||||
|
'textcard': {
|
||||||
|
'title': title,
|
||||||
|
'description': content,
|
||||||
|
'url': detail_url,
|
||||||
|
},
|
||||||
|
'safe': 0,
|
||||||
|
})
|
||||||
|
self.log_info(response)
|
||||||
|
except Exception as ex:
|
||||||
|
logger.error(ex, exc_info=True)
|
||||||
|
|
||||||
|
def get_user_language(self, username):
|
||||||
|
return Profile.objects.get_user_language(username)
|
||||||
|
|
||||||
|
def do_action(self):
|
||||||
|
now = datetime.now()
|
||||||
|
today = datetime.now().replace(hour=0).replace(minute=0).replace(
|
||||||
|
second=0).replace(microsecond=0)
|
||||||
|
|
||||||
|
# 1. get all users who are connected wechat work
|
||||||
|
socials = UserSocialAuth.objects.filter(provider='weixin-work')
|
||||||
|
users = [(x.username, x.uid) for x in socials]
|
||||||
|
if not users:
|
||||||
|
return
|
||||||
|
|
||||||
|
user_uid_map = {}
|
||||||
|
for username, uid in users:
|
||||||
|
user_uid_map[username] = uid
|
||||||
|
|
||||||
|
# 2. get previous time that command last runs
|
||||||
|
try:
|
||||||
|
cmd_last_check = CommandsLastCheck.objects.get(command_type=self.label)
|
||||||
|
self.log_debug('Last check time is %s' % cmd_last_check.last_check)
|
||||||
|
|
||||||
|
last_check_dt = cmd_last_check.last_check
|
||||||
|
|
||||||
|
cmd_last_check.last_check = now
|
||||||
|
cmd_last_check.save()
|
||||||
|
except CommandsLastCheck.DoesNotExist:
|
||||||
|
last_check_dt = today
|
||||||
|
self.log_debug('Create new last check time: %s' % now)
|
||||||
|
CommandsLastCheck(command_type=self.label, last_check=now).save()
|
||||||
|
|
||||||
|
# 3. get all unseen notices for those users
|
||||||
|
qs = UserNotification.objects.filter(
|
||||||
|
timestamp__gt=last_check_dt
|
||||||
|
).filter(seen=False).filter(
|
||||||
|
to_user__in=user_uid_map.keys()
|
||||||
|
)
|
||||||
|
|
||||||
|
user_notices = {}
|
||||||
|
for q in qs:
|
||||||
|
if q.to_user not in user_notices:
|
||||||
|
user_notices[q.to_user] = [q]
|
||||||
|
else:
|
||||||
|
user_notices[q.to_user].append(q)
|
||||||
|
|
||||||
|
# 4. send msg to users
|
||||||
|
url = get_site_scheme_and_netloc().rstrip('/') + reverse('user_notification_list')
|
||||||
|
|
||||||
|
for username, uid in users:
|
||||||
|
notices = user_notices.get(username, [])
|
||||||
|
count = len(notices)
|
||||||
|
if count == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# save current language
|
||||||
|
cur_language = translation.get_language()
|
||||||
|
|
||||||
|
# get and active user language
|
||||||
|
user_language = self.get_user_language(username)
|
||||||
|
translation.activate(user_language)
|
||||||
|
self.log_debug('Set language code to %s for user: %s' % (
|
||||||
|
user_language, username))
|
||||||
|
|
||||||
|
title = ungettext(
|
||||||
|
"\n"
|
||||||
|
"You've got 1 new notice on %(site_name)s:\n",
|
||||||
|
"\n"
|
||||||
|
"You've got %(num)s new notices on %(site_name)s:\n",
|
||||||
|
count
|
||||||
|
) % {
|
||||||
|
'num': count,
|
||||||
|
'site_name': get_site_name(),
|
||||||
|
}
|
||||||
|
content = ''.join([wrap_div(x.format_msg()) for x in notices])
|
||||||
|
self.send_wx_msg(uid, title, content, url)
|
||||||
|
|
||||||
|
translation.activate(cur_language)
|
21
seahub/notifications/migrations/0003_auto_20181115_0825.py
Normal file
21
seahub/notifications/migrations/0003_auto_20181115_0825.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.15 on 2018-11-15 08:25
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('notifications', '0002_auto_20180426_0710'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='usernotification',
|
||||||
|
name='timestamp',
|
||||||
|
field=models.DateTimeField(db_index=True, default=datetime.datetime.now),
|
||||||
|
),
|
||||||
|
]
|
@@ -333,7 +333,7 @@ class UserNotification(models.Model):
|
|||||||
to_user = LowerCaseCharField(db_index=True, max_length=255)
|
to_user = LowerCaseCharField(db_index=True, max_length=255)
|
||||||
msg_type = models.CharField(db_index=True, max_length=30)
|
msg_type = models.CharField(db_index=True, max_length=30)
|
||||||
detail = models.TextField()
|
detail = models.TextField()
|
||||||
timestamp = models.DateTimeField(default=datetime.datetime.now)
|
timestamp = models.DateTimeField(db_index=True, default=datetime.datetime.now)
|
||||||
seen = models.BooleanField('seen', default=False)
|
seen = models.BooleanField('seen', default=False)
|
||||||
objects = UserNotificationManager()
|
objects = UserNotificationManager()
|
||||||
|
|
||||||
@@ -487,6 +487,32 @@ class UserNotification(models.Model):
|
|||||||
return {'message': message, 'msg_from': msg_from}
|
return {'message': message, 'msg_from': msg_from}
|
||||||
|
|
||||||
########## functions used in templates
|
########## functions used in templates
|
||||||
|
def format_msg(self):
|
||||||
|
if self.is_group_msg():
|
||||||
|
return self.format_group_message_title()
|
||||||
|
elif self.is_file_uploaded_msg():
|
||||||
|
return self.format_file_uploaded_msg()
|
||||||
|
elif self.is_repo_share_msg():
|
||||||
|
return self.format_repo_share_msg()
|
||||||
|
elif self.is_repo_share_to_group_msg():
|
||||||
|
return self.format_repo_share_to_group_msg()
|
||||||
|
elif self.is_group_join_request():
|
||||||
|
return self.format_group_join_request()
|
||||||
|
elif self.is_file_comment_msg():
|
||||||
|
return self.format_file_comment_msg()
|
||||||
|
elif self.is_review_comment_msg():
|
||||||
|
return self.format_review_comment_msg()
|
||||||
|
elif self.is_update_review_msg():
|
||||||
|
return self.format_update_review_msg()
|
||||||
|
elif self.is_request_reviewer_msg():
|
||||||
|
return self.format_request_reviewer_msg()
|
||||||
|
elif self.is_guest_invitation_accepted_msg():
|
||||||
|
return self.format_guest_invitation_accepted_msg()
|
||||||
|
elif self.is_add_user_to_group():
|
||||||
|
return self.format_add_user_to_group()
|
||||||
|
else:
|
||||||
|
return ''
|
||||||
|
|
||||||
def format_file_uploaded_msg(self):
|
def format_file_uploaded_msg(self):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@@ -175,6 +175,29 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="setting-item" id="social-auth">
|
||||||
|
<h3>{% trans "Social Login" %}</h3>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
{% if request.LANGUAGE_CODE == 'zh-cn' %}
|
||||||
|
企业微信
|
||||||
|
{% else %}
|
||||||
|
WeChat Work
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if social_connected %}
|
||||||
|
<a class="social-disconnect" href="#" data-url="{% url "social:disconnect" 'weixin-work' %}?next={{ social_next_page }}">{% trans "Disconnect" %}</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="{% url "social:begin" 'weixin-work' %}?next={{ social_next_page }}">{% trans "Connect" %}</a>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
{% if ENABLE_DELETE_ACCOUNT %}
|
{% if ENABLE_DELETE_ACCOUNT %}
|
||||||
<div class="setting-item" id="del-account">
|
<div class="setting-item" id="del-account">
|
||||||
<h3>{% trans "Delete Account" %}</h3>
|
<h3>{% trans "Delete Account" %}</h3>
|
||||||
@@ -379,5 +402,11 @@ $('#set-email-notice-interval-form').on('submit', function() {
|
|||||||
});
|
});
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
addConfirmTo($('.social-disconnect'), {
|
||||||
|
'title':"{% trans "Disconnect" %}",
|
||||||
|
'con':"{% trans "Are you sure you want to disconnect?" %}",
|
||||||
|
'post':true
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@@ -86,6 +86,10 @@ def edit_profile(request):
|
|||||||
email_inverval = UserOptions.objects.get_file_updates_email_interval(username)
|
email_inverval = UserOptions.objects.get_file_updates_email_interval(username)
|
||||||
email_inverval = email_inverval if email_inverval is not None else 0
|
email_inverval = email_inverval if email_inverval is not None else 0
|
||||||
|
|
||||||
|
from social_django.models import UserSocialAuth
|
||||||
|
social_connected = UserSocialAuth.objects.filter(
|
||||||
|
username=request.user.username, provider='weixin-work').count() > 0
|
||||||
|
|
||||||
resp_dict = {
|
resp_dict = {
|
||||||
'form': form,
|
'form': form,
|
||||||
'server_crypto': server_crypto,
|
'server_crypto': server_crypto,
|
||||||
@@ -102,6 +106,8 @@ def edit_profile(request):
|
|||||||
'ENABLE_UPDATE_USER_INFO': ENABLE_UPDATE_USER_INFO,
|
'ENABLE_UPDATE_USER_INFO': ENABLE_UPDATE_USER_INFO,
|
||||||
'webdav_passwd': webdav_passwd,
|
'webdav_passwd': webdav_passwd,
|
||||||
'email_notification_interval': email_inverval,
|
'email_notification_interval': email_inverval,
|
||||||
|
'social_connected': social_connected,
|
||||||
|
'social_next_page': reverse('edit_profile'),
|
||||||
}
|
}
|
||||||
|
|
||||||
if has_two_factor_auth():
|
if has_two_factor_auth():
|
||||||
|
@@ -125,6 +125,7 @@ MIDDLEWARE_CLASSES = (
|
|||||||
'seahub.two_factor.middleware.OTPMiddleware',
|
'seahub.two_factor.middleware.OTPMiddleware',
|
||||||
'seahub.two_factor.middleware.ForceTwoFactorAuthMiddleware',
|
'seahub.two_factor.middleware.ForceTwoFactorAuthMiddleware',
|
||||||
'seahub.trusted_ip.middleware.LimitIpMiddleware',
|
'seahub.trusted_ip.middleware.LimitIpMiddleware',
|
||||||
|
'social_django.middleware.SocialAuthExceptionMiddleware',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -152,6 +153,9 @@ TEMPLATES = [
|
|||||||
'django.template.context_processors.request',
|
'django.template.context_processors.request',
|
||||||
'django.contrib.messages.context_processors.messages',
|
'django.contrib.messages.context_processors.messages',
|
||||||
|
|
||||||
|
'social_django.context_processors.backends',
|
||||||
|
'social_django.context_processors.login_redirect',
|
||||||
|
|
||||||
'seahub.auth.context_processors.auth',
|
'seahub.auth.context_processors.auth',
|
||||||
'seahub.base.context_processors.base',
|
'seahub.base.context_processors.base',
|
||||||
'seahub.base.context_processors.debug',
|
'seahub.base.context_processors.debug',
|
||||||
@@ -223,6 +227,7 @@ INSTALLED_APPS = (
|
|||||||
'post_office',
|
'post_office',
|
||||||
'termsandconditions',
|
'termsandconditions',
|
||||||
'webpack_loader',
|
'webpack_loader',
|
||||||
|
'social_django',
|
||||||
|
|
||||||
'seahub.api2',
|
'seahub.api2',
|
||||||
'seahub.avatar',
|
'seahub.avatar',
|
||||||
@@ -264,17 +269,42 @@ CONSTANCE_BACKEND = 'constance.backends.database.DatabaseBackend'
|
|||||||
CONSTANCE_DATABASE_CACHE_BACKEND = 'default'
|
CONSTANCE_DATABASE_CACHE_BACKEND = 'default'
|
||||||
|
|
||||||
AUTHENTICATION_BACKENDS = (
|
AUTHENTICATION_BACKENDS = (
|
||||||
|
'seahub.social_core.backends.weixin_enterprise.WeixinWorkOAuth2',
|
||||||
|
|
||||||
'seahub.base.accounts.AuthBackend',
|
'seahub.base.accounts.AuthBackend',
|
||||||
'seahub.oauth.backends.OauthRemoteUserBackend',
|
'seahub.oauth.backends.OauthRemoteUserBackend',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
SOCIAL_AUTH_URL_NAMESPACE = 'social'
|
||||||
|
SOCIAL_AUTH_VERIFY_SSL = True
|
||||||
|
SOCIAL_AUTH_METHODS = (
|
||||||
|
('weixin', 'WeChat'),
|
||||||
|
)
|
||||||
|
|
||||||
|
SOCIAL_AUTH_WEIXIN_WORK_AGENTID = ''
|
||||||
|
SOCIAL_AUTH_WEIXIN_WORK_KEY = ''
|
||||||
|
SOCIAL_AUTH_WEIXIN_WORK_SECRET = ''
|
||||||
|
|
||||||
|
SOCIAL_AUTH_PIPELINE = (
|
||||||
|
'social_core.pipeline.social_auth.social_details',
|
||||||
|
'social_core.pipeline.social_auth.social_uid',
|
||||||
|
'social_core.pipeline.social_auth.auth_allowed',
|
||||||
|
'seahub.social_core.pipeline.social_auth.social_user',
|
||||||
|
'seahub.social_core.pipeline.user.get_username',
|
||||||
|
'seahub.social_core.pipeline.user.create_user',
|
||||||
|
'seahub.social_core.pipeline.social_auth.associate_user',
|
||||||
|
'social_core.pipeline.social_auth.load_extra_data',
|
||||||
|
# 'social_core.pipeline.user.user_details',
|
||||||
|
'seahub.social_core.pipeline.user.save_profile',
|
||||||
|
)
|
||||||
|
|
||||||
ENABLE_OAUTH = False
|
ENABLE_OAUTH = False
|
||||||
ENABLE_WATERMARK = False
|
ENABLE_WATERMARK = False
|
||||||
|
|
||||||
# allow user to clean library trash
|
# allow user to clean library trash
|
||||||
ENABLE_USER_CLEAN_TRASH = True
|
ENABLE_USER_CLEAN_TRASH = True
|
||||||
|
|
||||||
LOGIN_REDIRECT_URL = '/profile/'
|
LOGIN_REDIRECT_URL = '/'
|
||||||
LOGIN_URL = '/accounts/login/'
|
LOGIN_URL = '/accounts/login/'
|
||||||
LOGOUT_URL = '/accounts/logout/'
|
LOGOUT_URL = '/accounts/logout/'
|
||||||
LOGOUT_REDIRECT_URL = None
|
LOGOUT_REDIRECT_URL = None
|
||||||
|
0
seahub/social_core/__init__.py
Normal file
0
seahub/social_core/__init__.py
Normal file
0
seahub/social_core/backends/__init__.py
Normal file
0
seahub/social_core/backends/__init__.py
Normal file
198
seahub/social_core/backends/weixin_enterprise.py
Normal file
198
seahub/social_core/backends/weixin_enterprise.py
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
import urllib
|
||||||
|
from requests import HTTPError
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
from social_core.backends.oauth import BaseOAuth2
|
||||||
|
from social_core.exceptions import AuthCanceled, AuthUnknownError
|
||||||
|
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
try:
|
||||||
|
WEIXIN_WORK_SP = True if settings.SOCIAL_AUTH_WEIXIN_WORK_SUITID else False
|
||||||
|
except AttributeError:
|
||||||
|
WEIXIN_WORK_SP = False
|
||||||
|
|
||||||
|
if WEIXIN_WORK_SP is True:
|
||||||
|
_AUTHORIZATION_URL = 'https://open.work.weixin.qq.com/wwopen/sso/3rd_qrConnect'
|
||||||
|
_ACCESS_TOKEN_URL = 'https://qyapi.weixin.qq.com/cgi-bin/service/get_provider_token'
|
||||||
|
_USER_INFO_URL = 'https://qyapi.weixin.qq.com/cgi-bin/service/get_login_info'
|
||||||
|
else:
|
||||||
|
_AUTHORIZATION_URL = 'https://open.work.weixin.qq.com/wwopen/sso/qrConnect'
|
||||||
|
_ACCESS_TOKEN_URL = 'https://qyapi.weixin.qq.com/cgi-bin/token'
|
||||||
|
_USER_INFO_URL = 'https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo'
|
||||||
|
|
||||||
|
|
||||||
|
class WeixinWorkOAuth2(BaseOAuth2):
|
||||||
|
"""WeChat Work OAuth authentication backend"""
|
||||||
|
name = 'weixin-work'
|
||||||
|
ID_KEY = 'UserId'
|
||||||
|
AUTHORIZATION_URL = _AUTHORIZATION_URL
|
||||||
|
ACCESS_TOKEN_URL = _ACCESS_TOKEN_URL
|
||||||
|
ACCESS_TOKEN_METHOD = 'POST'
|
||||||
|
DEFAULT_SCOPE = ['snsapi_login']
|
||||||
|
REDIRECT_STATE = False
|
||||||
|
EXTRA_DATA = [
|
||||||
|
('nickname', 'username'),
|
||||||
|
('headimgurl', 'profile_image_url'),
|
||||||
|
]
|
||||||
|
|
||||||
|
def extra_data(self, user, uid, response, details=None, *args, **kwargs):
|
||||||
|
data = super(BaseOAuth2, self).extra_data(user, uid, response,
|
||||||
|
details=details,
|
||||||
|
*args, **kwargs)
|
||||||
|
|
||||||
|
if WEIXIN_WORK_SP:
|
||||||
|
data['corp_info'] = response.get('corp_info')
|
||||||
|
data['user_info'] = response.get('user_info')
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def get_user_id(self, details, response):
|
||||||
|
"""Return a unique ID for the current user, by default from server
|
||||||
|
response."""
|
||||||
|
if WEIXIN_WORK_SP:
|
||||||
|
return response.get('user_info').get('userid')
|
||||||
|
else:
|
||||||
|
return response.get(self.ID_KEY)
|
||||||
|
|
||||||
|
def get_user_details(self, response):
|
||||||
|
"""Return user details from Weixin. API URL is:
|
||||||
|
https://api.weixin.qq.com/sns/userinfo
|
||||||
|
"""
|
||||||
|
if WEIXIN_WORK_SP:
|
||||||
|
user_info = response.get('user_info')
|
||||||
|
return {
|
||||||
|
'userid': user_info.get('userid'),
|
||||||
|
'user_name': user_info.get('name'),
|
||||||
|
'user_avatar': user_info.get('avatar'),
|
||||||
|
'corpid': response.get('corp_info').get('corpid'),
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
if self.setting('DOMAIN_AS_USERNAME'):
|
||||||
|
username = response.get('domain', '')
|
||||||
|
else:
|
||||||
|
username = response.get('nickname', '')
|
||||||
|
return {
|
||||||
|
'username': username,
|
||||||
|
'profile_image_url': response.get('headimgurl', '')
|
||||||
|
}
|
||||||
|
|
||||||
|
def user_data(self, access_token, *args, **kwargs):
|
||||||
|
if WEIXIN_WORK_SP:
|
||||||
|
data = self.get_json(_USER_INFO_URL,
|
||||||
|
params={'access_token': access_token},
|
||||||
|
json={'auth_code': kwargs['request'].GET.get('auth_code')},
|
||||||
|
headers={'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json'},
|
||||||
|
method='post')
|
||||||
|
|
||||||
|
else:
|
||||||
|
data = self.get_json(_USER_INFO_URL, params={
|
||||||
|
'access_token': access_token,
|
||||||
|
'code': kwargs['request'].GET.get('code')
|
||||||
|
})
|
||||||
|
|
||||||
|
nickname = data.get('nickname')
|
||||||
|
if nickname:
|
||||||
|
# weixin api has some encode bug, here need handle
|
||||||
|
data['nickname'] = nickname.encode(
|
||||||
|
'raw_unicode_escape'
|
||||||
|
).decode('utf-8')
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def auth_params(self, state=None):
|
||||||
|
appid, secret = self.get_key_and_secret()
|
||||||
|
|
||||||
|
if WEIXIN_WORK_SP:
|
||||||
|
params = {
|
||||||
|
'appid': appid,
|
||||||
|
'redirect_uri': self.get_redirect_uri(state),
|
||||||
|
'usertype': 'member',
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
params = {
|
||||||
|
'appid': appid,
|
||||||
|
'redirect_uri': self.get_redirect_uri(state),
|
||||||
|
'agentid': self.setting('AGENTID'),
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.STATE_PARAMETER and state:
|
||||||
|
params['state'] = state
|
||||||
|
if self.RESPONSE_TYPE:
|
||||||
|
params['response_type'] = self.RESPONSE_TYPE
|
||||||
|
return params
|
||||||
|
|
||||||
|
def auth_complete_params(self, state=None):
|
||||||
|
appid, secret = self.get_key_and_secret()
|
||||||
|
if WEIXIN_WORK_SP is True:
|
||||||
|
return {
|
||||||
|
'corpid': appid,
|
||||||
|
'provider_secret': secret,
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
'grant_type': 'authorization_code', # request auth code
|
||||||
|
'code': self.data.get('code', ''), # server response code
|
||||||
|
'appid': appid,
|
||||||
|
'secret': secret,
|
||||||
|
'redirect_uri': self.get_redirect_uri(state),
|
||||||
|
}
|
||||||
|
|
||||||
|
def refresh_token_params(self, token, *args, **kwargs):
|
||||||
|
appid, secret = self.get_key_and_secret()
|
||||||
|
return {
|
||||||
|
'refresh_token': token,
|
||||||
|
'grant_type': 'refresh_token',
|
||||||
|
'appid': appid,
|
||||||
|
'secret': secret
|
||||||
|
}
|
||||||
|
|
||||||
|
def access_token_url(self, appid, secret):
|
||||||
|
if WEIXIN_WORK_SP:
|
||||||
|
return self.ACCESS_TOKEN_URL
|
||||||
|
else:
|
||||||
|
return self.ACCESS_TOKEN_URL + '?corpid=%s&corpsecret=%s' % (appid, secret)
|
||||||
|
|
||||||
|
def auth_complete(self, *args, **kwargs):
|
||||||
|
"""Completes loging process, must return user instance"""
|
||||||
|
self.process_error(self.data)
|
||||||
|
|
||||||
|
appid, secret = self.get_key_and_secret()
|
||||||
|
try:
|
||||||
|
if WEIXIN_WORK_SP:
|
||||||
|
response = self.request_access_token(
|
||||||
|
self.access_token_url(appid, secret),
|
||||||
|
json=self.auth_complete_params(self.validate_state()),
|
||||||
|
headers={'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json'},
|
||||||
|
method=self.ACCESS_TOKEN_METHOD
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
response = self.request_access_token(
|
||||||
|
self.access_token_url(appid, secret),
|
||||||
|
data=self.auth_complete_params(self.validate_state()),
|
||||||
|
headers=self.auth_headers(),
|
||||||
|
method=self.ACCESS_TOKEN_METHOD
|
||||||
|
)
|
||||||
|
except HTTPError as err:
|
||||||
|
if err.response.status_code == 400:
|
||||||
|
raise AuthCanceled(self, response=err.response)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
except KeyError:
|
||||||
|
raise AuthUnknownError(self)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if response['errmsg'] != 'ok':
|
||||||
|
raise AuthCanceled(self)
|
||||||
|
except KeyError:
|
||||||
|
pass # assume response is ok if 'errmsg' key not found
|
||||||
|
|
||||||
|
self.process_error(response)
|
||||||
|
|
||||||
|
access_token = response['provider_access_token'] if WEIXIN_WORK_SP else response['access_token']
|
||||||
|
return self.do_auth(access_token, response=response,
|
||||||
|
*args, **kwargs)
|
0
seahub/social_core/pipeline/__init__.py
Normal file
0
seahub/social_core/pipeline/__init__.py
Normal file
34
seahub/social_core/pipeline/social_auth.py
Normal file
34
seahub/social_core/pipeline/social_auth.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
from social_core.exceptions import AuthAlreadyAssociated
|
||||||
|
|
||||||
|
def social_user(backend, uid, user=None, *args, **kwargs):
|
||||||
|
provider = backend.name
|
||||||
|
social = backend.strategy.storage.user.get_social_auth(provider, uid)
|
||||||
|
if social:
|
||||||
|
if user and social.user.username != user.username:
|
||||||
|
msg = 'This {0} account is already in use.'.format(provider)
|
||||||
|
raise AuthAlreadyAssociated(backend, msg)
|
||||||
|
elif not user:
|
||||||
|
user = social.user
|
||||||
|
return {'social': social,
|
||||||
|
'user': user,
|
||||||
|
'is_new': user is None,
|
||||||
|
'new_association': social is None}
|
||||||
|
|
||||||
|
|
||||||
|
def associate_user(backend, uid, user=None, social=None, *args, **kwargs):
|
||||||
|
if user and not social:
|
||||||
|
try:
|
||||||
|
social = backend.strategy.storage.user.create_social_auth(
|
||||||
|
user, uid, backend.name
|
||||||
|
)
|
||||||
|
except Exception as err:
|
||||||
|
if not backend.strategy.storage.is_integrity_error(err):
|
||||||
|
raise
|
||||||
|
# Protect for possible race condition, those bastard with FTL
|
||||||
|
# clicking capabilities, check issue #131:
|
||||||
|
# https://github.com/omab/django-social-auth/issues/131
|
||||||
|
return social_user(backend, uid, user, *args, **kwargs)
|
||||||
|
else:
|
||||||
|
return {'social': social,
|
||||||
|
'user': user,
|
||||||
|
'new_association': True}
|
87
seahub/social_core/pipeline/user.py
Normal file
87
seahub/social_core/pipeline/user.py
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
from seahub.profile.models import Profile
|
||||||
|
from seahub.utils.auth import gen_user_virtual_id
|
||||||
|
|
||||||
|
USER_FIELDS = ['username', 'email']
|
||||||
|
|
||||||
|
|
||||||
|
def get_username(strategy, details, backend, user=None, *args, **kwargs):
|
||||||
|
if 'username' not in backend.setting('USER_FIELDS', USER_FIELDS):
|
||||||
|
return
|
||||||
|
storage = strategy.storage
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
final_username = gen_user_virtual_id()
|
||||||
|
else:
|
||||||
|
final_username = storage.user.get_username(user)
|
||||||
|
|
||||||
|
return {'username': final_username}
|
||||||
|
|
||||||
|
|
||||||
|
def create_user(strategy, details, backend, user=None, *args, **kwargs):
|
||||||
|
if user:
|
||||||
|
return {'is_new': False}
|
||||||
|
|
||||||
|
fields = dict((name, kwargs.get(name, details.get(name)))
|
||||||
|
for name in backend.setting('USER_FIELDS', USER_FIELDS))
|
||||||
|
if not fields:
|
||||||
|
return
|
||||||
|
|
||||||
|
return {
|
||||||
|
'is_new': True,
|
||||||
|
'user': strategy.create_user(**fields)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def save_profile(strategy, details, backend, user=None, *args, **kwargs):
|
||||||
|
if not user:
|
||||||
|
return
|
||||||
|
email = details.get('email', '')
|
||||||
|
if email:
|
||||||
|
Profile.objects.add_or_update(username=user.username,
|
||||||
|
contact_email=email)
|
||||||
|
|
||||||
|
fullname = details.get('fullname', '')
|
||||||
|
if fullname:
|
||||||
|
Profile.objects.add_or_update(username=user.username,
|
||||||
|
nickname=fullname)
|
||||||
|
|
||||||
|
# weixin username and profile_image_url
|
||||||
|
nickname = details.get('username', '')
|
||||||
|
if nickname:
|
||||||
|
Profile.objects.add_or_update(username=user.username,
|
||||||
|
nickname=nickname)
|
||||||
|
|
||||||
|
avatar_url = details.get('profile_image_url', '')
|
||||||
|
if avatar_url:
|
||||||
|
_update_user_avatar(user, avatar_url)
|
||||||
|
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
import urllib2
|
||||||
|
from django.core.files import File
|
||||||
|
from seahub.avatar.models import Avatar
|
||||||
|
from seahub.avatar.signals import avatar_updated
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def _update_user_avatar(user, pic):
|
||||||
|
if not pic:
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info("retrieve pic from %s" % pic)
|
||||||
|
|
||||||
|
filedata = urllib2.urlopen(pic)
|
||||||
|
datatowrite = filedata.read()
|
||||||
|
filename = '/tmp/%s.jpg' % user.username
|
||||||
|
with open(filename, 'wb') as f:
|
||||||
|
f.write(datatowrite)
|
||||||
|
|
||||||
|
logger.info("save pic to %s" % filename)
|
||||||
|
avatar = Avatar(emailuser=user.username, primary=True)
|
||||||
|
avatar.avatar.save(
|
||||||
|
'image.jpg', File(open(filename))
|
||||||
|
)
|
||||||
|
avatar.save()
|
||||||
|
avatar_updated.send(sender=Avatar, user=user, avatar=avatar)
|
||||||
|
|
||||||
|
os.remove(filename)
|
275
seahub/social_core/utils/WXBizMsgCrypt.py
Normal file
275
seahub/social_core/utils/WXBizMsgCrypt.py
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
#-*- encoding:utf-8 -*-
|
||||||
|
|
||||||
|
""" 对企业微信发送给企业后台的消息加解密示例代码.
|
||||||
|
@copyright: Copyright (c) 1998-2014 Tencent Inc.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# ------------------------------------------------------------------------
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import string
|
||||||
|
import random
|
||||||
|
import hashlib
|
||||||
|
import time
|
||||||
|
import struct
|
||||||
|
try:
|
||||||
|
from Crypto.Cipher import AES
|
||||||
|
except ImportError:
|
||||||
|
AES = None
|
||||||
|
import xml.etree.cElementTree as ET
|
||||||
|
import sys
|
||||||
|
import socket
|
||||||
|
reload(sys)
|
||||||
|
from . import ierror
|
||||||
|
sys.setdefaultencoding('utf-8')
|
||||||
|
|
||||||
|
"""
|
||||||
|
关于Crypto.Cipher模块,ImportError: No module named 'Crypto'解决方案
|
||||||
|
请到官方网站 https://www.dlitz.net/software/pycrypto/ 下载pycrypto。
|
||||||
|
下载后,按照README中的“Installation”小节的提示进行pycrypto安装。
|
||||||
|
"""
|
||||||
|
class FormatException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def throw_exception(message, exception_class=FormatException):
|
||||||
|
"""my define raise exception function"""
|
||||||
|
raise exception_class(message)
|
||||||
|
|
||||||
|
class SHA1:
|
||||||
|
"""计算企业微信的消息签名接口"""
|
||||||
|
|
||||||
|
def getSHA1(self, token, timestamp, nonce, encrypt):
|
||||||
|
"""用SHA1算法生成安全签名
|
||||||
|
@param token: 票据
|
||||||
|
@param timestamp: 时间戳
|
||||||
|
@param encrypt: 密文
|
||||||
|
@param nonce: 随机字符串
|
||||||
|
@return: 安全签名
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
sortlist = [token, timestamp, nonce, encrypt]
|
||||||
|
sortlist.sort()
|
||||||
|
sha = hashlib.sha1()
|
||||||
|
sha.update("".join(sortlist))
|
||||||
|
return ierror.WXBizMsgCrypt_OK, sha.hexdigest()
|
||||||
|
except Exception,e:
|
||||||
|
print e
|
||||||
|
return ierror.WXBizMsgCrypt_ComputeSignature_Error, None
|
||||||
|
|
||||||
|
|
||||||
|
class XMLParse:
|
||||||
|
"""提供提取消息格式中的密文及生成回复消息格式的接口"""
|
||||||
|
|
||||||
|
# xml消息模板
|
||||||
|
AES_TEXT_RESPONSE_TEMPLATE = """<xml>
|
||||||
|
<Encrypt><![CDATA[%(msg_encrypt)s]]></Encrypt>
|
||||||
|
<MsgSignature><![CDATA[%(msg_signaturet)s]]></MsgSignature>
|
||||||
|
<TimeStamp>%(timestamp)s</TimeStamp>
|
||||||
|
<Nonce><![CDATA[%(nonce)s]]></Nonce>
|
||||||
|
</xml>"""
|
||||||
|
|
||||||
|
def extract(self, xmltext):
|
||||||
|
"""提取出xml数据包中的加密消息
|
||||||
|
@param xmltext: 待提取的xml字符串
|
||||||
|
@return: 提取出的加密消息字符串
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
xml_tree = ET.fromstring(xmltext)
|
||||||
|
encrypt = xml_tree.find("Encrypt")
|
||||||
|
return ierror.WXBizMsgCrypt_OK, encrypt.text
|
||||||
|
except Exception,e:
|
||||||
|
print e
|
||||||
|
return ierror.WXBizMsgCrypt_ParseXml_Error,None,None
|
||||||
|
|
||||||
|
def generate(self, encrypt, signature, timestamp, nonce):
|
||||||
|
"""生成xml消息
|
||||||
|
@param encrypt: 加密后的消息密文
|
||||||
|
@param signature: 安全签名
|
||||||
|
@param timestamp: 时间戳
|
||||||
|
@param nonce: 随机字符串
|
||||||
|
@return: 生成的xml字符串
|
||||||
|
"""
|
||||||
|
resp_dict = {
|
||||||
|
'msg_encrypt' : encrypt,
|
||||||
|
'msg_signaturet': signature,
|
||||||
|
'timestamp' : timestamp,
|
||||||
|
'nonce' : nonce,
|
||||||
|
}
|
||||||
|
resp_xml = self.AES_TEXT_RESPONSE_TEMPLATE % resp_dict
|
||||||
|
return resp_xml
|
||||||
|
|
||||||
|
|
||||||
|
class PKCS7Encoder():
|
||||||
|
"""提供基于PKCS7算法的加解密接口"""
|
||||||
|
|
||||||
|
block_size = 32
|
||||||
|
def encode(self, text):
|
||||||
|
""" 对需要加密的明文进行填充补位
|
||||||
|
@param text: 需要进行填充补位操作的明文
|
||||||
|
@return: 补齐明文字符串
|
||||||
|
"""
|
||||||
|
text_length = len(text)
|
||||||
|
# 计算需要填充的位数
|
||||||
|
amount_to_pad = self.block_size - (text_length % self.block_size)
|
||||||
|
if amount_to_pad == 0:
|
||||||
|
amount_to_pad = self.block_size
|
||||||
|
# 获得补位所用的字符
|
||||||
|
pad = chr(amount_to_pad)
|
||||||
|
return text + pad * amount_to_pad
|
||||||
|
|
||||||
|
def decode(self, decrypted):
|
||||||
|
"""删除解密后明文的补位字符
|
||||||
|
@param decrypted: 解密后的明文
|
||||||
|
@return: 删除补位字符后的明文
|
||||||
|
"""
|
||||||
|
pad = ord(decrypted[-1])
|
||||||
|
if pad<1 or pad >32:
|
||||||
|
pad = 0
|
||||||
|
return decrypted[:-pad]
|
||||||
|
|
||||||
|
|
||||||
|
class Prpcrypt(object):
|
||||||
|
"""提供接收和推送给企业微信消息的加解密接口"""
|
||||||
|
|
||||||
|
def __init__(self,key):
|
||||||
|
|
||||||
|
#self.key = base64.b64decode(key+"=")
|
||||||
|
self.key = key
|
||||||
|
# 设置加解密模式为AES的CBC模式
|
||||||
|
self.mode = AES.MODE_CBC
|
||||||
|
|
||||||
|
|
||||||
|
def encrypt(self,text,receiveid):
|
||||||
|
"""对明文进行加密
|
||||||
|
@param text: 需要加密的明文
|
||||||
|
@return: 加密得到的字符串
|
||||||
|
"""
|
||||||
|
# 16位随机字符串添加到明文开头
|
||||||
|
text = self.get_random_str() + struct.pack("I",socket.htonl(len(text))) + text + receiveid
|
||||||
|
# 使用自定义的填充方式对明文进行补位填充
|
||||||
|
pkcs7 = PKCS7Encoder()
|
||||||
|
text = pkcs7.encode(text)
|
||||||
|
# 加密
|
||||||
|
cryptor = AES.new(self.key,self.mode,self.key[:16])
|
||||||
|
try:
|
||||||
|
ciphertext = cryptor.encrypt(text)
|
||||||
|
# 使用BASE64对加密后的字符串进行编码
|
||||||
|
return ierror.WXBizMsgCrypt_OK, base64.b64encode(ciphertext)
|
||||||
|
except Exception,e:
|
||||||
|
print e
|
||||||
|
return ierror.WXBizMsgCrypt_EncryptAES_Error,None
|
||||||
|
|
||||||
|
def decrypt(self,text,receiveid):
|
||||||
|
"""对解密后的明文进行补位删除
|
||||||
|
@param text: 密文
|
||||||
|
@return: 删除填充补位后的明文
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
cryptor = AES.new(self.key,self.mode,self.key[:16])
|
||||||
|
# 使用BASE64对密文进行解码,然后AES-CBC解密
|
||||||
|
plain_text = cryptor.decrypt(base64.b64decode(text))
|
||||||
|
except Exception,e:
|
||||||
|
print e
|
||||||
|
return ierror.WXBizMsgCrypt_DecryptAES_Error,None
|
||||||
|
try:
|
||||||
|
pad = ord(plain_text[-1])
|
||||||
|
# 去掉补位字符串
|
||||||
|
#pkcs7 = PKCS7Encoder()
|
||||||
|
#plain_text = pkcs7.encode(plain_text)
|
||||||
|
# 去除16位随机字符串
|
||||||
|
content = plain_text[16:-pad]
|
||||||
|
xml_len = socket.ntohl(struct.unpack("I",content[ : 4])[0])
|
||||||
|
xml_content = content[4 : xml_len+4]
|
||||||
|
from_receiveid = content[xml_len+4:]
|
||||||
|
except Exception,e:
|
||||||
|
print e
|
||||||
|
return ierror.WXBizMsgCrypt_IllegalBuffer,None
|
||||||
|
if from_receiveid != receiveid:
|
||||||
|
return ierror.WXBizMsgCrypt_ValidateCorpid_Error,None
|
||||||
|
return 0,xml_content
|
||||||
|
|
||||||
|
def get_random_str(self):
|
||||||
|
""" 随机生成16位字符串
|
||||||
|
@return: 16位字符串
|
||||||
|
"""
|
||||||
|
rule = string.letters + string.digits
|
||||||
|
str = random.sample(rule, 16)
|
||||||
|
return "".join(str)
|
||||||
|
|
||||||
|
class WXBizMsgCrypt(object):
|
||||||
|
#构造函数
|
||||||
|
def __init__(self,sToken,sEncodingAESKey,sReceiveId):
|
||||||
|
try:
|
||||||
|
self.key = base64.b64decode(sEncodingAESKey+"=")
|
||||||
|
assert len(self.key) == 32
|
||||||
|
except:
|
||||||
|
throw_exception("[error]: EncodingAESKey unvalid !", FormatException)
|
||||||
|
# return ierror.WXBizMsgCrypt_IllegalAesKey,None
|
||||||
|
self.m_sToken = sToken
|
||||||
|
self.m_sReceiveId = sReceiveId
|
||||||
|
|
||||||
|
#验证URL
|
||||||
|
#@param sMsgSignature: 签名串,对应URL参数的msg_signature
|
||||||
|
#@param sTimeStamp: 时间戳,对应URL参数的timestamp
|
||||||
|
#@param sNonce: 随机串,对应URL参数的nonce
|
||||||
|
#@param sEchoStr: 随机串,对应URL参数的echostr
|
||||||
|
#@param sReplyEchoStr: 解密之后的echostr,当return返回0时有效
|
||||||
|
#@return:成功0,失败返回对应的错误码
|
||||||
|
|
||||||
|
def VerifyURL(self, sMsgSignature, sTimeStamp, sNonce, sEchoStr):
|
||||||
|
sha1 = SHA1()
|
||||||
|
ret,signature = sha1.getSHA1(self.m_sToken, sTimeStamp, sNonce, sEchoStr)
|
||||||
|
if ret != 0:
|
||||||
|
return ret, None
|
||||||
|
if not signature == sMsgSignature:
|
||||||
|
return ierror.WXBizMsgCrypt_ValidateSignature_Error, None
|
||||||
|
pc = Prpcrypt(self.key)
|
||||||
|
ret,sReplyEchoStr = pc.decrypt(sEchoStr,self.m_sReceiveId)
|
||||||
|
return ret,sReplyEchoStr
|
||||||
|
|
||||||
|
def EncryptMsg(self, sReplyMsg, sNonce, timestamp = None):
|
||||||
|
#将企业回复用户的消息加密打包
|
||||||
|
#@param sReplyMsg: 企业号待回复用户的消息,xml格式的字符串
|
||||||
|
#@param sTimeStamp: 时间戳,可以自己生成,也可以用URL参数的timestamp,如为None则自动用当前时间
|
||||||
|
#@param sNonce: 随机串,可以自己生成,也可以用URL参数的nonce
|
||||||
|
#sEncryptMsg: 加密后的可以直接回复用户的密文,包括msg_signature, timestamp, nonce, encrypt的xml格式的字符串,
|
||||||
|
#return:成功0,sEncryptMsg,失败返回对应的错误码None
|
||||||
|
pc = Prpcrypt(self.key)
|
||||||
|
ret,encrypt = pc.encrypt(sReplyMsg, self.m_sReceiveId)
|
||||||
|
if ret != 0:
|
||||||
|
return ret,None
|
||||||
|
if timestamp is None:
|
||||||
|
timestamp = str(int(time.time()))
|
||||||
|
# 生成安全签名
|
||||||
|
sha1 = SHA1()
|
||||||
|
ret,signature = sha1.getSHA1(self.m_sToken, timestamp, sNonce, encrypt)
|
||||||
|
if ret != 0:
|
||||||
|
return ret,None
|
||||||
|
xmlParse = XMLParse()
|
||||||
|
return ret,xmlParse.generate(encrypt, signature, timestamp, sNonce)
|
||||||
|
|
||||||
|
def DecryptMsg(self, sPostData, sMsgSignature, sTimeStamp, sNonce):
|
||||||
|
# 检验消息的真实性,并且获取解密后的明文
|
||||||
|
# @param sMsgSignature: 签名串,对应URL参数的msg_signature
|
||||||
|
# @param sTimeStamp: 时间戳,对应URL参数的timestamp
|
||||||
|
# @param sNonce: 随机串,对应URL参数的nonce
|
||||||
|
# @param sPostData: 密文,对应POST请求的数据
|
||||||
|
# xml_content: 解密后的原文,当return返回0时有效
|
||||||
|
# @return: 成功0,失败返回对应的错误码
|
||||||
|
# 验证安全签名
|
||||||
|
xmlParse = XMLParse()
|
||||||
|
ret,encrypt = xmlParse.extract(sPostData)
|
||||||
|
if ret != 0:
|
||||||
|
return ret, None
|
||||||
|
sha1 = SHA1()
|
||||||
|
ret,signature = sha1.getSHA1(self.m_sToken, sTimeStamp, sNonce, encrypt)
|
||||||
|
if ret != 0:
|
||||||
|
return ret, None
|
||||||
|
if not signature == sMsgSignature:
|
||||||
|
return ierror.WXBizMsgCrypt_ValidateSignature_Error, None
|
||||||
|
pc = Prpcrypt(self.key)
|
||||||
|
ret,xml_content = pc.decrypt(encrypt,self.m_sReceiveId)
|
||||||
|
return ret,xml_content
|
||||||
|
|
||||||
|
|
0
seahub/social_core/utils/__init__.py
Normal file
0
seahub/social_core/utils/__init__.py
Normal file
20
seahub/social_core/utils/ierror.py
Normal file
20
seahub/social_core/utils/ierror.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#########################################################################
|
||||||
|
# Author: jonyqin
|
||||||
|
# Created Time: Thu 11 Sep 2014 01:53:58 PM CST
|
||||||
|
# File Name: ierror.py
|
||||||
|
# Description:定义错误码含义
|
||||||
|
#########################################################################
|
||||||
|
WXBizMsgCrypt_OK = 0
|
||||||
|
WXBizMsgCrypt_ValidateSignature_Error = -40001
|
||||||
|
WXBizMsgCrypt_ParseXml_Error = -40002
|
||||||
|
WXBizMsgCrypt_ComputeSignature_Error = -40003
|
||||||
|
WXBizMsgCrypt_IllegalAesKey = -40004
|
||||||
|
WXBizMsgCrypt_ValidateCorpid_Error = -40005
|
||||||
|
WXBizMsgCrypt_EncryptAES_Error = -40006
|
||||||
|
WXBizMsgCrypt_DecryptAES_Error = -40007
|
||||||
|
WXBizMsgCrypt_IllegalBuffer = -40008
|
||||||
|
WXBizMsgCrypt_EncodeBase64_Error = -40009
|
||||||
|
WXBizMsgCrypt_DecodeBase64_Error = -40010
|
||||||
|
WXBizMsgCrypt_GenReturnXml_Error = -40011
|
145
seahub/social_core/views.py
Normal file
145
seahub/social_core/views.py
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.cache import cache
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
from django.http import HttpResponse, HttpResponseRedirect
|
||||||
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
from django.utils.http import urlquote
|
||||||
|
|
||||||
|
from seahub.social_core.utils.WXBizMsgCrypt import WXBizMsgCrypt
|
||||||
|
from seahub.utils.urls import abs_reverse
|
||||||
|
|
||||||
|
# Get an instance of a logger
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@csrf_exempt
|
||||||
|
def weixin_work_cb(request):
|
||||||
|
"""Callback for weixin work provider API.
|
||||||
|
|
||||||
|
Used in callback config at app details page.
|
||||||
|
e.g. https://open.work.weixin.qq.com/wwopen/developer#/sass/apps/detail/ww24c53566499d354f
|
||||||
|
|
||||||
|
ref: https://work.weixin.qq.com/api/doc#90001/90143/91116
|
||||||
|
"""
|
||||||
|
|
||||||
|
token = settings.SOCIAL_AUTH_WEIXIN_WORK_TOKEN
|
||||||
|
EncodingAESKey = settings.SOCIAL_AUTH_WEIXIN_WORK_AES_KEY
|
||||||
|
|
||||||
|
msg_signature = request.GET.get('msg_signature', None)
|
||||||
|
timestamp = request.GET.get('timestamp', None)
|
||||||
|
nonce = request.GET.get('nonce', None)
|
||||||
|
if not (msg_signature and timestamp and nonce):
|
||||||
|
assert False, 'Request Error'
|
||||||
|
|
||||||
|
if request.method == 'GET':
|
||||||
|
wxcpt = WXBizMsgCrypt(token, EncodingAESKey,
|
||||||
|
settings.SOCIAL_AUTH_WEIXIN_WORK_KEY)
|
||||||
|
|
||||||
|
echostr = request.GET.get('echostr', '')
|
||||||
|
ret, decoded_echostr = wxcpt.VerifyURL(msg_signature, timestamp, nonce, echostr)
|
||||||
|
if ret != 0:
|
||||||
|
assert False, 'Verify Error'
|
||||||
|
|
||||||
|
return HttpResponse(decoded_echostr)
|
||||||
|
|
||||||
|
elif request.method == 'POST':
|
||||||
|
wxcpt = WXBizMsgCrypt(token, EncodingAESKey,
|
||||||
|
settings.SOCIAL_AUTH_WEIXIN_WORK_SUITID)
|
||||||
|
|
||||||
|
ret, xml_msg = wxcpt.DecryptMsg(request.body, msg_signature, timestamp, nonce)
|
||||||
|
if ret != 0:
|
||||||
|
assert False, 'Decrypt Error'
|
||||||
|
|
||||||
|
import xml.etree.cElementTree as ET
|
||||||
|
xml_tree = ET.fromstring(xml_msg)
|
||||||
|
suite_ticket = xml_tree.find("SuiteTicket").text
|
||||||
|
logger.info('suite ticket: %s' % suite_ticket)
|
||||||
|
|
||||||
|
# TODO: use persistent store
|
||||||
|
cache.set('wx_work_suite_ticket', suite_ticket, 3600)
|
||||||
|
|
||||||
|
return HttpResponse('success')
|
||||||
|
|
||||||
|
def _get_suite_access_token():
|
||||||
|
suite_access_token = cache.get('wx_work_suite_access_token', None)
|
||||||
|
if suite_access_token:
|
||||||
|
return suite_access_token
|
||||||
|
|
||||||
|
suite_ticket = cache.get('wx_work_suite_ticket', None)
|
||||||
|
if not suite_ticket:
|
||||||
|
assert False, 'suite ticket is None!'
|
||||||
|
|
||||||
|
get_suite_token_url = 'https://qyapi.weixin.qq.com/cgi-bin/service/get_suite_token'
|
||||||
|
resp = requests.request(
|
||||||
|
'POST', get_suite_token_url,
|
||||||
|
json={
|
||||||
|
"suite_id": settings.SOCIAL_AUTH_WEIXIN_WORK_SUITID,
|
||||||
|
"suite_secret": settings.SOCIAL_AUTH_WEIXIN_WORK_SUIT_SECRET,
|
||||||
|
"suite_ticket": suite_ticket,
|
||||||
|
},
|
||||||
|
headers={'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json'},
|
||||||
|
)
|
||||||
|
|
||||||
|
suite_access_token = resp.json().get('suite_access_token', None)
|
||||||
|
if not suite_access_token:
|
||||||
|
logger.error('Failed to get suite_access_token!')
|
||||||
|
logger.error(resp.content)
|
||||||
|
assert False, 'suite_access_token is None!'
|
||||||
|
else:
|
||||||
|
cache.set('wx_work_suite_access_token', suite_access_token, 3600)
|
||||||
|
return suite_access_token
|
||||||
|
|
||||||
|
def weixin_work_3rd_app_install(request):
|
||||||
|
"""Redirect user to weixin work 3rd app install page.
|
||||||
|
"""
|
||||||
|
# 0. get suite access token
|
||||||
|
suite_access_token = _get_suite_access_token()
|
||||||
|
print('suite access token', suite_access_token)
|
||||||
|
|
||||||
|
# 1. get pre_auth_code
|
||||||
|
get_pre_auth_code_url = 'https://qyapi.weixin.qq.com/cgi-bin/service/get_pre_auth_code?suite_access_token=' + suite_access_token
|
||||||
|
resp = requests.request('GET', get_pre_auth_code_url)
|
||||||
|
|
||||||
|
pre_auth_code = resp.json().get('pre_auth_code', None)
|
||||||
|
if not pre_auth_code:
|
||||||
|
logger.error('Failed to get pre_auth_code')
|
||||||
|
logger.error(resp.content)
|
||||||
|
assert False, 'pre_auth_code is None'
|
||||||
|
|
||||||
|
# 2. set session info
|
||||||
|
# ref: https://work.weixin.qq.com/api/doc#90001/90143/90602
|
||||||
|
url = 'https://qyapi.weixin.qq.com/cgi-bin/service/set_session_info?suite_access_token=' + suite_access_token
|
||||||
|
resp = requests.request(
|
||||||
|
'POST', url,
|
||||||
|
json={
|
||||||
|
"pre_auth_code": pre_auth_code,
|
||||||
|
"session_info":
|
||||||
|
{
|
||||||
|
"appid": [],
|
||||||
|
"auth_type": 1 # TODO: 0: production; 1: testing.
|
||||||
|
}
|
||||||
|
},
|
||||||
|
headers={'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json'},
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
# TODO: use random state
|
||||||
|
url = 'https://open.work.weixin.qq.com/3rdapp/install?suite_id=%s&pre_auth_code=%s&redirect_uri=%s&state=STATE123' % (
|
||||||
|
settings.SOCIAL_AUTH_WEIXIN_WORK_SUITID,
|
||||||
|
pre_auth_code,
|
||||||
|
abs_reverse('weixin_work_3rd_app_install_cb'),
|
||||||
|
)
|
||||||
|
return HttpResponseRedirect(url)
|
||||||
|
|
||||||
|
@csrf_exempt
|
||||||
|
def weixin_work_3rd_app_install_cb(request):
|
||||||
|
"""Callback for weixin work 3rd app install API.
|
||||||
|
|
||||||
|
https://work.weixin.qq.com/api/doc#90001/90143/90597
|
||||||
|
"""
|
||||||
|
# TODO: check state
|
||||||
|
pass
|
@@ -137,6 +137,7 @@ urlpatterns = [
|
|||||||
url(r'^sso/$', sso, name='sso'),
|
url(r'^sso/$', sso, name='sso'),
|
||||||
url(r'^shib-login/', shib_login, name="shib_login"),
|
url(r'^shib-login/', shib_login, name="shib_login"),
|
||||||
url(r'^oauth/', include('seahub.oauth.urls')),
|
url(r'^oauth/', include('seahub.oauth.urls')),
|
||||||
|
url(r'^social/', include('social_django.urls', namespace='social')),
|
||||||
|
|
||||||
url(r'^$', libraries, name='libraries'),
|
url(r'^$', libraries, name='libraries'),
|
||||||
#url(r'^home/$', direct_to_template, { 'template': 'home.html' } ),
|
#url(r'^home/$', direct_to_template, { 'template': 'home.html' } ),
|
||||||
@@ -693,3 +694,15 @@ if getattr(settings, 'ENABLE_CAS', False):
|
|||||||
url(r'^accounts/cas-logout/$', cas_logout, name='cas_ng_logout'),
|
url(r'^accounts/cas-logout/$', cas_logout, name='cas_ng_logout'),
|
||||||
url(r'^accounts/cas-callback/$', cas_callback, name='cas_ng_proxy_callback'),
|
url(r'^accounts/cas-callback/$', cas_callback, name='cas_ng_proxy_callback'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
from seahub.social_core.views import (
|
||||||
|
weixin_work_cb, weixin_work_3rd_app_install, weixin_work_3rd_app_install_cb
|
||||||
|
)
|
||||||
|
|
||||||
|
urlpatterns += [
|
||||||
|
url(r'^weixin-work/callback/$', weixin_work_cb),
|
||||||
|
url(r'^weixin-work/3rd-app-install/$', weixin_work_3rd_app_install),
|
||||||
|
url(r'^weixin-work/3rd-app-install/callback/$',
|
||||||
|
weixin_work_3rd_app_install_cb, name='weixin_work_3rd_app_install_cb'),
|
||||||
|
]
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
from seahub.settings import LOGIN_BG_IMAGE_PATH, MEDIA_ROOT
|
from seahub.settings import LOGIN_BG_IMAGE_PATH, MEDIA_ROOT
|
||||||
|
from seahub.utils import gen_token
|
||||||
|
|
||||||
def get_login_bg_image_path():
|
def get_login_bg_image_path():
|
||||||
""" Return custom background image path if it exists, otherwise return default background image path.
|
""" Return custom background image path if it exists, otherwise return default background image path.
|
||||||
@@ -15,3 +16,6 @@ def get_custom_login_bg_image_path():
|
|||||||
""" Ensure consistency between utils and api.
|
""" Ensure consistency between utils and api.
|
||||||
"""
|
"""
|
||||||
return 'custom/login-bg.jpg'
|
return 'custom/login-bg.jpg'
|
||||||
|
|
||||||
|
def gen_user_virtual_id():
|
||||||
|
return gen_token(max_length=32) + '@auth.local'
|
||||||
|
7
seahub/utils/urls.py
Normal file
7
seahub/utils/urls.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
|
||||||
|
from seahub.utils import get_site_scheme_and_netloc
|
||||||
|
|
||||||
|
def abs_reverse(viewname, urlconf=None, args=None, kwargs=None, current_app=None):
|
||||||
|
return get_site_scheme_and_netloc().rstrip('/') + reverse(
|
||||||
|
viewname, urlconf, args, kwargs, current_app)
|
0
tests/seahub/social_core/__init__.py
Normal file
0
tests/seahub/social_core/__init__.py
Normal file
22
tests/seahub/social_core/test_views.py
Normal file
22
tests/seahub/social_core/test_views.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import os
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from seahub.test_utils import BaseTestCase
|
||||||
|
|
||||||
|
TRAVIS = 'TRAVIS' in os.environ
|
||||||
|
|
||||||
|
|
||||||
|
class WeixinWorkCBTest(BaseTestCase):
|
||||||
|
@pytest.mark.skipif(TRAVIS, reason="This test can only be run in local.")
|
||||||
|
def test_get(self, ):
|
||||||
|
resp = self.client.get('/weixin-work/callback/?msg_signature=61a7d120857cdb70d8b936ec5b6e8ed172a41926×tamp=1543304575&nonce=1542460575&echostr=9uB%2FReg5PQk%2FjzejPjhjWmvKXuxh0R4VK7BJRP62lfRj5kZhuAu0mLMM7hnREJQTJxWWw3Y1BB%2F%2FLkE3V88auA%3D%3D')
|
||||||
|
assert resp.content == '6819653789729882111'
|
||||||
|
|
||||||
|
@pytest.mark.skipif(TRAVIS, reason="This test can only be run in local.")
|
||||||
|
def test_post(self, ):
|
||||||
|
data = '<xml><ToUserName><![CDATA[ww24c53566499d354f]]></ToUserName><Encrypt><![CDATA[1fBBPRF7NW4ocCIWFIZK/Pjcn5a0okyx3O8OdbX6Ci2MYq34NaIWuK9jW6dq8pVORvUUsxNP0RVD3vqpq94P932bMyBNKHvFgdn62NaM3vUCSN2SJhwlvNp1KDqMDCX+oiMjcSWJFWXJ0daTpxycSJ88LKH1tA/Z3n18yGq7qs/7qmFJp2kaL6/sb9ATWriA/BCH5UhOaJolqLNm281yAbap+1myr2ELCHPqWz0Gd6Zpvolab6caAp+ivAK5+LohgkrppAjkW7CXI1yM08X0VNArmIT55ZKTFwSW6jeMTBUIIVdYimAKxfxmITxtcu7dVGFQ63hyJTtH6MI0yc7wZRL2ZX9OR5cbO5WTksXv0Rai/3lGSPjThOUS02EI8j4h]]></Encrypt><AgentID><![CDATA[]]></AgentID></xml>'
|
||||||
|
resp = self.client.post(
|
||||||
|
'/weixin-work/callback/?msg_signature=a237bf482cc9ae8424010eb63a24859c731b2aa7×tamp=1543309590&nonce=1542845878',
|
||||||
|
data=data,
|
||||||
|
content_type='application/xml',
|
||||||
|
)
|
23
thirdpart/social_django/__init__.py
Normal file
23
thirdpart/social_django/__init__.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
__version__ = '2.1.0'
|
||||||
|
|
||||||
|
|
||||||
|
from social_core.backends.base import BaseAuth
|
||||||
|
|
||||||
|
# django.contrib.auth.load_backend() will import and instanciate the
|
||||||
|
# authentication backend ignoring the possibility that it might
|
||||||
|
# require more arguments. Here we set a monkey patch to
|
||||||
|
# BaseAuth.__init__ to ignore the mandatory strategy argument and load
|
||||||
|
# it.
|
||||||
|
|
||||||
|
def baseauth_init_workaround(original_init):
|
||||||
|
def fake_init(self, strategy=None, *args, **kwargs):
|
||||||
|
from .utils import load_strategy
|
||||||
|
original_init(self, strategy or load_strategy(), *args, **kwargs)
|
||||||
|
return fake_init
|
||||||
|
|
||||||
|
|
||||||
|
if not getattr(BaseAuth, '__init_patched', False):
|
||||||
|
BaseAuth.__init__ = baseauth_init_workaround(BaseAuth.__init__)
|
||||||
|
BaseAuth.__init_patched = True
|
||||||
|
|
||||||
|
default_app_config = 'social_django.config.PythonSocialAuthConfig'
|
62
thirdpart/social_django/admin.py
Normal file
62
thirdpart/social_django/admin.py
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
"""Admin settings"""
|
||||||
|
from itertools import chain
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from social_core.utils import setting_name
|
||||||
|
from .models import UserSocialAuth, Nonce, Association
|
||||||
|
|
||||||
|
|
||||||
|
class UserSocialAuthOption(admin.ModelAdmin):
|
||||||
|
"""Social Auth user options"""
|
||||||
|
list_display = ('user', 'id', 'provider', 'uid')
|
||||||
|
list_filter = ('provider',)
|
||||||
|
raw_id_fields = ('user',)
|
||||||
|
list_select_related = True
|
||||||
|
|
||||||
|
def get_search_fields(self, request=None):
|
||||||
|
search_fields = getattr(
|
||||||
|
settings, setting_name('ADMIN_USER_SEARCH_FIELDS'), None
|
||||||
|
)
|
||||||
|
if search_fields is None:
|
||||||
|
_User = UserSocialAuth.user_model()
|
||||||
|
username = getattr(_User, 'USERNAME_FIELD', None) or \
|
||||||
|
hasattr(_User, 'username') and 'username' or \
|
||||||
|
None
|
||||||
|
fieldnames = ('first_name', 'last_name', 'email', username)
|
||||||
|
all_names = self._get_all_field_names(_User._meta)
|
||||||
|
search_fields = [name for name in fieldnames
|
||||||
|
if name and name in all_names]
|
||||||
|
return ['user__' + name for name in search_fields] + \
|
||||||
|
getattr(settings, setting_name('ADMIN_SEARCH_FIELDS'), [])
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_all_field_names(model):
|
||||||
|
names = chain.from_iterable(
|
||||||
|
(field.name, field.attname)
|
||||||
|
if hasattr(field, 'attname') else (field.name,)
|
||||||
|
for field in model.get_fields()
|
||||||
|
# For complete backwards compatibility, you may want to exclude
|
||||||
|
# GenericForeignKey from the results.
|
||||||
|
if not (field.many_to_one and field.related_model is None)
|
||||||
|
)
|
||||||
|
return list(set(names))
|
||||||
|
|
||||||
|
|
||||||
|
class NonceOption(admin.ModelAdmin):
|
||||||
|
"""Nonce options"""
|
||||||
|
list_display = ('id', 'server_url', 'timestamp', 'salt')
|
||||||
|
search_fields = ('server_url',)
|
||||||
|
|
||||||
|
|
||||||
|
class AssociationOption(admin.ModelAdmin):
|
||||||
|
"""Association options"""
|
||||||
|
list_display = ('id', 'server_url', 'assoc_type')
|
||||||
|
list_filter = ('assoc_type',)
|
||||||
|
search_fields = ('server_url',)
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.register(UserSocialAuth, UserSocialAuthOption)
|
||||||
|
admin.site.register(Nonce, NonceOption)
|
||||||
|
admin.site.register(Association, AssociationOption)
|
34
thirdpart/social_django/compat.py
Normal file
34
thirdpart/social_django/compat.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# coding=utf-8
|
||||||
|
import six
|
||||||
|
import django
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
from django.urls import reverse
|
||||||
|
except ImportError:
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
|
||||||
|
try:
|
||||||
|
from django.utils.deprecation import MiddlewareMixin
|
||||||
|
except ImportError:
|
||||||
|
MiddlewareMixin = object
|
||||||
|
|
||||||
|
|
||||||
|
def get_rel_model(field):
|
||||||
|
if django.VERSION >= (2, 0):
|
||||||
|
return field.remote_field.model
|
||||||
|
|
||||||
|
user_model = field.rel.to
|
||||||
|
if isinstance(user_model, six.string_types):
|
||||||
|
app_label, model_name = user_model.split('.')
|
||||||
|
user_model = models.get_model(app_label, model_name)
|
||||||
|
return user_model
|
||||||
|
|
||||||
|
|
||||||
|
def get_request_port(request):
|
||||||
|
if django.VERSION >= (1, 9):
|
||||||
|
return request.get_port()
|
||||||
|
|
||||||
|
host_parts = request.get_host().partition(':')
|
||||||
|
return host_parts[2] or request.META['SERVER_PORT']
|
10
thirdpart/social_django/config.py
Normal file
10
thirdpart/social_django/config.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class PythonSocialAuthConfig(AppConfig):
|
||||||
|
# Full Python path to the application eg. 'django.contrib.admin'.
|
||||||
|
name = 'social_django'
|
||||||
|
# Last component of the Python path to the application eg. 'admin'.
|
||||||
|
label = 'social_django'
|
||||||
|
# Human-readable name for the application eg. "Admin".
|
||||||
|
verbose_name = 'Python Social Auth'
|
52
thirdpart/social_django/context_processors.py
Normal file
52
thirdpart/social_django/context_processors.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
from django.contrib.auth import REDIRECT_FIELD_NAME
|
||||||
|
from django.utils.functional import SimpleLazyObject
|
||||||
|
from django.utils.http import urlquote
|
||||||
|
|
||||||
|
try:
|
||||||
|
from django.utils.functional import empty as _empty
|
||||||
|
empty = _empty
|
||||||
|
except ImportError: # django < 1.4
|
||||||
|
empty = None
|
||||||
|
|
||||||
|
|
||||||
|
from social_core.backends.utils import user_backends_data
|
||||||
|
from .utils import Storage, BACKENDS
|
||||||
|
|
||||||
|
|
||||||
|
class LazyDict(SimpleLazyObject):
|
||||||
|
"""Lazy dict initialization."""
|
||||||
|
def __getitem__(self, name):
|
||||||
|
if self._wrapped is empty:
|
||||||
|
self._setup()
|
||||||
|
return self._wrapped[name]
|
||||||
|
|
||||||
|
def __setitem__(self, name, value):
|
||||||
|
if self._wrapped is empty:
|
||||||
|
self._setup()
|
||||||
|
self._wrapped[name] = value
|
||||||
|
|
||||||
|
|
||||||
|
def backends(request):
|
||||||
|
"""Load Social Auth current user data to context under the key 'backends'.
|
||||||
|
Will return the output of social_core.backends.utils.user_backends_data."""
|
||||||
|
return {'backends': LazyDict(lambda: user_backends_data(request.user,
|
||||||
|
BACKENDS,
|
||||||
|
Storage))}
|
||||||
|
|
||||||
|
|
||||||
|
def login_redirect(request):
|
||||||
|
"""Load current redirect to context."""
|
||||||
|
value = request.method == 'POST' and \
|
||||||
|
request.POST.get(REDIRECT_FIELD_NAME) or \
|
||||||
|
request.GET.get(REDIRECT_FIELD_NAME)
|
||||||
|
if value:
|
||||||
|
value = urlquote(value)
|
||||||
|
querystring = REDIRECT_FIELD_NAME + '=' + value
|
||||||
|
else:
|
||||||
|
querystring = ''
|
||||||
|
|
||||||
|
return {
|
||||||
|
'REDIRECT_FIELD_NAME': REDIRECT_FIELD_NAME,
|
||||||
|
'REDIRECT_FIELD_VALUE': value,
|
||||||
|
'REDIRECT_QUERYSTRING': querystring
|
||||||
|
}
|
94
thirdpart/social_django/fields.py
Normal file
94
thirdpart/social_django/fields.py
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import json
|
||||||
|
import six
|
||||||
|
import functools
|
||||||
|
|
||||||
|
import django
|
||||||
|
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
from social_core.utils import setting_name
|
||||||
|
|
||||||
|
try:
|
||||||
|
from django.utils.encoding import smart_unicode as smart_text
|
||||||
|
smart_text # placate pyflakes
|
||||||
|
except ImportError:
|
||||||
|
from django.utils.encoding import smart_text
|
||||||
|
|
||||||
|
# SubfieldBase causes RemovedInDjango110Warning in 1.8 and 1.9, and
|
||||||
|
# will not work in 1.10 or later
|
||||||
|
if django.VERSION[:2] >= (1, 8):
|
||||||
|
field_metaclass = type
|
||||||
|
else:
|
||||||
|
from django.db.models import SubfieldBase
|
||||||
|
field_metaclass = SubfieldBase
|
||||||
|
|
||||||
|
field_class = functools.partial(six.with_metaclass, field_metaclass)
|
||||||
|
|
||||||
|
if getattr(settings, setting_name('POSTGRES_JSONFIELD'), False):
|
||||||
|
from django.contrib.postgres.fields import JSONField as JSONFieldBase
|
||||||
|
else:
|
||||||
|
JSONFieldBase = field_class(models.TextField)
|
||||||
|
|
||||||
|
|
||||||
|
class JSONField(JSONFieldBase):
|
||||||
|
"""Simple JSON field that stores python structures as JSON strings
|
||||||
|
on database.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
kwargs.setdefault('default', dict)
|
||||||
|
super(JSONField, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def from_db_value(self, value, expression, connection, context):
|
||||||
|
return self.to_python(value)
|
||||||
|
|
||||||
|
def to_python(self, value):
|
||||||
|
"""
|
||||||
|
Convert the input JSON value into python structures, raises
|
||||||
|
django.core.exceptions.ValidationError if the data can't be converted.
|
||||||
|
"""
|
||||||
|
if self.blank and not value:
|
||||||
|
return {}
|
||||||
|
value = value or '{}'
|
||||||
|
if isinstance(value, six.binary_type):
|
||||||
|
value = six.text_type(value, 'utf-8')
|
||||||
|
if isinstance(value, six.string_types):
|
||||||
|
try:
|
||||||
|
# with django 1.6 i have '"{}"' as default value here
|
||||||
|
if value[0] == value[-1] == '"':
|
||||||
|
value = value[1:-1]
|
||||||
|
|
||||||
|
return json.loads(value)
|
||||||
|
except Exception as err:
|
||||||
|
raise ValidationError(str(err))
|
||||||
|
else:
|
||||||
|
return value
|
||||||
|
|
||||||
|
def validate(self, value, model_instance):
|
||||||
|
"""Check value is a valid JSON string, raise ValidationError on
|
||||||
|
error."""
|
||||||
|
if isinstance(value, six.string_types):
|
||||||
|
super(JSONField, self).validate(value, model_instance)
|
||||||
|
try:
|
||||||
|
json.loads(value)
|
||||||
|
except Exception as err:
|
||||||
|
raise ValidationError(str(err))
|
||||||
|
|
||||||
|
def get_prep_value(self, value):
|
||||||
|
"""Convert value to JSON string before save"""
|
||||||
|
try:
|
||||||
|
return json.dumps(value)
|
||||||
|
except Exception as err:
|
||||||
|
raise ValidationError(str(err))
|
||||||
|
|
||||||
|
def value_to_string(self, obj):
|
||||||
|
"""Return value from object converted to string properly"""
|
||||||
|
return smart_text(self.value_from_object(obj))
|
||||||
|
|
||||||
|
def value_from_object(self, obj):
|
||||||
|
"""Return value dumped to string."""
|
||||||
|
orig_val = super(JSONField, self).value_from_object(obj)
|
||||||
|
return self.get_prep_value(orig_val)
|
||||||
|
|
0
thirdpart/social_django/management/__init__.py
Normal file
0
thirdpart/social_django/management/__init__.py
Normal file
35
thirdpart/social_django/management/commands/clearsocial.py
Normal file
35
thirdpart/social_django/management/commands/clearsocial.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from social_django.models import Code, Partial
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'removes old not used verification codes and partials'
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
super(Command, self).add_arguments(parser)
|
||||||
|
parser.add_argument(
|
||||||
|
'--age',
|
||||||
|
action='store',
|
||||||
|
type=int,
|
||||||
|
dest='age',
|
||||||
|
default=14,
|
||||||
|
help='how long to keep unused data (in days, defaults to 14)'
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
age = timezone.now() - timedelta(days=options['age'])
|
||||||
|
|
||||||
|
# Delete old not verified codes
|
||||||
|
Code.objects.filter(
|
||||||
|
verified=False,
|
||||||
|
timestamp__lt=age
|
||||||
|
).delete()
|
||||||
|
|
||||||
|
# Delete old partial data
|
||||||
|
Partial.objects.filter(
|
||||||
|
timestamp__lt=age
|
||||||
|
).delete()
|
15
thirdpart/social_django/managers.py
Normal file
15
thirdpart/social_django/managers.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class UserSocialAuthManager(models.Manager):
|
||||||
|
"""Manager for the UserSocialAuth django model."""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
app_label = "social_django"
|
||||||
|
|
||||||
|
def get_social_auth(self, provider, uid):
|
||||||
|
try:
|
||||||
|
return self.select_related('user').get(provider=provider,
|
||||||
|
uid=uid)
|
||||||
|
except self.model.DoesNotExist:
|
||||||
|
return None
|
68
thirdpart/social_django/middleware.py
Normal file
68
thirdpart/social_django/middleware.py
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import six
|
||||||
|
|
||||||
|
from django.apps import apps
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.contrib.messages.api import MessageFailure
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
from django.shortcuts import redirect
|
||||||
|
from django.utils.http import urlquote
|
||||||
|
|
||||||
|
from social_core.exceptions import SocialAuthBaseException
|
||||||
|
from social_core.utils import social_logger
|
||||||
|
from .compat import MiddlewareMixin
|
||||||
|
|
||||||
|
|
||||||
|
class SocialAuthExceptionMiddleware(MiddlewareMixin):
|
||||||
|
"""Middleware that handles Social Auth AuthExceptions by providing the user
|
||||||
|
with a message, logging an error, and redirecting to some next location.
|
||||||
|
|
||||||
|
By default, the exception message itself is sent to the user and they are
|
||||||
|
redirected to the location specified in the SOCIAL_AUTH_LOGIN_ERROR_URL
|
||||||
|
setting.
|
||||||
|
|
||||||
|
This middleware can be extended by overriding the get_message or
|
||||||
|
get_redirect_uri methods, which each accept request and exception.
|
||||||
|
"""
|
||||||
|
def process_exception(self, request, exception):
|
||||||
|
strategy = getattr(request, 'social_strategy', None)
|
||||||
|
if strategy is None or self.raise_exception(request, exception):
|
||||||
|
return
|
||||||
|
|
||||||
|
if isinstance(exception, SocialAuthBaseException):
|
||||||
|
backend = getattr(request, 'backend', None)
|
||||||
|
backend_name = getattr(backend, 'name', 'unknown-backend')
|
||||||
|
|
||||||
|
message = self.get_message(request, exception)
|
||||||
|
url = self.get_redirect_uri(request, exception)
|
||||||
|
|
||||||
|
if apps.is_installed('django.contrib.messages'):
|
||||||
|
social_logger.info(message)
|
||||||
|
try:
|
||||||
|
messages.error(request, message,
|
||||||
|
extra_tags='social-auth ' + backend_name)
|
||||||
|
except MessageFailure:
|
||||||
|
if url:
|
||||||
|
url += ('?' in url and '&' or '?') + \
|
||||||
|
'message={0}&backend={1}'.format(urlquote(message),
|
||||||
|
backend_name)
|
||||||
|
else:
|
||||||
|
social_logger.error(message)
|
||||||
|
|
||||||
|
if url:
|
||||||
|
return redirect(url)
|
||||||
|
else:
|
||||||
|
return redirect(reverse('edit_profile'))
|
||||||
|
|
||||||
|
def raise_exception(self, request, exception):
|
||||||
|
strategy = getattr(request, 'social_strategy', None)
|
||||||
|
if strategy is not None:
|
||||||
|
return strategy.setting('RAISE_EXCEPTIONS') or settings.DEBUG
|
||||||
|
|
||||||
|
def get_message(self, request, exception):
|
||||||
|
return six.text_type(exception)
|
||||||
|
|
||||||
|
def get_redirect_uri(self, request, exception):
|
||||||
|
strategy = getattr(request, 'social_strategy', None)
|
||||||
|
return strategy.setting('LOGIN_ERROR_URL')
|
107
thirdpart/social_django/migrations/0001_initial.py
Normal file
107
thirdpart/social_django/migrations/0001_initial.py
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.11 on 2018-05-25 03:27
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import seahub.base.fields
|
||||||
|
import social_django.fields
|
||||||
|
import social_django.storage
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Association',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('server_url', models.CharField(max_length=255)),
|
||||||
|
('handle', models.CharField(max_length=255)),
|
||||||
|
('secret', models.CharField(max_length=255)),
|
||||||
|
('issued', models.IntegerField()),
|
||||||
|
('lifetime', models.IntegerField()),
|
||||||
|
('assoc_type', models.CharField(max_length=64)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'db_table': 'social_auth_association',
|
||||||
|
},
|
||||||
|
bases=(models.Model, social_django.storage.DjangoAssociationMixin),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Code',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('email', models.EmailField(max_length=254)),
|
||||||
|
('code', models.CharField(db_index=True, max_length=32)),
|
||||||
|
('verified', models.BooleanField(default=False)),
|
||||||
|
('timestamp', models.DateTimeField(auto_now_add=True, db_index=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'db_table': 'social_auth_code',
|
||||||
|
},
|
||||||
|
bases=(models.Model, social_django.storage.DjangoCodeMixin),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Nonce',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('server_url', models.CharField(max_length=255)),
|
||||||
|
('timestamp', models.IntegerField()),
|
||||||
|
('salt', models.CharField(max_length=65)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'db_table': 'social_auth_nonce',
|
||||||
|
},
|
||||||
|
bases=(models.Model, social_django.storage.DjangoNonceMixin),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Partial',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('token', models.CharField(db_index=True, max_length=32)),
|
||||||
|
('next_step', models.PositiveSmallIntegerField(default=0)),
|
||||||
|
('backend', models.CharField(max_length=32)),
|
||||||
|
('data', social_django.fields.JSONField(default=dict)),
|
||||||
|
('timestamp', models.DateTimeField(auto_now_add=True, db_index=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'db_table': 'social_auth_partial',
|
||||||
|
},
|
||||||
|
bases=(models.Model, social_django.storage.DjangoPartialMixin),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='UserSocialAuth',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('username', seahub.base.fields.LowerCaseCharField(db_index=True, max_length=255)),
|
||||||
|
('provider', models.CharField(max_length=32)),
|
||||||
|
('uid', models.CharField(max_length=255)),
|
||||||
|
('extra_data', social_django.fields.JSONField(default=dict)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'db_table': 'social_auth_usersocialauth',
|
||||||
|
},
|
||||||
|
bases=(models.Model, social_django.storage.DjangoUserMixin),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='usersocialauth',
|
||||||
|
unique_together=set([('provider', 'uid')]),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='nonce',
|
||||||
|
unique_together=set([('server_url', 'timestamp', 'salt')]),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='code',
|
||||||
|
unique_together=set([('email', 'code')]),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='association',
|
||||||
|
unique_together=set([('server_url', 'handle')]),
|
||||||
|
),
|
||||||
|
]
|
@@ -0,0 +1,20 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.15 on 2018-11-15 08:25
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('social_django', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='usersocialauth',
|
||||||
|
name='uid',
|
||||||
|
field=models.CharField(max_length=150),
|
||||||
|
),
|
||||||
|
]
|
0
thirdpart/social_django/migrations/__init__.py
Normal file
0
thirdpart/social_django/migrations/__init__.py
Normal file
150
thirdpart/social_django/models.py
Normal file
150
thirdpart/social_django/models.py
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
"""Django ORM models for Social Auth"""
|
||||||
|
import six
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db.utils import IntegrityError
|
||||||
|
|
||||||
|
from social_core.utils import setting_name
|
||||||
|
from seahub.base.accounts import User
|
||||||
|
from seahub.base.fields import LowerCaseCharField
|
||||||
|
|
||||||
|
from .compat import get_rel_model
|
||||||
|
from .storage import DjangoUserMixin, DjangoAssociationMixin, \
|
||||||
|
DjangoNonceMixin, DjangoCodeMixin, \
|
||||||
|
DjangoPartialMixin, BaseDjangoStorage
|
||||||
|
from .fields import JSONField
|
||||||
|
from .managers import UserSocialAuthManager
|
||||||
|
|
||||||
|
USER_MODEL = getattr(settings, setting_name('USER_MODEL'), None) or \
|
||||||
|
getattr(settings, 'AUTH_USER_MODEL', None) or \
|
||||||
|
'auth.User'
|
||||||
|
UID_LENGTH = getattr(settings, setting_name('UID_LENGTH'), 150)
|
||||||
|
EMAIL_LENGTH = getattr(settings, setting_name('EMAIL_LENGTH'), 254)
|
||||||
|
NONCE_SERVER_URL_LENGTH = getattr(
|
||||||
|
settings, setting_name('NONCE_SERVER_URL_LENGTH'), 255)
|
||||||
|
ASSOCIATION_SERVER_URL_LENGTH = getattr(
|
||||||
|
settings, setting_name('ASSOCIATION_SERVER_URL_LENGTH'), 255)
|
||||||
|
ASSOCIATION_HANDLE_LENGTH = getattr(
|
||||||
|
settings, setting_name('ASSOCIATION_HANDLE_LENGTH'), 255)
|
||||||
|
|
||||||
|
|
||||||
|
class AbstractUserSocialAuth(models.Model, DjangoUserMixin):
|
||||||
|
"""Abstract Social Auth association model"""
|
||||||
|
# user = models.ForeignKey(USER_MODEL, related_name='social_auth',
|
||||||
|
# on_delete=models.CASCADE)
|
||||||
|
username = LowerCaseCharField(max_length=255, db_index=True)
|
||||||
|
provider = models.CharField(max_length=32)
|
||||||
|
uid = models.CharField(max_length=UID_LENGTH)
|
||||||
|
extra_data = JSONField()
|
||||||
|
objects = UserSocialAuthManager()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return str(self.username)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
app_label = "social_django"
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_social_auth(cls, provider, uid):
|
||||||
|
try:
|
||||||
|
social_auth = cls.objects.get(provider=provider, uid=uid)
|
||||||
|
except cls.DoesNotExist:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
u = User.objects.get(email=social_auth.username)
|
||||||
|
social_auth.user = u
|
||||||
|
except User.DoesNotExist:
|
||||||
|
social_auth.user = None
|
||||||
|
|
||||||
|
return social_auth
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def username_max_length(cls):
|
||||||
|
return 255
|
||||||
|
# username_field = cls.username_field()
|
||||||
|
# field = cls.user_model()._meta.get_field(username_field)
|
||||||
|
# return field.max_length
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def user_model(cls):
|
||||||
|
return User
|
||||||
|
# user_model = get_rel_model(field=cls._meta.get_field('user'))
|
||||||
|
# return user_model
|
||||||
|
|
||||||
|
|
||||||
|
class UserSocialAuth(AbstractUserSocialAuth):
|
||||||
|
"""Social Auth association model"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""Meta data"""
|
||||||
|
app_label = "social_django"
|
||||||
|
unique_together = ('provider', 'uid')
|
||||||
|
db_table = 'social_auth_usersocialauth'
|
||||||
|
|
||||||
|
|
||||||
|
class Nonce(models.Model, DjangoNonceMixin):
|
||||||
|
"""One use numbers"""
|
||||||
|
server_url = models.CharField(max_length=NONCE_SERVER_URL_LENGTH)
|
||||||
|
timestamp = models.IntegerField()
|
||||||
|
salt = models.CharField(max_length=65)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
app_label = "social_django"
|
||||||
|
unique_together = ('server_url', 'timestamp', 'salt')
|
||||||
|
db_table = 'social_auth_nonce'
|
||||||
|
|
||||||
|
|
||||||
|
class Association(models.Model, DjangoAssociationMixin):
|
||||||
|
"""OpenId account association"""
|
||||||
|
server_url = models.CharField(max_length=ASSOCIATION_SERVER_URL_LENGTH)
|
||||||
|
handle = models.CharField(max_length=ASSOCIATION_HANDLE_LENGTH)
|
||||||
|
secret = models.CharField(max_length=255) # Stored base64 encoded
|
||||||
|
issued = models.IntegerField()
|
||||||
|
lifetime = models.IntegerField()
|
||||||
|
assoc_type = models.CharField(max_length=64)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
app_label = "social_django"
|
||||||
|
db_table = 'social_auth_association'
|
||||||
|
unique_together = (
|
||||||
|
('server_url', 'handle',)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Code(models.Model, DjangoCodeMixin):
|
||||||
|
email = models.EmailField(max_length=EMAIL_LENGTH)
|
||||||
|
code = models.CharField(max_length=32, db_index=True)
|
||||||
|
verified = models.BooleanField(default=False)
|
||||||
|
timestamp = models.DateTimeField(auto_now_add=True, db_index=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
app_label = "social_django"
|
||||||
|
db_table = 'social_auth_code'
|
||||||
|
unique_together = ('email', 'code')
|
||||||
|
|
||||||
|
|
||||||
|
class Partial(models.Model, DjangoPartialMixin):
|
||||||
|
token = models.CharField(max_length=32, db_index=True)
|
||||||
|
next_step = models.PositiveSmallIntegerField(default=0)
|
||||||
|
backend = models.CharField(max_length=32)
|
||||||
|
data = JSONField()
|
||||||
|
timestamp = models.DateTimeField(auto_now_add=True, db_index=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
app_label = "social_django"
|
||||||
|
db_table = 'social_auth_partial'
|
||||||
|
|
||||||
|
|
||||||
|
class DjangoStorage(BaseDjangoStorage):
|
||||||
|
user = UserSocialAuth
|
||||||
|
nonce = Nonce
|
||||||
|
association = Association
|
||||||
|
code = Code
|
||||||
|
partial = Partial
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def is_integrity_error(cls, exception):
|
||||||
|
return exception.__class__ is IntegrityError
|
220
thirdpart/social_django/storage.py
Normal file
220
thirdpart/social_django/storage.py
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
"""Django ORM models for Social Auth"""
|
||||||
|
import base64
|
||||||
|
import six
|
||||||
|
import sys
|
||||||
|
from django.db import transaction
|
||||||
|
from django.db.utils import IntegrityError
|
||||||
|
|
||||||
|
from social_core.storage import UserMixin, AssociationMixin, NonceMixin, \
|
||||||
|
CodeMixin, PartialMixin, BaseStorage
|
||||||
|
from seahub.base.accounts import User
|
||||||
|
|
||||||
|
|
||||||
|
class DjangoUserMixin(UserMixin):
|
||||||
|
"""Social Auth association model"""
|
||||||
|
@classmethod
|
||||||
|
def changed(cls, user):
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
def set_extra_data(self, extra_data=None):
|
||||||
|
if super(DjangoUserMixin, self).set_extra_data(extra_data):
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def allowed_to_disconnect(cls, user, backend_name, association_id=None):
|
||||||
|
if association_id is not None:
|
||||||
|
qs = cls.objects.exclude(id=association_id)
|
||||||
|
else:
|
||||||
|
qs = cls.objects.exclude(provider=backend_name)
|
||||||
|
qs = qs.filter(username=user.username)
|
||||||
|
|
||||||
|
if hasattr(user, 'has_usable_password'):
|
||||||
|
valid_password = user.has_usable_password()
|
||||||
|
else:
|
||||||
|
valid_password = True
|
||||||
|
return valid_password or qs.count() > 0
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def disconnect(cls, entry):
|
||||||
|
entry.delete()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def username_field(cls):
|
||||||
|
return 'username'
|
||||||
|
# return getattr(cls.user_model(), 'USERNAME_FIELD', 'username')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def user_exists(cls, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Return True/False if a User instance exists with the given arguments.
|
||||||
|
Arguments are directly passed to filter() manager method.
|
||||||
|
"""
|
||||||
|
if 'username' in kwargs:
|
||||||
|
kwargs[cls.username_field()] = kwargs.pop('username')
|
||||||
|
|
||||||
|
assert 'username' in kwargs
|
||||||
|
|
||||||
|
try:
|
||||||
|
User.objects.get(email=kwargs['username'])
|
||||||
|
return True
|
||||||
|
except User.DoesNotExist:
|
||||||
|
return False
|
||||||
|
# return cls.user_model().objects.filter(*args, **kwargs).count() > 0
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_username(cls, user):
|
||||||
|
return getattr(user, cls.username_field(), None)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_user(cls, *args, **kwargs):
|
||||||
|
username_field = cls.username_field()
|
||||||
|
if 'username' in kwargs and username_field not in kwargs:
|
||||||
|
kwargs[username_field] = kwargs.pop('username')
|
||||||
|
|
||||||
|
assert 'username' in kwargs
|
||||||
|
|
||||||
|
user = User.objects.create_user(email=kwargs['username'],
|
||||||
|
is_active=True,
|
||||||
|
save_profile=False)
|
||||||
|
|
||||||
|
# try:
|
||||||
|
# if hasattr(transaction, 'atomic'):
|
||||||
|
# # In Django versions that have an "atomic" transaction decorator / context
|
||||||
|
# # manager, there's a transaction wrapped around this call.
|
||||||
|
# # If the create fails below due to an IntegrityError, ensure that the transaction
|
||||||
|
# # stays undamaged by wrapping the create in an atomic.
|
||||||
|
# with transaction.atomic():
|
||||||
|
# user = cls.user_model().objects.create_user(*args, **kwargs)
|
||||||
|
# else:
|
||||||
|
# user = cls.user_model().objects.create_user(*args, **kwargs)
|
||||||
|
# except IntegrityError:
|
||||||
|
# # User might have been created on a different thread, try and find them.
|
||||||
|
# # If we don't, re-raise the IntegrityError.
|
||||||
|
# exc_info = sys.exc_info()
|
||||||
|
# # If email comes in as None it won't get found in the get
|
||||||
|
# if kwargs.get('email', True) is None:
|
||||||
|
# kwargs['email'] = ''
|
||||||
|
# try:
|
||||||
|
# user = cls.user_model().objects.get(*args, **kwargs)
|
||||||
|
# except cls.user_model().DoesNotExist:
|
||||||
|
# six.reraise(*exc_info)
|
||||||
|
return user
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_user(cls, pk=None, **kwargs):
|
||||||
|
if pk:
|
||||||
|
kwargs = {'pk': pk}
|
||||||
|
|
||||||
|
try:
|
||||||
|
return User.objects.get(email=pk)
|
||||||
|
except User.DoesNotExist:
|
||||||
|
return None
|
||||||
|
# try:
|
||||||
|
# return cls.user_model().objects.get(**kwargs)
|
||||||
|
# except cls.user_model().DoesNotExist:
|
||||||
|
# return None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_users_by_email(cls, email):
|
||||||
|
user_model = cls.user_model()
|
||||||
|
email_field = getattr(user_model, 'EMAIL_FIELD', 'email')
|
||||||
|
return user_model.objects.filter(**{email_field + '__iexact': email})
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_social_auth(cls, provider, uid):
|
||||||
|
if not isinstance(uid, six.string_types):
|
||||||
|
uid = str(uid)
|
||||||
|
try:
|
||||||
|
return cls.objects.get(provider=provider, uid=uid)
|
||||||
|
except cls.DoesNotExist:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_social_auth_for_user(cls, user, provider=None, id=None):
|
||||||
|
qs = cls.objects.filter(username=user.username)
|
||||||
|
|
||||||
|
if provider:
|
||||||
|
qs = qs.filter(provider=provider)
|
||||||
|
|
||||||
|
if id:
|
||||||
|
qs = qs.filter(id=id)
|
||||||
|
return qs
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_social_auth(cls, user, uid, provider):
|
||||||
|
if not isinstance(uid, six.string_types):
|
||||||
|
uid = str(uid)
|
||||||
|
if hasattr(transaction, 'atomic'):
|
||||||
|
# In Django versions that have an "atomic" transaction decorator / context
|
||||||
|
# manager, there's a transaction wrapped around this call.
|
||||||
|
# If the create fails below due to an IntegrityError, ensure that the transaction
|
||||||
|
# stays undamaged by wrapping the create in an atomic.
|
||||||
|
with transaction.atomic():
|
||||||
|
social_auth = cls.objects.create(username=user.username, uid=uid, provider=provider)
|
||||||
|
else:
|
||||||
|
social_auth = cls.objects.create(username=user.username, uid=uid, provider=provider)
|
||||||
|
return social_auth
|
||||||
|
|
||||||
|
|
||||||
|
class DjangoNonceMixin(NonceMixin):
|
||||||
|
@classmethod
|
||||||
|
def use(cls, server_url, timestamp, salt):
|
||||||
|
return cls.objects.get_or_create(server_url=server_url,
|
||||||
|
timestamp=timestamp,
|
||||||
|
salt=salt)[1]
|
||||||
|
|
||||||
|
|
||||||
|
class DjangoAssociationMixin(AssociationMixin):
|
||||||
|
@classmethod
|
||||||
|
def store(cls, server_url, association):
|
||||||
|
# Don't use get_or_create because issued cannot be null
|
||||||
|
try:
|
||||||
|
assoc = cls.objects.get(server_url=server_url,
|
||||||
|
handle=association.handle)
|
||||||
|
except cls.DoesNotExist:
|
||||||
|
assoc = cls(server_url=server_url,
|
||||||
|
handle=association.handle)
|
||||||
|
assoc.secret = base64.encodestring(association.secret)
|
||||||
|
assoc.issued = association.issued
|
||||||
|
assoc.lifetime = association.lifetime
|
||||||
|
assoc.assoc_type = association.assoc_type
|
||||||
|
assoc.save()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get(cls, *args, **kwargs):
|
||||||
|
return cls.objects.filter(*args, **kwargs)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def remove(cls, ids_to_delete):
|
||||||
|
cls.objects.filter(pk__in=ids_to_delete).delete()
|
||||||
|
|
||||||
|
|
||||||
|
class DjangoCodeMixin(CodeMixin):
|
||||||
|
@classmethod
|
||||||
|
def get_code(cls, code):
|
||||||
|
try:
|
||||||
|
return cls.objects.get(code=code)
|
||||||
|
except cls.DoesNotExist:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class DjangoPartialMixin(PartialMixin):
|
||||||
|
@classmethod
|
||||||
|
def load(cls, token):
|
||||||
|
try:
|
||||||
|
return cls.objects.get(token=token)
|
||||||
|
except cls.DoesNotExist:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def destroy(cls, token):
|
||||||
|
partial = cls.load(token)
|
||||||
|
if partial:
|
||||||
|
partial.delete()
|
||||||
|
|
||||||
|
|
||||||
|
class BaseDjangoStorage(BaseStorage):
|
||||||
|
user = DjangoUserMixin
|
||||||
|
nonce = DjangoNonceMixin
|
||||||
|
association = DjangoAssociationMixin
|
||||||
|
code = DjangoCodeMixin
|
159
thirdpart/social_django/strategy.py
Normal file
159
thirdpart/social_django/strategy.py
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
# coding=utf-8
|
||||||
|
from django.conf import settings
|
||||||
|
from django.http import HttpResponse, HttpRequest
|
||||||
|
from django.db.models import Model
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.contrib.auth import authenticate
|
||||||
|
from django.shortcuts import redirect, resolve_url
|
||||||
|
from django.template import TemplateDoesNotExist, loader, engines
|
||||||
|
from django.utils.crypto import get_random_string
|
||||||
|
from django.utils.encoding import force_text
|
||||||
|
from django.utils.functional import Promise
|
||||||
|
from django.utils.translation import get_language
|
||||||
|
|
||||||
|
from social_core.strategy import BaseStrategy, BaseTemplateStrategy
|
||||||
|
from .compat import get_request_port
|
||||||
|
|
||||||
|
|
||||||
|
def render_template_string(request, html, context=None):
|
||||||
|
"""Take a template in the form of a string and render it for the
|
||||||
|
given context"""
|
||||||
|
template = engines['django'].from_string(html)
|
||||||
|
return template.render(context=context, request=request)
|
||||||
|
|
||||||
|
|
||||||
|
class DjangoTemplateStrategy(BaseTemplateStrategy):
|
||||||
|
def render_template(self, tpl, context):
|
||||||
|
template = loader.get_template(tpl)
|
||||||
|
return template.render(context=context, request=self.strategy.request)
|
||||||
|
|
||||||
|
def render_string(self, html, context):
|
||||||
|
return render_template_string(self.strategy.request, html, context)
|
||||||
|
|
||||||
|
|
||||||
|
class DjangoStrategy(BaseStrategy):
|
||||||
|
DEFAULT_TEMPLATE_STRATEGY = DjangoTemplateStrategy
|
||||||
|
|
||||||
|
def __init__(self, storage, request=None, tpl=None):
|
||||||
|
self.request = request
|
||||||
|
self.session = request.session if request else {}
|
||||||
|
super(DjangoStrategy, self).__init__(storage, tpl)
|
||||||
|
|
||||||
|
def get_setting(self, name):
|
||||||
|
value = getattr(settings, name)
|
||||||
|
# Force text on URL named settings that are instance of Promise
|
||||||
|
if name.endswith('_URL'):
|
||||||
|
if isinstance(value, Promise):
|
||||||
|
value = force_text(value)
|
||||||
|
value = resolve_url(value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
def request_data(self, merge=True):
|
||||||
|
if not self.request:
|
||||||
|
return {}
|
||||||
|
if merge:
|
||||||
|
data = self.request.GET.copy()
|
||||||
|
data.update(self.request.POST)
|
||||||
|
elif self.request.method == 'POST':
|
||||||
|
data = self.request.POST
|
||||||
|
else:
|
||||||
|
data = self.request.GET
|
||||||
|
return data
|
||||||
|
|
||||||
|
def request_host(self):
|
||||||
|
if self.request:
|
||||||
|
return self.request.get_host()
|
||||||
|
|
||||||
|
def request_is_secure(self):
|
||||||
|
"""Is the request using HTTPS?"""
|
||||||
|
return self.request.is_secure()
|
||||||
|
|
||||||
|
def request_path(self):
|
||||||
|
"""path of the current request"""
|
||||||
|
return self.request.path
|
||||||
|
|
||||||
|
def request_port(self):
|
||||||
|
"""Port in use for this request"""
|
||||||
|
return get_request_port(request=self.request)
|
||||||
|
|
||||||
|
def request_get(self):
|
||||||
|
"""Request GET data"""
|
||||||
|
return self.request.GET.copy()
|
||||||
|
|
||||||
|
def request_post(self):
|
||||||
|
"""Request POST data"""
|
||||||
|
return self.request.POST.copy()
|
||||||
|
|
||||||
|
def redirect(self, url):
|
||||||
|
return redirect(url)
|
||||||
|
|
||||||
|
def html(self, content):
|
||||||
|
return HttpResponse(content, content_type='text/html;charset=UTF-8')
|
||||||
|
|
||||||
|
def render_html(self, tpl=None, html=None, context=None):
|
||||||
|
if not tpl and not html:
|
||||||
|
raise ValueError('Missing template or html parameters')
|
||||||
|
context = context or {}
|
||||||
|
try:
|
||||||
|
template = loader.get_template(tpl)
|
||||||
|
return template.render(context=context, request=self.request)
|
||||||
|
except TemplateDoesNotExist:
|
||||||
|
return render_template_string(self.request, html, context)
|
||||||
|
|
||||||
|
def authenticate(self, backend, *args, **kwargs):
|
||||||
|
kwargs['strategy'] = self
|
||||||
|
kwargs['storage'] = self.storage
|
||||||
|
kwargs['backend'] = backend
|
||||||
|
return authenticate(*args, **kwargs)
|
||||||
|
|
||||||
|
def clean_authenticate_args(self, *args, **kwargs):
|
||||||
|
"""Cleanup request argument if present, which is passed to
|
||||||
|
authenticate as for Django 1.11"""
|
||||||
|
if len(args) > 0 and isinstance(args[0], HttpRequest):
|
||||||
|
kwargs['request'], args = args[0], args[1:]
|
||||||
|
return args, kwargs
|
||||||
|
|
||||||
|
def session_get(self, name, default=None):
|
||||||
|
return self.session.get(name, default)
|
||||||
|
|
||||||
|
def session_set(self, name, value):
|
||||||
|
self.session[name] = value
|
||||||
|
if hasattr(self.session, 'modified'):
|
||||||
|
self.session.modified = True
|
||||||
|
|
||||||
|
def session_pop(self, name):
|
||||||
|
return self.session.pop(name, None)
|
||||||
|
|
||||||
|
def session_setdefault(self, name, value):
|
||||||
|
return self.session.setdefault(name, value)
|
||||||
|
|
||||||
|
def build_absolute_uri(self, path=None):
|
||||||
|
if self.request:
|
||||||
|
return self.request.build_absolute_uri(path)
|
||||||
|
else:
|
||||||
|
return path
|
||||||
|
|
||||||
|
def random_string(self, length=12, chars=BaseStrategy.ALLOWED_CHARS):
|
||||||
|
return get_random_string(length, chars)
|
||||||
|
|
||||||
|
def to_session_value(self, val):
|
||||||
|
"""Converts values that are instance of Model to a dictionary
|
||||||
|
with enough information to retrieve the instance back later."""
|
||||||
|
if isinstance(val, Model):
|
||||||
|
val = {
|
||||||
|
'pk': val.pk,
|
||||||
|
'ctype': ContentType.objects.get_for_model(val).pk
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
|
||||||
|
def from_session_value(self, val):
|
||||||
|
"""Converts back the instance saved by self._ctype function."""
|
||||||
|
if isinstance(val, dict) and 'pk' in val and 'ctype' in val:
|
||||||
|
ctype = ContentType.objects.get_for_id(val['ctype'])
|
||||||
|
ModelClass = ctype.model_class()
|
||||||
|
val = ModelClass.objects.get(pk=val['pk'])
|
||||||
|
return val
|
||||||
|
|
||||||
|
def get_language(self):
|
||||||
|
"""Return current language"""
|
||||||
|
return get_language()
|
24
thirdpart/social_django/urls.py
Normal file
24
thirdpart/social_django/urls.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
"""URLs module"""
|
||||||
|
from django.conf import settings
|
||||||
|
from django.conf.urls import url
|
||||||
|
|
||||||
|
from social_core.utils import setting_name
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
|
||||||
|
extra = getattr(settings, setting_name('TRAILING_SLASH'), True) and '/' or ''
|
||||||
|
|
||||||
|
app_name = 'social'
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
# authentication / association
|
||||||
|
url(r'^login/(?P<backend>[^/]+){0}$'.format(extra), views.auth,
|
||||||
|
name='begin'),
|
||||||
|
url(r'^complete/(?P<backend>[^/]+){0}$'.format(extra), views.complete,
|
||||||
|
name='complete'),
|
||||||
|
# disconnection
|
||||||
|
url(r'^disconnect/(?P<backend>[^/]+){0}$'.format(extra), views.disconnect,
|
||||||
|
name='disconnect'),
|
||||||
|
url(r'^disconnect/(?P<backend>[^/]+)/(?P<association_id>\d+){0}$'
|
||||||
|
.format(extra), views.disconnect, name='disconnect_individual'),
|
||||||
|
]
|
51
thirdpart/social_django/utils.py
Normal file
51
thirdpart/social_django/utils.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# coding=utf-8
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.http import Http404
|
||||||
|
|
||||||
|
from social_core.utils import setting_name, module_member, get_strategy
|
||||||
|
from social_core.exceptions import MissingBackend
|
||||||
|
from social_core.backends.utils import get_backend
|
||||||
|
from .compat import reverse
|
||||||
|
|
||||||
|
|
||||||
|
BACKENDS = settings.AUTHENTICATION_BACKENDS
|
||||||
|
STRATEGY = getattr(settings, setting_name('STRATEGY'),
|
||||||
|
'social_django.strategy.DjangoStrategy')
|
||||||
|
STORAGE = getattr(settings, setting_name('STORAGE'),
|
||||||
|
'social_django.models.DjangoStorage')
|
||||||
|
Strategy = module_member(STRATEGY)
|
||||||
|
Storage = module_member(STORAGE)
|
||||||
|
|
||||||
|
|
||||||
|
def load_strategy(request=None):
|
||||||
|
return get_strategy(STRATEGY, STORAGE, request)
|
||||||
|
|
||||||
|
|
||||||
|
def load_backend(strategy, name, redirect_uri):
|
||||||
|
Backend = get_backend(BACKENDS, name)
|
||||||
|
return Backend(strategy, redirect_uri)
|
||||||
|
|
||||||
|
|
||||||
|
def psa(redirect_uri=None, load_strategy=load_strategy):
|
||||||
|
def decorator(func):
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(request, backend, *args, **kwargs):
|
||||||
|
uri = redirect_uri
|
||||||
|
if uri and not uri.startswith('/'):
|
||||||
|
uri = reverse(redirect_uri, args=(backend,))
|
||||||
|
request.social_strategy = load_strategy(request)
|
||||||
|
# backward compatibility in attribute name, only if not already
|
||||||
|
# defined
|
||||||
|
if not hasattr(request, 'strategy'):
|
||||||
|
request.strategy = request.social_strategy
|
||||||
|
|
||||||
|
try:
|
||||||
|
request.backend = load_backend(request.social_strategy,
|
||||||
|
backend, uri)
|
||||||
|
except MissingBackend:
|
||||||
|
raise Http404('Backend not found')
|
||||||
|
return func(request, backend, *args, **kwargs)
|
||||||
|
return wrapper
|
||||||
|
return decorator
|
131
thirdpart/social_django/views.py
Normal file
131
thirdpart/social_django/views.py
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.auth import REDIRECT_FIELD_NAME
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.views.decorators.csrf import csrf_exempt, csrf_protect
|
||||||
|
from django.views.decorators.http import require_POST
|
||||||
|
from django.views.decorators.cache import never_cache
|
||||||
|
|
||||||
|
from seahub.auth import login
|
||||||
|
|
||||||
|
from social_core.utils import setting_name
|
||||||
|
from social_core.actions import do_auth, do_complete, do_disconnect
|
||||||
|
from .utils import psa
|
||||||
|
|
||||||
|
|
||||||
|
NAMESPACE = getattr(settings, setting_name('URL_NAMESPACE'), None) or 'social'
|
||||||
|
|
||||||
|
# Calling `session.set_expiry(None)` results in a session lifetime equal to
|
||||||
|
# platform default session lifetime.
|
||||||
|
DEFAULT_SESSION_TIMEOUT = None
|
||||||
|
|
||||||
|
|
||||||
|
@never_cache
|
||||||
|
@psa('{0}:complete'.format(NAMESPACE))
|
||||||
|
def auth(request, backend):
|
||||||
|
return do_auth(request.backend, redirect_name=REDIRECT_FIELD_NAME)
|
||||||
|
|
||||||
|
|
||||||
|
@never_cache
|
||||||
|
@csrf_exempt
|
||||||
|
@psa('{0}:complete'.format(NAMESPACE))
|
||||||
|
def complete(request, backend, *args, **kwargs):
|
||||||
|
"""Authentication complete view"""
|
||||||
|
return do_complete(request.backend, _do_login, request.user,
|
||||||
|
redirect_name=REDIRECT_FIELD_NAME, request=request,
|
||||||
|
*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@never_cache
|
||||||
|
@login_required
|
||||||
|
@psa()
|
||||||
|
@require_POST
|
||||||
|
@csrf_protect
|
||||||
|
def disconnect(request, backend, association_id=None):
|
||||||
|
"""Disconnects given backend from current logged in user."""
|
||||||
|
return do_disconnect(request.backend, request.user, association_id,
|
||||||
|
redirect_name=REDIRECT_FIELD_NAME)
|
||||||
|
|
||||||
|
|
||||||
|
def get_session_timeout(social_user, enable_session_expiration=False,
|
||||||
|
max_session_length=None):
|
||||||
|
if enable_session_expiration:
|
||||||
|
# Retrieve an expiration date from the social user who just finished
|
||||||
|
# logging in; this value was set by the social auth backend, and was
|
||||||
|
# typically received from the server.
|
||||||
|
expiration = social_user.expiration_datetime()
|
||||||
|
|
||||||
|
# We've enabled session expiration. Check to see if we got
|
||||||
|
# a specific expiration time from the provider for this user;
|
||||||
|
# if not, use the platform default expiration.
|
||||||
|
if expiration:
|
||||||
|
received_expiration_time = expiration.total_seconds()
|
||||||
|
else:
|
||||||
|
received_expiration_time = DEFAULT_SESSION_TIMEOUT
|
||||||
|
|
||||||
|
# Check to see if the backend set a value as a maximum length
|
||||||
|
# that a session may be; if they did, then we should use the minimum
|
||||||
|
# of that and the received session expiration time, if any, to
|
||||||
|
# set the session length.
|
||||||
|
if received_expiration_time is None and max_session_length is None:
|
||||||
|
# We neither received an expiration length, nor have a maximum
|
||||||
|
# session length. Use the platform default.
|
||||||
|
session_expiry = DEFAULT_SESSION_TIMEOUT
|
||||||
|
elif received_expiration_time is None and max_session_length is not None:
|
||||||
|
# We only have a maximum session length; use that.
|
||||||
|
session_expiry = max_session_length
|
||||||
|
elif received_expiration_time is not None and max_session_length is None:
|
||||||
|
# We only have an expiration time received by the backend
|
||||||
|
# from the provider, with no set maximum. Use that.
|
||||||
|
session_expiry = received_expiration_time
|
||||||
|
else:
|
||||||
|
# We received an expiration time from the backend, and we also
|
||||||
|
# have a set maximum session length. Use the smaller of the two.
|
||||||
|
session_expiry = min(received_expiration_time, max_session_length)
|
||||||
|
else:
|
||||||
|
# If there's an explicitly-set maximum session length, use that
|
||||||
|
# even if we don't want to retrieve session expiry times from
|
||||||
|
# the backend. If there isn't, then use the platform default.
|
||||||
|
if max_session_length is None:
|
||||||
|
session_expiry = DEFAULT_SESSION_TIMEOUT
|
||||||
|
else:
|
||||||
|
session_expiry = max_session_length
|
||||||
|
|
||||||
|
return session_expiry
|
||||||
|
|
||||||
|
|
||||||
|
def _do_login(backend, user, social_user):
|
||||||
|
user.backend = '{0}.{1}'.format(backend.__module__,
|
||||||
|
backend.__class__.__name__)
|
||||||
|
# Get these details early to avoid any issues involved in the
|
||||||
|
# session switch that happens when we call login().
|
||||||
|
enable_session_expiration = backend.setting('SESSION_EXPIRATION', False)
|
||||||
|
max_session_length_setting = backend.setting('MAX_SESSION_LENGTH', None)
|
||||||
|
|
||||||
|
# Log the user in, creating a new session.
|
||||||
|
login(backend.strategy.request, user)
|
||||||
|
|
||||||
|
# Make sure that the max_session_length value is either an integer or
|
||||||
|
# None. Because we get this as a setting from the backend, it can be set
|
||||||
|
# to whatever the backend creator wants; we want to be resilient against
|
||||||
|
# unexpected types being presented to us.
|
||||||
|
try:
|
||||||
|
max_session_length = int(max_session_length_setting)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
# We got a response that doesn't look like a number; use the default.
|
||||||
|
max_session_length = None
|
||||||
|
|
||||||
|
# Get the session expiration length based on the maximum session length
|
||||||
|
# setting, combined with any session length received from the backend.
|
||||||
|
session_expiry = get_session_timeout(
|
||||||
|
social_user,
|
||||||
|
enable_session_expiration=enable_session_expiration,
|
||||||
|
max_session_length=max_session_length,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Set the session length to our previously determined expiry length.
|
||||||
|
backend.strategy.request.session.set_expiry(session_expiry)
|
||||||
|
except OverflowError:
|
||||||
|
# The timestamp we used wasn't in the range of values supported by
|
||||||
|
# Django for session length; use the platform default. We tried.
|
||||||
|
backend.strategy.request.session.set_expiry(DEFAULT_SESSION_TIMEOUT)
|
144
thirdpart/weworkapi/AbstractApi.py
Normal file
144
thirdpart/weworkapi/AbstractApi.py
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding:utf-8 -*-
|
||||||
|
##
|
||||||
|
# Copyright (C) 2018 All rights reserved.
|
||||||
|
#
|
||||||
|
# @File AbstractApi.py
|
||||||
|
# @Brief
|
||||||
|
# @Author abelzhu, abelzhu@tencent.com
|
||||||
|
# @Version 1.0
|
||||||
|
# @Date 2018-02-24
|
||||||
|
#
|
||||||
|
#
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
import json
|
||||||
|
import requests
|
||||||
|
|
||||||
|
DEBUG = False
|
||||||
|
|
||||||
|
class ApiException(Exception) :
|
||||||
|
def __init__(self, errCode, errMsg) :
|
||||||
|
self.errCode = errCode
|
||||||
|
self.errMsg = errMsg
|
||||||
|
|
||||||
|
class AbstractApi(object) :
|
||||||
|
def __init__(self) :
|
||||||
|
return
|
||||||
|
|
||||||
|
def getAccessToken(self) :
|
||||||
|
raise NotImplementedError
|
||||||
|
def refreshAccessToken(self) :
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def getSuiteAccessToken(self) :
|
||||||
|
raise NotImplementedError
|
||||||
|
def refreshSuiteAccessToken(self) :
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def getProviderAccessToken(self) :
|
||||||
|
raise NotImplementedError
|
||||||
|
def refreshProviderAccessToken(self) :
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def httpCall(self, urlType, args=None) :
|
||||||
|
shortUrl = urlType[0]
|
||||||
|
method = urlType[1]
|
||||||
|
response = {}
|
||||||
|
for retryCnt in range(0, 3) :
|
||||||
|
if 'POST' == method :
|
||||||
|
url = self.__makeUrl(shortUrl)
|
||||||
|
response = self.__httpPost(url, args)
|
||||||
|
elif 'GET' == method :
|
||||||
|
url = self.__makeUrl(shortUrl)
|
||||||
|
url = self.__appendArgs(url, args)
|
||||||
|
response = self.__httpGet(url)
|
||||||
|
else :
|
||||||
|
raise ApiException(-1, "unknown method type")
|
||||||
|
|
||||||
|
# check if token expired
|
||||||
|
if self.__tokenExpired(response.get('errcode')) :
|
||||||
|
self.__refreshToken(shortUrl)
|
||||||
|
retryCnt += 1
|
||||||
|
continue
|
||||||
|
else :
|
||||||
|
break
|
||||||
|
|
||||||
|
return self.__checkResponse(response)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def __appendArgs(url, args) :
|
||||||
|
if args is None :
|
||||||
|
return url
|
||||||
|
|
||||||
|
for key, value in args.items() :
|
||||||
|
if '?' in url :
|
||||||
|
url += ('&' + key + '=' + value)
|
||||||
|
else :
|
||||||
|
url += ('?' + key + '=' + value)
|
||||||
|
return url
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def __makeUrl(shortUrl) :
|
||||||
|
base = "https://qyapi.weixin.qq.com"
|
||||||
|
if shortUrl[0] == '/' :
|
||||||
|
return base + shortUrl
|
||||||
|
else :
|
||||||
|
return base + '/' + shortUrl
|
||||||
|
|
||||||
|
def __appendToken(self, url) :
|
||||||
|
if 'SUITE_ACCESS_TOKEN' in url :
|
||||||
|
return url.replace('SUITE_ACCESS_TOKEN', self.getSuiteAccessToken())
|
||||||
|
elif 'PROVIDER_ACCESS_TOKEN' in url :
|
||||||
|
return url.replace('PROVIDER_ACCESS_TOKEN', self.getProviderAccessToken())
|
||||||
|
elif 'ACCESS_TOKEN' in url :
|
||||||
|
return url.replace('ACCESS_TOKEN', self.getAccessToken())
|
||||||
|
else :
|
||||||
|
return url
|
||||||
|
|
||||||
|
def __httpPost(self, url, args) :
|
||||||
|
realUrl = self.__appendToken(url)
|
||||||
|
|
||||||
|
if DEBUG is True :
|
||||||
|
print realUrl, args
|
||||||
|
|
||||||
|
return requests.post(realUrl, data = json.dumps(args, ensure_ascii = False).encode('utf-8')).json()
|
||||||
|
|
||||||
|
def __httpGet(self, url) :
|
||||||
|
realUrl = self.__appendToken(url)
|
||||||
|
|
||||||
|
if DEBUG is True :
|
||||||
|
print realUrl
|
||||||
|
|
||||||
|
return requests.get(realUrl).json()
|
||||||
|
|
||||||
|
def __post_file(self, url, media_file):
|
||||||
|
return requests.post(url, file=media_file).json()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def __checkResponse(response):
|
||||||
|
errCode = response.get('errcode')
|
||||||
|
errMsg = response.get('errmsg')
|
||||||
|
|
||||||
|
if errCode is 0:
|
||||||
|
return response
|
||||||
|
else:
|
||||||
|
raise ApiException(errCode, errMsg)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def __tokenExpired(errCode) :
|
||||||
|
if errCode == 40014 or errCode == 42001 or errCode == 42007 or errCode == 42009 :
|
||||||
|
return True
|
||||||
|
else :
|
||||||
|
return False
|
||||||
|
|
||||||
|
def __refreshToken(self, url) :
|
||||||
|
if 'SUITE_ACCESS_TOKEN' in url :
|
||||||
|
self.refreshSuiteAccessToken()
|
||||||
|
elif 'PROVIDER_ACCESS_TOKEN' in url :
|
||||||
|
self.refreshProviderAccessToken()
|
||||||
|
elif 'ACCESS_TOKEN' in url :
|
||||||
|
self.refreshAccessToken()
|
104
thirdpart/weworkapi/CorpApi.py
Normal file
104
thirdpart/weworkapi/CorpApi.py
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding:utf-8 -*-
|
||||||
|
##
|
||||||
|
# Copyright (C) 2018 All rights reserved.
|
||||||
|
#
|
||||||
|
# @File CorpApi.py
|
||||||
|
# @Brief
|
||||||
|
# @Author abelzhu, abelzhu@tencent.com
|
||||||
|
# @Version 1.0
|
||||||
|
# @Date 2018-02-24
|
||||||
|
#
|
||||||
|
#
|
||||||
|
|
||||||
|
from .AbstractApi import *
|
||||||
|
|
||||||
|
CORP_API_TYPE = {
|
||||||
|
'GET_ACCESS_TOKEN' : ['/cgi-bin/gettoken', 'GET'],
|
||||||
|
'USER_CREATE' : ['/cgi-bin/user/create?access_token=ACCESS_TOKEN', 'POST'],
|
||||||
|
'USER_GET' : ['/cgi-bin/user/get?access_token=ACCESS_TOKEN', 'GET'],
|
||||||
|
'USER_UPDATE' : ['/cgi-bin/user/update?access_token=ACCESS_TOKEN', 'POST'],
|
||||||
|
'USER_DELETE' : ['/cgi-bin/user/delete?access_token=ACCESS_TOKEN', 'GET'],
|
||||||
|
'USER_BATCH_DELETE': ['/cgi-bin/user/batchdelete?access_token=ACCESS_TOKEN', 'POST'],
|
||||||
|
'USER_SIMPLE_LIST ': ['/cgi-bin/user/simplelist?access_token=ACCESS_TOKEN', 'GET'],
|
||||||
|
'USER_LIST' : ['/cgi-bin/user/list?access_token=ACCESS_TOKEN', 'GET'],
|
||||||
|
'USERID_TO_OPENID' : ['/cgi-bin/user/convert_to_openid?access_token=ACCESS_TOKEN', 'POST'],
|
||||||
|
'OPENID_TO_USERID' : ['/cgi-bin/user/convert_to_userid?access_token=ACCESS_TOKEN', 'POST'],
|
||||||
|
'USER_AUTH_SUCCESS': ['/cgi-bin/user/authsucc?access_token=ACCESS_TOKEN', 'GET'],
|
||||||
|
|
||||||
|
'DEPARTMENT_CREATE': ['/cgi-bin/department/create?access_token=ACCESS_TOKEN', 'POST'],
|
||||||
|
'DEPARTMENT_UPDATE': ['/cgi-bin/department/update?access_token=ACCESS_TOKEN', 'POST'],
|
||||||
|
'DEPARTMENT_DELETE': ['/cgi-bin/department/delete?access_token=ACCESS_TOKEN', 'GET'],
|
||||||
|
'DEPARTMENT_LIST' : ['/cgi-bin/department/list?access_token=ACCESS_TOKEN', 'GET'],
|
||||||
|
|
||||||
|
'TAG_CREATE' : ['/cgi-bin/tag/create?access_token=ACCESS_TOKEN', 'POST'],
|
||||||
|
'TAG_UPDATE' : ['/cgi-bin/tag/update?access_token=ACCESS_TOKEN', 'POST'],
|
||||||
|
'TAG_DELETE' : ['/cgi-bin/tag/delete?access_token=ACCESS_TOKEN', 'GET'],
|
||||||
|
'TAG_GET_USER' : ['/cgi-bin/tag/get?access_token=ACCESS_TOKEN', 'GET'],
|
||||||
|
'TAG_ADD_USER' : ['/cgi-bin/tag/addtagusers?access_token=ACCESS_TOKEN', 'POST'],
|
||||||
|
'TAG_DELETE_USER' : ['/cgi-bin/tag/deltagusers?access_token=ACCESS_TOKEN', 'POST'],
|
||||||
|
'TAG_GET_LIST' : ['/cgi-bin/tag/list?access_token=ACCESS_TOKEN', 'GET'],
|
||||||
|
|
||||||
|
'BATCH_JOB_GET_RESULT' : ['/cgi-bin/batch/getresult?access_token=ACCESS_TOKEN', 'GET'],
|
||||||
|
|
||||||
|
'BATCH_INVITE' : ['/cgi-bin/batch/invite?access_token=ACCESS_TOKEN', 'POST'],
|
||||||
|
|
||||||
|
'AGENT_GET' : ['/cgi-bin/agent/get?access_token=ACCESS_TOKEN', 'GET'],
|
||||||
|
'AGENT_SET' : ['/cgi-bin/agent/set?access_token=ACCESS_TOKEN', 'POST'],
|
||||||
|
'AGENT_GET_LIST' : ['/cgi-bin/agent/list?access_token=ACCESS_TOKEN', 'GET'],
|
||||||
|
|
||||||
|
'MENU_CREATE' : ['/cgi-bin/menu/create?access_token=ACCESS_TOKEN', 'POST'], ## TODO
|
||||||
|
'MENU_GET' : ['/cgi-bin/menu/get?access_token=ACCESS_TOKEN', 'GET'],
|
||||||
|
'MENU_DELETE' : ['/cgi-bin/menu/delete?access_token=ACCESS_TOKEN', 'GET'],
|
||||||
|
|
||||||
|
'MESSAGE_SEND' : ['/cgi-bin/message/send?access_token=ACCESS_TOKEN', 'POST'],
|
||||||
|
'MESSAGE_REVOKE' : ['/cgi-bin/message/revoke?access_token=ACCESS_TOKEN', 'POST'],
|
||||||
|
|
||||||
|
'MEDIA_GET' : ['/cgi-bin/media/get?access_token=ACCESS_TOKEN', 'GET'],
|
||||||
|
|
||||||
|
'GET_USER_INFO_BY_CODE' : ['/cgi-bin/user/getuserinfo?access_token=ACCESS_TOKEN', 'GET'],
|
||||||
|
'GET_USER_DETAIL' : ['/cgi-bin/user/getuserdetail?access_token=ACCESS_TOKEN', 'POST'],
|
||||||
|
|
||||||
|
'GET_TICKET' : ['/cgi-bin/ticket/get?access_token=ACCESS_TOKEN', 'GET'],
|
||||||
|
'GET_JSAPI_TICKET' : ['/cgi-bin/get_jsapi_ticket?access_token=ACCESS_TOKEN', 'GET'],
|
||||||
|
|
||||||
|
'GET_CHECKIN_OPTION' : ['/cgi-bin/checkin/getcheckinoption?access_token=ACCESS_TOKEN', 'POST'],
|
||||||
|
'GET_CHECKIN_DATA' : ['/cgi-bin/checkin/getcheckindata?access_token=ACCESS_TOKEN', 'POST'],
|
||||||
|
'GET_APPROVAL_DATA': ['/cgi-bin/corp/getapprovaldata?access_token=ACCESS_TOKEN', 'POST'],
|
||||||
|
|
||||||
|
'GET_INVOICE_INFO' : ['/cgi-bin/card/invoice/reimburse/getinvoiceinfo?access_token=ACCESS_TOKEN', 'POST'],
|
||||||
|
'UPDATE_INVOICE_STATUS' :
|
||||||
|
['/cgi-bin/card/invoice/reimburse/updateinvoicestatus?access_token=ACCESS_TOKEN', 'POST'],
|
||||||
|
'BATCH_UPDATE_INVOICE_STATUS' :
|
||||||
|
['/cgi-bin/card/invoice/reimburse/updatestatusbatch?access_token=ACCESS_TOKEN', 'POST'],
|
||||||
|
'BATCH_GET_INVOICE_INFO' :
|
||||||
|
['/cgi-bin/card/invoice/reimburse/getinvoiceinfobatch?access_token=ACCESS_TOKEN', 'POST'],
|
||||||
|
|
||||||
|
'APP_CHAT_CREATE' : ['/cgi-bin/appchat/create?access_token=ACCESS_TOKEN', 'POST'],
|
||||||
|
'APP_CHAT_GET' : ['/cgi-bin/appchat/get?access_token=ACCESS_TOKEN', 'GET'],
|
||||||
|
'APP_CHAT_UPDATE' : ['/cgi-bin/appchat/update?access_token=ACCESS_TOKEN', 'POST'],
|
||||||
|
'APP_CHAT_SEND' : ['/cgi-bin/appchat/send?access_token=ACCESS_TOKEN', 'POST'],
|
||||||
|
|
||||||
|
'MINIPROGRAM_CODE_TO_SESSION_KEY' : ['/cgi-bin/miniprogram/jscode2session?access_token=ACCESS_TOKEN', 'GET'],
|
||||||
|
}
|
||||||
|
|
||||||
|
class CorpApi(AbstractApi) :
|
||||||
|
def __init__(self, corpid, secret) :
|
||||||
|
self.corpid = corpid
|
||||||
|
self.secret = secret
|
||||||
|
self.access_token = None
|
||||||
|
|
||||||
|
def getAccessToken(self) :
|
||||||
|
if self.access_token is None :
|
||||||
|
self.refreshAccessToken()
|
||||||
|
return self.access_token
|
||||||
|
|
||||||
|
def refreshAccessToken(self) :
|
||||||
|
response = self.httpCall(
|
||||||
|
CORP_API_TYPE['GET_ACCESS_TOKEN'],
|
||||||
|
{
|
||||||
|
'corpid' : self.corpid,
|
||||||
|
'corpsecret': self.secret,
|
||||||
|
})
|
||||||
|
self.access_token = response.get('access_token')
|
||||||
|
|
1
thirdpart/weworkapi/__init__.py
Normal file
1
thirdpart/weworkapi/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""ref: https://github.com/sbzhu/weworkapi_python"""
|
Reference in New Issue
Block a user