2021SC@SDUSC
在进行了六次seahub-frontend前端组件代码分析之后,大致熟悉了代码风格以及组件的实现以及应用和在UI界面中与之对应的内容。所以这次决定先暂停一下frontend的代码分析工作,将视角转到seahub-extra中。这一部分是为seafile增加的一部分新的功能,源码见seahub-extra。
对于seahub-extra的描述是:The extra pro contents for Seahub.主要是进行CAS统一身份认证使用。
这一部分因为是服务端的内容,对我来说,我另一个队友王子安负责seahub后端代码的分析,但在分工时,我也承担下来了这一部分的代码分析任务。
seahub-extra目录结构:
在adfs_auth目录中,包含一个Attribute-maps文件夹,顾名思义就是属性映射集合。
先来看一下basic.py中的内容,简单截取了一部分:
经查找资料了解到,这是SAML2.0版本协议。SAML 即安全断言标记语言,英文全称是 Security Assertion Markup Language。它是一个基于 XML 的标准,用于在不同的安全域(security domain)之间交换认证和授权数据。在 SAML 标准定义了身份提供者 (identity provider) 和服务提供者 (service provider),这两者构成了前面所说的不同的安全域。也如上图显示的一样,定义了常见资源访问操作的基于URI的标识符、使用者名称标识符格式和属性名称格式。在可能的情况下,使用现有的URN来指定协议。在IETF协议的情况下,使用指定协议的最新RFC的URN。
主要作用:
认证声明:声明用户是否已经认证,通常用于单点登录。
属性声明:声明某个 Subject 所具有的属性。
授权决策声明:声明某个资源的权限,即一个用户在资源 R 上具有给定的 E 权限而能够执行 A 操作。
saml_uri.py中使用了eduPersonAffiliation,也就是在学生、教师、员工等广泛类别中指定用户与机构的关系。EduPersonAffiliation的主要目的是在身份联盟的成员之间传递广泛类别的从属关系断言。在这种跨机构背景下,只有在定义和实践上具有广泛共识的教育者从属价值观才有实际价值。Object类的当前版本中的允许值列表肯定是不完整的,特别是在本地机构使用方面。编辑们认为,任何额外的价值都应该来自与利益相关者社区的讨论。任何商定的附加值都将包含在更高版本的eduPerson中。
文件内容简单截取如下:
在本目录下的view.py代码中,实现了IDP将向该视图发送响应,该视图将使用pysaml2帮助对其进行处理,并使用自定义授权后端djangosaml2.backends.Saml2Backend(应该在settings.py中启用)让用户登录。
Customization文件夹下,是一个html文件,作用是用户注册认证,可以选择个人认证或组织认证。由于js和css文件丢失,网页不能正确显示。
在django-csa-ng中主要是进行了Django 集成 CAS 实现单点登陆。
locale文件夹则是负责将功能以多国语言的形式进行转换,实现多地用户使用时,语言本土化。这里不必细究。
management文件夹下的代码主要实现了清除链接到过期会话的SessionTicket和ProxyGrantingTicket,代码如下:
def handle(self, *args, **options):
models.ProxyGrantingTicket.clean_deleted_sessions()
models.SessionTicket.clean_deleted_sessions()
在backend.py中包含了CAS Authentication的一些具体方法:
get_user
通过用户名获得user对象
def get_user(self, username):
try:
user = User.objects.get(email=username)
except User.DoesNotExist:
user = None
return user
authenticate()
函数验证CAS票证并获取或创建用户对象
def authenticate(self, request, ticket, service):
"""Verifies CAS ticket and gets or creates User object"""
client = get_cas_client(service_url=service, request=request)
username, attributes, pgtiou = client.verify_ticket(ticket)
if attributes and request:
request.session['attributes'] = attributes
if not username:
return None
if CAS_SEAFILE_DOMAIN:
username = username.split('@')[0] + '@' + CAS_SEAFILE_DOMAIN
user = None
username = self.clean_username(username)
if attributes:
reject = self.bad_attributes_reject(request, username, attributes)
if reject:
return None
#如果满足条件,将按照设置文件中的说明重命名属性。
#现有属性将被覆盖
for cas_attr_name, req_attr_name in list(settings.CAS_RENAME_ATTRIBUTES.items()):
if cas_attr_name in attributes:
attributes[req_attr_name] = attributes[cas_attr_name]
attributes.pop(cas_attr_name)
#这可以在一个try-exclude子句中完成,但是我们在创建未知用户时使用get_or_create,因为它具有针对多线程的内置保护措施。
if settings.CAS_CREATE_USER:
user_kwargs = {
username: username
}
if settings.CAS_CREATE_USER_WITH_ID:
user_kwargs['id'] = self.get_user_id(attributes)
try: #成功获取到用户信息,则不进行创建
user = User.objects.get(email=username)
created = False
except User.DoesNotExist: #用户不存在则新建一个用户
user = User.objects.create_user(
email=username, is_active=True)
user = self.configure_user(user)
created = True
else:
created = False
try:
user = User.objects.get(email=username)
except User.DoesNotExist:
pass
if not self.user_can_authenticate(user):
return None
if pgtiou and settings.CAS_PROXY_CALLBACK and request:
request.session['pgtiou'] = pgtiou
# 在完成CAS认证工作流程之后,发送cas_user_Authenticated signal
cas_user_authenticated.send(
sender=self,
user=user,
created=created,
attributes=attributes,
ticket=ticket,
service=service,
request=request
)
return user
get_user_id
函数保证了在CAS_CREATE_USER_WITH_ID为True时使用,当无法访问user_id时,将引发不正确配置的异常。这一点很重要,因为如果我们试图保持用户主键的同步,就不应该使用自动分配的ID创建用户。
if not attributes:
raise ImproperlyConfigured("CAS_CREATE_USER_WITH_ID is True, but "
"no attributes were provided")
user_id = attributes.get('id')
if not user_id:
raise ImproperlyConfigured("CAS_CREATE_USER_WITH_ID is True, but "
"`'id'` is not part of attributes.")
return user_id
在使用“username”获取或删除“username”之前,对其执行任何清理,创建用户对象操作,返回清理后的用户名。默认情况下,根据settings.CAS_FORCE_CHANGE_USERNAME_CASE中定义好的内容进行返回。
def clean_username(self, username):
username_case = settings.CAS_FORCE_CHANGE_USERNAME_CASE
if username_case == 'lower':
username = username.lower()
elif username_case == 'upper':
username = username.upper()
elif username_case is not None:
raise ImproperlyConfigured(
"Invalid value for the CAS_FORCE_CHANGE_USERNAME_CASE setting. "
"Valid values are `'lower'`, `'upper'`, and `None`.")
return username
在decorators.py中,实现了如果用户已登录,则替换返回403Forted的django.contrib.auth.decorators.user_passes_test
def user_passes_test(test_func, login_url=None,
redirect_field_name=REDIRECT_FIELD_NAME):
if not login_url:
from django.conf import settings #从配置中获取url
login_url = settings.LOGIN_URL
def decorator(view_func):
@wraps(view_func)
def wrapper(request, *args, **kwargs):
if django.VERSION[0] < 2:
is_user_authenticated = request.user.is_authenticated
else:
is_user_authenticated = request.user.is_authenticated
if test_func(request.user): #符合条件返回正确的函数结果
return view_func(request, *args, **kwargs)
elif is_user_authenticated:
raise PermissionDenied #请求被拒绝
else: #重定向,回到登录界面
path = '%s?%s=%s' % (login_url, redirect_field_name,
urlquote(request.get_full_path()))
return HttpResponseRedirect(path)
return wrapper
return decorator
middleware.py
实现管理页面上的CAS身份验证
将对管理页面的未经身份验证的请求转发到CAS,登录URL,以及对django.contri.auth.views.login和注销的调用
以下是部分代码:
if settings.CAS_ADMIN_PREFIX:
if not request.path.startswith(settings.CAS_ADMIN_PREFIX):
return None
elif not view_func.__module__.startswith('django.contrib.admin.'):
return None
if django.VERSION[0] < 2:
is_user_authenticated = request.user.is_authenticated
else:
is_user_authenticated = request.user.is_authenticated
if is_user_authenticated:
if request.user.is_staff:
return None
else:
raise PermissionDenied(_('You do not have staff privileges.'))
params = urllib_parse.urlencode({REDIRECT_FIELD_NAME: request.get_full_path()})
return HttpResponseRedirect(reverse(cas_login) + '?' + params)
关于更多seahub-extra的代码分析,将在下次博客继续进行。