Commit f8378530 authored by Valentin Samir's avatar Valentin Samir Committed by GitHub

Merge pull request #8 from nitmir/dev

Merge dev into master
parents 4ad4d13b d3b4e230
......@@ -4,12 +4,13 @@
*.swp
build/
bootstrap3
cas/
dist/
db.sqlite3
manage.py
coverage.xml
docs/_build/
docs/django.inv
.tox
test_venv
......@@ -17,3 +18,4 @@ test_venv
htmlcov/
tox_logs/
.cache/
.eggs/
language: python
python:
- "2.7"
env:
matrix:
- TOX_ENV=coverage
- TOX_ENV=flake8
- TOX_ENV=check_rst
- TOX_ENV=py27-django17
- TOX_ENV=py27-django18
- TOX_ENV=py27-django19
- TOX_ENV=py34-django17
- TOX_ENV=py34-django18
- TOX_ENV=py34-django19
matrix:
include:
- python: "2.7"
env: TOX_ENV=coverage
- python: "2.7"
env: TOX_ENV=flake8
- python: "2.7"
env: TOX_ENV=check_rst
- python: "2.7"
env: TOX_ENV=py27-django17
- python: "2.7"
env: TOX_ENV=py27-django18
- python: "2.7"
env: TOX_ENV=py27-django19
- python: "3.4"
env: TOX_ENV=py34-django17
- python: "3.4"
env: TOX_ENV=py34-django18
- python: "3.4"
env: TOX_ENV=py34-django19
- python: "3.5"
env: TOX_ENV=py35-django18
- python: "3.5"
env: TOX_ENV=py35-django19
cache:
directories:
- $HOME/.cache/pip/http/
......
include tox.ini
include LICENSE
include README.rst
include .coveragerc
include Makefile
include pytest.ini
include requirements-dev.txt
include requirements.txt
prune .tox
recursive-include cas_server/static *
recursive-include cas_server/templates *
recursive-include cas_server/locale *
include docs/conf.py
include docs/index.rst
include docs/Makefile
include docs/README.rst
recursive-include docs/_ext *
recursive-include docs/package *
recursive-include docs/_static *
recursive-include docs/_templates *
.PHONY: build dist
.PHONY: build dist docs
VERSION=`python setup.py -V`
build:
......@@ -24,10 +24,14 @@ clean_coverage:
rm -rf coverage.xml .coverage htmlcov
clean_tild_backup:
find ./ -name '*~' -delete
clean_docs:
rm -rf docs/_build/ docs/django.inv
clean_eggs:
rm -rf .eggs/
clean: clean_pyc clean_build clean_coverage clean_tild_backup
clean_all: clean clean_tox clean_test_venv
clean_all: clean clean_tox clean_test_venv clean_docs clean_eggs
dist:
python setup.py sdist
......@@ -40,7 +44,7 @@ test_venv/cas/manage.py: test_venv
mkdir -p test_venv/cas
test_venv/bin/django-admin startproject cas test_venv/cas
ln -s ../../cas_server test_venv/cas/cas_server
sed -i "s/'django.contrib.staticfiles',/'django.contrib.staticfiles',\n 'bootstrap3',\n 'cas_server',/" test_venv/cas/cas/settings.py
sed -i "s/'django.contrib.staticfiles',/'django.contrib.staticfiles',\n 'cas_server',/" test_venv/cas/cas/settings.py
sed -i "s/'django.middleware.clickjacking.XFrameOptionsMiddleware',/'django.middleware.clickjacking.XFrameOptionsMiddleware',\n 'django.middleware.locale.LocaleMiddleware',/" test_venv/cas/cas/settings.py
sed -i 's/from django.conf.urls import url/from django.conf.urls import url, include/' test_venv/cas/cas/urls.py
sed -i "s@url(r'^admin/', admin.site.urls),@url(r'^admin/', admin.site.urls),\n url(r'^', include('cas_server.urls', namespace='cas_server')),@" test_venv/cas/cas/urls.py
......@@ -60,3 +64,12 @@ run_tests: test_venv
python setup.py check --restructuredtext --stric
test_venv/bin/py.test --cov=cas_server --cov-report html
rm htmlcov/coverage_html.js # I am really pissed off by those keybord shortcuts
test_venv/bin/sphinx-build: test_venv
test_venv/bin/pip install Sphinx sphinx_rtd_theme
docs: test_venv/bin/sphinx-build
bash -c "source test_venv/bin/activate; cd docs; make html"
publish_pypi_release:
python setup.py sdist bdist_wheel upload --sign
CAS Server
##########
.. image:: https://travis-ci.org/nitmir/django-cas-server.svg?branch=master
:target: https://travis-ci.org/nitmir/django-cas-server
.. image:: https://img.shields.io/pypi/v/django-cas-server.svg
:target: https://pypi.python.org/pypi/django-cas-server
.. image:: https://img.shields.io/pypi/l/django-cas-server.svg
:target: https://www.gnu.org/licenses/gpl-3.0.html
.. image:: https://api.codacy.com/project/badge/Grade/255c21623d6946ef8802fa7995b61366
:target: https://www.codacy.com/app/valentin-samir/django-cas-server
.. image:: https://api.codacy.com/project/badge/Coverage/255c21623d6946ef8802fa7995b61366
:target: https://www.codacy.com/app/valentin-samir/django-cas-server
|travis| |version| |lisence| |codacy| |coverage|
CAS Server is a Django application implementing the `CAS Protocol 3.0 Specification
<https://apereo.github.io/cas/4.2.x/protocol/CAS-Protocol-Specification.html>`_.
......@@ -22,13 +9,6 @@ CAS Server is a Django application implementing the `CAS Protocol 3.0 Specificat
By default, the authentication process use django internal users but you can easily
use any sources (see auth classes in the auth.py file)
The default login/logout template use `django-bootstrap3 <https://github.com/dyve/django-bootstrap3>`__
but you can use your own templates using settings variables.
Note that for Django 1.7 compatibility, you need a version of
`django-bootstrap3 <https://github.com/dyve/django-bootstrap3>`__ < 7.0.0
like the 6.2.2 version.
.. contents:: Table of Contents
Features
......@@ -52,8 +32,6 @@ Dependencies
* Django >= 1.7 < 1.10
* requests >= 2.4
* requests_futures >= 0.9.5
* django-picklefield >= 0.3.1
* django-bootstrap3 >= 5.4 (< 7.0.0 if using django 1.7)
* lxml >= 3.4
* six >= 1
......@@ -68,7 +46,7 @@ The recommended installation mode is to use a virtualenv with ``--system-site-pa
On debian like systems::
$ sudo apt-get install python-django python-requests python-django-picklefield python-six python-lxml
$ sudo apt-get install python-django python-requests python-six python-lxml python-requests-futures
On debian jessie, you can use the version of python-django available in the
`backports <https://backports.debian.org/Instructions/>`_.
......@@ -118,7 +96,6 @@ Quick start
INSTALLED_APPS = (
'django.contrib.admin',
...
'bootstrap3',
'cas_server',
)
......@@ -186,6 +163,17 @@ Template settings
* ``CAS_LOGO_URL``: URL to the logo showed in the up left corner on the default
templates. Set it to ``False`` to disable it.
* ``CAS_COMPONENT_URLS``: URLs to css and javascript external components. It is a dictionnary
and it must have the five following keys: ``"bootstrap3_css"``, ``"bootstrap3_js"``,
``"html5shiv"``, ``"respond"``, ``"jquery"``. The default is::
{
"bootstrap3_css": "//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css",
"bootstrap3_js": "//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js",
"html5shiv": "//oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js",
"respond": "//oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js",
"jquery": "//code.jquery.com/jquery.min.js",
}
* ``CAS_LOGIN_TEMPLATE``: Path to the template showed on ``/login`` then the user
is not autenticated. The default is ``"cas_server/login.html"``.
......@@ -489,3 +477,20 @@ You could for example do as bellow :
.. code-block::
10 0 * * * cas-user /path/to/project/manage.py cas_clean_federate
.. |travis| image:: https://badges.genua.fr/travis/nitmir/django-cas-server/master.svg
:target: https://travis-ci.org/nitmir/django-cas-server
.. |version| image:: https://badges.genua.fr/pypi/v/django-cas-server.svg
:target: https://pypi.python.org/pypi/django-cas-server
.. |lisence| image:: https://badges.genua.fr/pypi/l/django-cas-server.svg
:target: https://www.gnu.org/licenses/gpl-3.0.html
.. |codacy| image:: https://badges.genua.fr/codacy/grade/255c21623d6946ef8802fa7995b61366/master.svg
:target: https://www.codacy.com/app/valentin-samir/django-cas-server
.. |coverage| image:: https://badges.genua.fr/codacy/coverage/255c21623d6946ef8802fa7995b61366/master.svg
:target: https://www.codacy.com/app/valentin-samir/django-cas-server
......@@ -9,4 +9,5 @@
#
# (c) 2015-2016 Valentin Samir
"""A django CAS server application"""
#: path the the application configuration class
default_app_config = 'cas_server.apps.CasAppConfig'
......@@ -15,86 +15,155 @@ from .models import Username, ReplaceAttributName, ReplaceAttributValue, FilterA
from .models import FederatedIendityProvider
from .forms import TicketForm
TICKETS_READONLY_FIELDS = ('validate', 'service', 'service_pattern',
'creation', 'renew', 'single_log_out', 'value')
TICKETS_FIELDS = ('validate', 'service', 'service_pattern',
'creation', 'renew', 'single_log_out')
class BaseInlines(admin.TabularInline):
"""
Bases: :class:`django.contrib.admin.TabularInline`
class ServiceTicketInline(admin.TabularInline):
"""`ServiceTicket` in admin interface"""
model = ServiceTicket
Base class for inlines in the admin interface.
"""
#: This controls the number of extra forms the formset will display in addition to
#: the initial forms.
extra = 0
class UserAdminInlines(BaseInlines):
"""
Bases: :class:`BaseInlines`
Base class for inlines in :class:`UserAdmin` interface
"""
#: The form :class:`TicketForm<cas_server.forms.TicketForm>` used to display tickets.
form = TicketForm
readonly_fields = TICKETS_READONLY_FIELDS
fields = TICKETS_FIELDS
#: Fields to display on a object that are read only (not editable).
readonly_fields = (
'validate', 'service', 'service_pattern',
'creation', 'renew', 'single_log_out', 'value'
)
#: Fields to display on a object.
fields = (
'validate', 'service', 'service_pattern',
'creation', 'renew', 'single_log_out'
)
class ServiceTicketInline(UserAdminInlines):
"""
Bases: :class:`UserAdminInlines`
class ProxyTicketInline(admin.TabularInline):
"""`ProxyTicket` in admin interface"""
:class:`ServiceTicket<cas_server.models.ServiceTicket>` in admin interface
"""
#: The model which the inline is using.
model = ServiceTicket
class ProxyTicketInline(UserAdminInlines):
"""
Bases: :class:`UserAdminInlines`
:class:`ProxyTicket<cas_server.models.ProxyTicket>` in admin interface
"""
#: The model which the inline is using.
model = ProxyTicket
extra = 0
form = TicketForm
readonly_fields = TICKETS_READONLY_FIELDS
fields = TICKETS_FIELDS
class ProxyGrantingInline(admin.TabularInline):
"""`ProxyGrantingTicket` in admin interface"""
class ProxyGrantingInline(UserAdminInlines):
"""
Bases: :class:`UserAdminInlines`
:class:`ProxyGrantingTicket<cas_server.models.ProxyGrantingTicket>` in admin interface
"""
#: The model which the inline is using.
model = ProxyGrantingTicket
extra = 0
form = TicketForm
readonly_fields = TICKETS_READONLY_FIELDS
fields = TICKETS_FIELDS[1:]
class UserAdmin(admin.ModelAdmin):
"""`User` in admin interface"""
"""
Bases: :class:`django.contrib.admin.ModelAdmin`
:class:`User<cas_server.models.User>` in admin interface
"""
#: See :class:`ServiceTicketInline`, :class:`ProxyTicketInline`, :class:`ProxyGrantingInline`
#: objects below the :class:`UserAdmin` fields.
inlines = (ServiceTicketInline, ProxyTicketInline, ProxyGrantingInline)
#: Fields to display on a object that are read only (not editable).
readonly_fields = ('username', 'date', "session_key")
#: Fields to display on a object.
fields = ('username', 'date', "session_key")
#: Fields to display on the list of class:`UserAdmin` objects.
list_display = ('username', 'date', "session_key")
class UsernamesInline(admin.TabularInline):
"""`Username` in admin interface"""
class UsernamesInline(BaseInlines):
"""
Bases: :class:`BaseInlines`
:class:`Username<cas_server.models.Username>` in admin interface
"""
#: The model which the inline is using.
model = Username
extra = 0
class ReplaceAttributNameInline(admin.TabularInline):
"""`ReplaceAttributName` in admin interface"""
class ReplaceAttributNameInline(BaseInlines):
"""
Bases: :class:`BaseInlines`
:class:`ReplaceAttributName<cas_server.models.ReplaceAttributName>` in admin interface
"""
#: The model which the inline is using.
model = ReplaceAttributName
extra = 0
class ReplaceAttributValueInline(admin.TabularInline):
"""`ReplaceAttributValue` in admin interface"""
class ReplaceAttributValueInline(BaseInlines):
"""
Bases: :class:`BaseInlines`
:class:`ReplaceAttributValue<cas_server.models.ReplaceAttributValue>` in admin interface
"""
#: The model which the inline is using.
model = ReplaceAttributValue
extra = 0
class FilterAttributValueInline(admin.TabularInline):
"""`FilterAttributValue` in admin interface"""
class FilterAttributValueInline(BaseInlines):
"""
Bases: :class:`BaseInlines`
:class:`FilterAttributValue<cas_server.models.FilterAttributValue>` in admin interface
"""
#: The model which the inline is using.
model = FilterAttributValue
extra = 0
class ServicePatternAdmin(admin.ModelAdmin):
"""`ServicePattern` in admin interface"""
"""
Bases: :class:`django.contrib.admin.ModelAdmin`
:class:`ServicePattern<cas_server.models.ServicePattern>` in admin interface
"""
#: See :class:`UsernamesInline`, :class:`ReplaceAttributNameInline`,
#: :class:`ReplaceAttributValueInline`, :class:`FilterAttributValueInline` objects below
#: the :class:`ServicePatternAdmin` fields.
inlines = (
UsernamesInline,
ReplaceAttributNameInline,
ReplaceAttributValueInline,
FilterAttributValueInline
)
#: Fields to display on the list of class:`ServicePatternAdmin` objects.
list_display = ('pos', 'name', 'pattern', 'proxy',
'single_log_out', 'proxy_callback', 'restrict_users')
class FederatedIendityProviderAdmin(admin.ModelAdmin):
"""`FederatedIendityProvider` in admin interface"""
"""
Bases: :class:`django.contrib.admin.ModelAdmin`
:class:`FederatedIendityProvider<cas_server.models.FederatedIendityProvider>` in admin
interface
"""
#: Fields to display on a object.
fields = ('pos', 'suffix', 'server_url', 'cas_protocol_version', 'verbose_name', 'display')
#: Fields to display on the list of class:`FederatedIendityProviderAdmin` objects.
list_display = ('verbose_name', 'suffix', 'display')
......
......@@ -14,6 +14,12 @@ from django.apps import AppConfig
class CasAppConfig(AppConfig):
"""django CAS application config class"""
"""
Bases: :class:`django.apps.AppConfig`
django CAS application config class
"""
#: Full Python path to the application. It must be unique across a Django project.
name = 'cas_server'
#: Human-readable name for the application.
verbose_name = _('Central Authentication Service')
......@@ -26,55 +26,112 @@ from .models import FederatedUser
class AuthUser(object):
"""Authentication base class"""
"""
Authentication base class
:param unicode username: A username, stored in the :attr:`username` class attribute.
"""
#: username used to instanciate the current object
username = None
def __init__(self, username):
self.username = username
def test_password(self, password):
"""test `password` agains the user"""
"""
Tests ``password`` agains the user password.
:raises NotImplementedError: always. The method need to be implemented by subclasses
"""
raise NotImplementedError()
def attributs(self):
"""return a dict of user attributes"""
"""
The user attributes.
raises NotImplementedError: always. The method need to be implemented by subclasses
"""
raise NotImplementedError()
class DummyAuthUser(AuthUser): # pragma: no cover
"""A Dummy authentication class"""
"""
A Dummy authentication class. Authentication always fails
def __init__(self, username):
super(DummyAuthUser, self).__init__(username)
:param unicode username: A username, stored in the :attr:`username<AuthUser.username>`
class attribute. There is no valid value for this attribute here.
"""
def test_password(self, password):
"""test `password` agains the user"""
"""
Tests ``password`` agains the user password.
:param unicode password: a clear text password as submited by the user.
:return: always ``False``
:rtype: bool
"""
return False
def attributs(self):
"""return a dict of user attributes"""
"""
The user attributes.
:return: en empty :class:`dict`.
:rtype: dict
"""
return {}
class TestAuthUser(AuthUser):
"""A test authentication class with one user test having
alose test as password and some attributes"""
"""
A test authentication class only working for one unique user.
def __init__(self, username):
super(TestAuthUser, self).__init__(username)
:param unicode username: A username, stored in the :attr:`username<AuthUser.username>`
class attribute. The uniq valid value is ``settings.CAS_TEST_USER``.
"""
def test_password(self, password):
"""test `password` agains the user"""
"""
Tests ``password`` agains the user password.
:param unicode password: a clear text password as submited by the user.
:return: ``True`` if :attr:`username<AuthUser.username>` is valid and
``password`` is equal to ``settings.CAS_TEST_PASSWORD``, ``False`` otherwise.
:rtype: bool
"""
return self.username == settings.CAS_TEST_USER and password == settings.CAS_TEST_PASSWORD
def attributs(self):
"""return a dict of user attributes"""
return settings.CAS_TEST_ATTRIBUTES
"""
The user attributes.
:return: the ``settings.CAS_TEST_ATTRIBUTES`` :class:`dict` if
:attr:`username<AuthUser.username>` is valid, an empty :class:`dict` otherwise.
:rtype: dict
"""
if self.username == settings.CAS_TEST_USER:
return settings.CAS_TEST_ATTRIBUTES
else: # pragma: no cover (should not happen)
return {}
class MysqlAuthUser(AuthUser): # pragma: no cover
"""A mysql auth class: authentication user agains a mysql database"""
"""
A mysql authentication class: authentication user agains a mysql database
:param unicode username: A username, stored in the :attr:`username<AuthUser.username>`
class attribute. Valid value are fetched from the MySQL database set with
``settings.CAS_SQL_*`` settings parameters using the query
``settings.CAS_SQL_USER_QUERY``.
"""
#: Mysql user attributes as a :class:`dict` if the username is found in the database.
user = None
def __init__(self, username):
# see the connect function at
# http://mysql-python.sourceforge.net/MySQLdb.html#functions-and-attributes
# for possible mysql config parameters.
mysql_config = {
"user": settings.CAS_SQL_USERNAME,
"passwd": settings.CAS_SQL_PASSWORD,
......@@ -94,7 +151,14 @@ class MysqlAuthUser(AuthUser): # pragma: no cover
super(MysqlAuthUser, self).__init__(username)
def test_password(self, password):
"""test `password` agains the user"""
"""
Tests ``password`` agains the user password.
:param unicode password: a clear text password as submited by the user.
:return: ``True`` if :attr:`username<AuthUser.username>` is valid and ``password`` is
correct, ``False`` otherwise.
:rtype: bool
"""
if self.user:
return check_password(
settings.CAS_SQL_PASSWORD_CHECK,
......@@ -106,7 +170,14 @@ class MysqlAuthUser(AuthUser): # pragma: no cover
return False
def attributs(self):
"""return a dict of user attributes"""
"""
The user attributes.
:return: a :class:`dict` with the user attributes. Attributes may be :func:`unicode`
or :class:`list` of :func:`unicode`. If the user do not exists, the returned
:class:`dict` is empty.
:rtype: dict
"""
if self.user:
return self.user
else:
......@@ -114,7 +185,14 @@ class MysqlAuthUser(AuthUser): # pragma: no cover
class DjangoAuthUser(AuthUser): # pragma: no cover
"""A django auth class: authenticate user agains django internal users"""
"""
A django auth class: authenticate user agains django internal users
:param unicode username: A username, stored in the :attr:`username<AuthUser.username>`
class attribute. Valid value are usernames of django internal users.
"""
#: a django user object if the username is found. The user model is retreived
#: using :func:`django.contrib.auth.get_user_model`.
user = None
def __init__(self, username):
......@@ -126,14 +204,27 @@ class DjangoAuthUser(AuthUser): # pragma: no cover
super(DjangoAuthUser, self).__init__(username)
def test_password(self, password):
"""test `password` agains the user"""
"""
Tests ``password`` agains the user password.
:param unicode password: a clear text password as submited by the user.
:return: ``True`` if :attr:`user` is valid and ``password`` is
correct, ``False`` otherwise.
:rtype: bool
"""
if self.user:
return self.user.check_password(password)
else:
return False
def attributs(self):
"""return a dict of user attributes"""
"""
The user attributes, defined as the fields on the :attr:`user` object.
:return: a :class:`dict` with the :attr:`user` object fields. Attributes may be
If the user do not exists, the returned :class:`dict` is empty.
:rtype: dict
"""
if self.user:
attr = {}
for field in self.user._meta.fields:
......@@ -144,7 +235,16 @@ class DjangoAuthUser(AuthUser): # pragma: no cover
class CASFederateAuth(AuthUser):
"""Authentication class used then CAS_FEDERATE is True"""
"""
Authentication class used then CAS_FEDERATE is True
:param unicode username: A username, stored in the :attr:`username<AuthUser.username>`
class attribute. Valid value are usernames of
:class:`FederatedUser<cas_server.models.FederatedUser>` object.
:class:`FederatedUser<cas_server.models.FederatedUser>` object are created on CAS
backends successful ticket validation.
"""
#: a :class`FederatedUser<cas_server.models.FederatedUser>` object if ``username`` is found.
user = None
def __init__(self, username):
......@@ -157,7 +257,17 @@ class CASFederateAuth(AuthUser):
super(CASFederateAuth, self).__init__(username)
def test_password(self, ticket):
"""test `password` agains the user"""
"""
Tests ``password`` agains the user password.
:param unicode password: The CAS tickets just used to validate the user authentication
against its CAS backend.
:return: ``True`` if :attr:`user` is valid and ``password`` is
a ticket validated less than ``settings.CAS_TICKET_VALIDITY`` secondes and has not
being previously used for authenticated this
:class:`FederatedUser<cas_server.models.FederatedUser>`. ``False`` otherwise.
:rtype: bool
"""
if not self.user or not self.user.ticket:
return False
else:
......@@ -168,7 +278,13 @@ class CASFederateAuth(AuthUser):
)
def attributs(self):
"""return a dict of user attributes"""
"""
The user attributes, as returned by the CAS backend.
:return: :obj:`FederatedUser.attributs<cas_server.models.FederatedUser.attributs>`.
If the user do not exists, the returned :class:`dict` is empty.
:rtype: dict
"""
if not self.user: # pragma: no cover (should not happen)
return {}
else:
......
......@@ -36,7 +36,7 @@ class CASError(ValueError):
class ReturnUnicode(object):
@staticmethod
def unicode(string, charset):
def u(string, charset):
if not isinstance(string, six.text_type):
return string.decode(charset)
else:
......@@ -157,7 +157,7 @@ class CASClientV1(CASClientBase, ReturnUnicode):
charset = content_type.split("charset=")[-1]
else:
charset = "ascii"
user = self.unicode(page.readline().strip(), charset)
user = self.u(page.readline().strip(), charset)
return user, None, None
else:
return None, None, None
......@@ -202,18 +202,18 @@ class CASClientV2(CASClientBase, ReturnUnicode):
def parse_attributes_xml_element(cls, element, charset):
attributes = dict()
for attribute in element:
tag = cls.self.unicode(attribute.tag, charset).split(u"}").pop()
tag = cls.self.u(attribute.tag, charset).split(u"}").pop()
if tag in attributes:
if isinstance(attributes[tag], list):
attributes[tag].append(cls.unicode(attribute.text, charset))
attributes[tag].append(cls.u(attribute.text, charset))
else:
attributes[tag] = [attributes[tag]]
attributes[tag].append(cls.unicode(attribute.text, charset))
attributes[tag].append(cls.u(attribute.text, charset))
else:
if tag == u'attraStyle':
pass
else:
attributes[tag] = cls.unicode(attribute.text, charset)
attributes[tag] = cls.u(attribute.text, charset)
return attributes
@classmethod
......@@ -238,9 +238,9 @@ class CASClientV2(CASClientBase, ReturnUnicode):
if tree[0].tag.endswith('authenticationSuccess'):
for element in tree[0]:
if element.tag.endswith('user'):
user = cls.unicode(element.text, charset)
user = cls.u(element.text, charset)
elif element.tag.endswith('proxyGrantingTicket'):
pgtiou = cls.unicode(element.text, charset)
pgtiou = cls.u(element.text, charset)
elif element.tag.endswith('attributes'):
attributes = cls.parse_attributes_xml_element(element, charset)
return user, attributes, pgtiou
......@@ -255,15 +255,15 @@ class CASClientV3(CASClientV2, SingleLogoutMixin):
def parse_attributes_xml_element(cls, element, charset):
attributes = dict()
for attribute in element:
tag = cls.unicode(attribute.tag, charset).split(u"}").pop()
tag = cls.u(attribute.tag, charset).split(u"}").pop()
if tag in attributes: