Skip to content

Commit

Permalink
feat(阿里云sso对接): 支持阿里云角色SSO接入
Browse files Browse the repository at this point in the history
  • Loading branch information
Oo-RR-oO committed Jul 24, 2020
1 parent e1bb526 commit bdc71de
Show file tree
Hide file tree
Showing 8 changed files with 215 additions and 9 deletions.
13 changes: 13 additions & 0 deletions djangosaml2idp/dev.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,16 @@ def get(self, request): # pylint: disable=no-self-use
"""
token_url = '/siteapi/v1/ucenter/login/'
return render(request, 'dev/mock_login.html', context={'token_url': token_url})


class AliyunRoleSSOLoginView(View):
"""
模拟OneID的登录页面
run as FE
"""
def get(self, request): # pylint: disable=no-self-use
"""
aliyun role sso login
"""
token_url = '/siteapi/v1/ucenter/login/'
return render(request, 'dev/aliyun_role_sso_login.html', context={'token_url': token_url})
124 changes: 116 additions & 8 deletions djangosaml2idp/idpview.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from six import text_type
from saml2.saml import NAMEID_FORMAT_EMAILADDRESS, NAMEID_FORMAT_UNSPECIFIED
from saml2.sigver import get_xmlsec_binary
from saml2 import BINDING_HTTP_POST, BINDING_HTTP_REDIRECT
from saml2 import BINDING_HTTP_POST, BINDING_HTTP_REDIRECT, saml
from saml2.authn_context import PASSWORD, AuthnBroker, authn_context_class_ref
from saml2.config import IdPConfig
from saml2.ident import NameID
Expand All @@ -41,6 +41,7 @@
BASEDIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))


# pylint: disable=too-many-lines
def create_self_signed_cert():
'''
生成自签名证书存放于相对路径下
Expand Down Expand Up @@ -269,13 +270,12 @@ def get(self, request, *args, **kwargs): # pylint: disable=missing-function-d
resp_args = self.IDP.response_args(req_info.message)
except (UnknownPrincipal, UnsupportedBinding) as excp:
return self.handle_error(request, exception=excp, status=400)
print('resp_args is', resp_args)
# print('resp_args is', resp_args)
try:
# sp_config = SAML_IDP_SPCONFIG[resp_args['sp_entity_id']]
sp_config = {
'processor': 'djangosaml2idp.processors.BaseProcessor',
'attribute_mapping': {
# DJANGO: SAML
'email': 'email',
'private_email': 'private_email',
'username': 'username',
Expand All @@ -299,9 +299,9 @@ def get(self, request, *args, **kwargs): # pylint: disable=missing-function-d
cookie_user = self.cookie_user(request)
identity = self.get_identity(processor, cookie_user, sp_config)
req_authn_context = req_info.message.requested_authn_context or PASSWORD
print('cookie_user is', cookie_user)
print('identity is', identity)
print('req_authn_context is', req_authn_context)
# print('cookie_user is', cookie_user)
# print('identity is', identity)
# print('req_authn_context is', req_authn_context)
AUTHN_BROKER = AuthnBroker() # pylint: disable=invalid-name
AUTHN_BROKER.add(authn_context_class_ref(req_authn_context), "")
user_id = processor.get_user_id(cookie_user)
Expand All @@ -324,7 +324,7 @@ def get(self, request, *args, **kwargs): # pylint: disable=missing-function-d
**resp_args)
except Exception as excp: # pylint: disable=broad-except
return self.handle_error(request, exception=excp, status=500)
print('authn_resp is', authn_resp)
# print('authn_resp is', authn_resp)
http_args = self.IDP.apply_binding(binding=resp_args['binding'],
msg_str="%s" % authn_resp,
destination=resp_args['destination'],
Expand Down Expand Up @@ -367,7 +367,6 @@ def get(self, request, *args, **kwargs): # pylint: disable=missing-function-d
sp_config = {
'processor': 'djangosaml2idp.processors.BaseProcessor',
'attribute_mapping': {
# DJANGO: SAML
'username': 'username',
'email': 'email',
'name': 'first_name',
Expand Down Expand Up @@ -482,3 +481,112 @@ def get_success_url_allowed_hosts(self): # pylint: disable=missing-function-d
allowed_hosts = {self.request.get_host()}
allowed_hosts.update(self.success_url_allowed_hosts)
return allowed_hosts


class AliyunRoleSSOAccessMixin(AccessMixin):
"""
Abstract CBV mixin that gives access mixins the same customizable
functionality.
"""
login_url = settings.ALIYUN_ROLE_SSO_LOGIN_URL

def dispatch(self, request, *args, **kwargs):
"""检查用户cookies是否登录"""
try:
spauthn = request.COOKIES['spauthn']
token = ExpiringToken.objects.get(key=spauthn)
exp = token.expired()
if not exp:
return super().dispatch(request, *args, **kwargs)
except Exception: # pylint: disable=broad-except
return self.handle_no_permission()

def handle_no_permission(self, request_data=None):
"""未登录用户跳转登录页面"""
if self.raise_exception:
raise PermissionDenied(self.get_permission_denied_message())
return HttpResponseRedirect(settings.ALIYUN_ROLE_SSO_LOGIN_URL)


@method_decorator(never_cache, name='dispatch')
class AliyunRoleSSOView(AliyunRoleSSOAccessMixin, IdPHandlerViewMixin, View):
"""
阿里云角色SSO登录
"""

SP_ENTITY_ID = 'urn:alibaba:cloudcomputing'
IN_RESPONSE_TO = 'https://signin.aliyun.com/saml-role/sso'
DESTINATION = 'https://signin.aliyun.com/saml-role/sso'
CUSTOM_CONFIG = {
'role': 'https://www.aliyun.com/SAML-Role/Attributes/Role',
'role_session_name': 'https://www.aliyun.com/SAML-Role/Attributes/RoleSessionName',
'session_duration': 'https://www.aliyun.com/SAML-Role/Attributes/SessionDuration',
}

def cookie_user(self, request): # pylint: disable=no-self-use
"""
返回cookie对应的用户
"""
try:
spauthn = request.COOKIES['spauthn']
token = ExpiringToken.objects.get(key=spauthn)
return token.user
except Exception: # pylint: disable=broad-except
return request.user

def get(self, request, *args, **kwargs): # pylint: disable=missing-function-docstring, unused-argument, too-many-locals
resp_args = {
'in_response_to': self.IN_RESPONSE_TO,
'sp_entity_id': self.SP_ENTITY_ID,
'name_id_policy': saml.NAMEID_FORMAT_PERSISTENT,
'binding': BINDING_HTTP_POST,
'destination': self.DESTINATION,
}
sp_config = {
'processor': 'djangosaml2idp.processors.BaseProcessor',
'attribute_mapping': {
'username': 'username',
'token': 'token',
'aliyun_sso_roles': self.CUSTOM_CONFIG['role'],
'display_name': self.CUSTOM_CONFIG['role_session_name'],
'aliyun_sso_session_duration': self.CUSTOM_CONFIG['session_duration'],
},
}
processor = self.get_processor(resp_args['sp_entity_id'], sp_config)
# Check if user has access to the service of this SP
if not processor.has_access(request):
return self.handle_error(request,
exception=PermissionDenied("You do not have access to this resource"),
status=403)
cookie_user = self.cookie_user(request)
if not cookie_user.aliyun_sso_role.is_active:
# 用户的角色SSO被禁用
return self.handle_error(request, exception=PermissionDenied("Your role SSO has been disabled"), status=403)
identity = self.get_identity(processor, cookie_user, sp_config)
# print('identity is', identity)
AUTHN_BROKER = AuthnBroker() # pylint: disable=invalid-name
AUTHN_BROKER.add(authn_context_class_ref(PASSWORD), "")
user_id = processor.get_user_id(cookie_user)
# Construct SamlResponse message
try:
app = SAMLAPP.valid_objects.get(entity_id=resp_args['sp_entity_id'])
_spsso_descriptor = entity_descriptor_from_string(app.xmldata).spsso_descriptor.pop() # pylint: disable=no-member
authn_resp = self.IDP.create_authn_response(identity=identity,
userid=user_id,
name_id=NameID(format=resp_args['name_id_policy'],
sp_name_qualifier=resp_args['sp_entity_id'],
text=user_id),
authn=AUTHN_BROKER.get_authn_by_accr(PASSWORD),
sign_response=getattr(_spsso_descriptor, 'want_response_signed',
'') == 'true',
sign_assertion=getattr(_spsso_descriptor,
'want_assertions_signed', '') == 'true',
**resp_args)
except Exception as excp: # pylint: disable=broad-except
return self.handle_error(request, exception=excp, status=500)
# print('authn_resp is', authn_resp)
http_args = self.IDP.apply_binding(binding=resp_args['binding'],
msg_str="%s" % authn_resp,
destination=resp_args['destination'],
response=True)
return HttpResponse(http_args['data'])
27 changes: 27 additions & 0 deletions djangosaml2idp/templates/dev/aliyun_role_sso_login.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{#oneid_base_url需要更改为本地开发环境IP:port#}

Aliyun Role SSO Login
<form class='form' method="POST">
username<input name="username">
password<input name="password">
<button type='button' id='submit'>submit</button>
</form>
<script src="https://code.jquery.com/jquery-latest.js"></script>
<script type="text/javascript">
const oneid_base_url = 'http://localhost:8000'
let next = oneid_base_url + '/saml/aliyun/sso-role/login/';

$('#submit').click(function(){
const username = $('input[name=username]').val();
const password = $('input[name=password]').val();
$.ajax({
method: 'POST',
url: '{{token_url}}',
data: {'username': username, 'password': password},
success: function(){
window.location.href = next;
}
})
return false;
})
</script>
2 changes: 2 additions & 0 deletions djangosaml2idp/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@
# path('login/process_multi_factor/', idpview.ProcessMultiFactorView.as_view(), name='saml_multi_factor'),
path('metadata/', idpview.metadata, name='saml2_idp_metadata'),
path('download/metadata/', idpview.download_metadata, name='saml2_idp_download_metadata'),
path('aliyun/sso-role/login/', idpview.AliyunRoleSSOView.as_view(), name='aliyun_role_sso_login'),
]

fe_urlpatterns = [
path('fe/login/', dev_views.LoginView.as_view(), name='fe_login'),
path('aliyun/sso-role/fe/login/', dev_views.AliyunRoleSSOLoginView.as_view(), name='aliyun_role_sso_fe_login'),
]

urlpatterns = base_urlpatterns + fe_urlpatterns
1 change: 1 addition & 0 deletions oneid/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@

FE_TOKEN_URL = '/oauth/fe/token/'
SAML_LOGIN_URL = '/saml/fe/login/'
ALIYUN_ROLE_SSO_LOGIN_URL = '/saml/aliyun/sso-role/fe/login/'

# TODO
FE_EMAIL_REGISTER_URL = '/oneid#/oneid/signup' # 邮件注册页面
Expand Down
33 changes: 33 additions & 0 deletions oneid_meta/migrations/0076_aliyunssorole.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Generated by Django 2.2.10 on 2020-07-24 07:37

from django.db import migrations, models
import django.db.models.deletion
import jsonfield.fields
import uuid


class Migration(migrations.Migration):

dependencies = [
('oneid_meta', '0075_auto_20200612_1042'),
]

operations = [
migrations.CreateModel(
name='AliyunSSORole',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('uuid', models.UUIDField(default=uuid.uuid4, unique=True)),
('is_del', models.BooleanField(default=False, verbose_name='是否删除')),
('is_active', models.BooleanField(default=True, verbose_name='是否可用')),
('updated', models.DateTimeField(auto_now=True, null=True, verbose_name='更新时间')),
('created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='创建时间')),
('role', jsonfield.fields.JSONField(blank=True, default=[], verbose_name='阿里云SSO角色分配')),
('session_duration', models.IntegerField(blank=True, default=900)),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='aliyun_sso_role', to='oneid_meta.User', verbose_name='用户')),
],
options={
'abstract': False,
},
),
]
2 changes: 1 addition & 1 deletion oneid_meta/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
'''

from oneid_meta.models.user import (User, PosixUser, CustomUser, DingUser, AlipayUser,\
WorkWechatUser, WechatUser, QQUser, SubAccount)
WorkWechatUser, WechatUser, QQUser, SubAccount, AliyunSSORole)

from oneid_meta.models.dept import (
Dept,
Expand Down
22 changes: 22 additions & 0 deletions oneid_meta/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from itertools import chain

import django
import jsonfield
from django.core.cache import cache
from django.db import models
from django.conf import settings
Expand Down Expand Up @@ -563,6 +564,18 @@ def update_last_active_time(self, gap_minutes=5):
self.last_active_time = now
self.save(update_fields=['last_active_time'])

@property
def aliyun_sso_roles(self):
"""与阿里云角色sso相关联信息"""
# pylint: disable=no-member
return self.aliyun_sso_role.role if self.aliyun_sso_role.is_active else []

@property
def aliyun_sso_session_duration(self):
"""与阿里云角色sso相关联信息"""
# pylint: disable=no-member
return str(self.aliyun_sso_role.session_duration)


class PosixUser(BaseModel):
'''
Expand Down Expand Up @@ -689,3 +702,12 @@ class SubAccount(BaseModel):
domain = models.CharField(max_length=255, verbose_name='登录域名')
username = models.CharField(max_length=255, default="", null=True, verbose_name='用户名')
password = models.CharField(max_length=512, verbose_name='密码、token')


class AliyunSSORole(BaseModel):
"""
阿里云角色SSO与user对接信息
"""
user = models.OneToOneField(User, verbose_name='用户', related_name='aliyun_sso_role', on_delete=models.CASCADE)
role = jsonfield.JSONField(default=[], blank=True, verbose_name='阿里云SSO角色分配')
session_duration = models.IntegerField(blank=True, default=900)

0 comments on commit bdc71de

Please sign in to comment.