django-cas-server/cas_server/auth.py

434 lines
15 KiB
Python
Raw Normal View History

2016-07-03 16:11:48 +00:00
# -*- coding: utf-8 -*-
2015-05-27 20:10:06 +00:00
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License version 3 for
# more details.
#
# You should have received a copy of the GNU General Public License version 3
# along with this program; if not, write to the Free Software Foundation, Inc., 51
# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
2016-06-30 22:00:53 +00:00
# (c) 2015-2016 Valentin Samir
2015-05-27 19:56:39 +00:00
"""Some authentication classes for the CAS"""
2015-05-17 21:24:41 +00:00
from django.conf import settings
2015-12-12 16:26:19 +00:00
from django.contrib.auth import get_user_model
2016-06-17 17:28:49 +00:00
from django.utils import timezone
from django.db import connections, DatabaseError
2016-06-17 17:28:49 +00:00
import warnings
2016-06-17 17:28:49 +00:00
from datetime import timedelta
from six.moves import range
2016-06-26 13:34:26 +00:00
try: # pragma: no cover
2015-05-17 21:24:41 +00:00
import MySQLdb
import MySQLdb.cursors
except ImportError:
MySQLdb = None
2015-05-27 19:56:39 +00:00
try: # pragma: no cover
import ldap3
except ImportError:
ldap3 = None
2016-06-17 17:28:49 +00:00
from .models import FederatedUser
from .utils import check_password, dictfetchall
2016-06-17 17:28:49 +00:00
2015-06-12 16:10:52 +00:00
2015-12-12 16:26:19 +00:00
class AuthUser(object):
2016-07-20 16:28:23 +00:00
"""
Authentication base class
:param unicode username: A username, stored in the :attr:`username` class attribute.
"""
#: username used to instanciate the current object
username = None
2015-12-12 16:26:19 +00:00
def __init__(self, username):
self.username = username
def test_password(self, password):
2016-07-20 16:28:23 +00:00
"""
Tests ``password`` agains the user password.
:raises NotImplementedError: always. The method need to be implemented by subclasses
"""
2016-06-26 09:16:41 +00:00
raise NotImplementedError()
2015-12-12 16:26:19 +00:00
def attributs(self):
2016-07-20 16:28:23 +00:00
"""
The user attributes.
raises NotImplementedError: always. The method need to be implemented by subclasses
"""
2016-06-26 09:16:41 +00:00
raise NotImplementedError()
2015-12-12 16:26:19 +00:00
2016-06-26 13:34:26 +00:00
class DummyAuthUser(AuthUser): # pragma: no cover
2016-07-20 16:28:23 +00:00
"""
A Dummy authentication class. Authentication always fails
2015-06-12 16:10:52 +00:00
2016-07-20 16:28:23 +00:00
:param unicode username: A username, stored in the :attr:`username<AuthUser.username>`
class attribute. There is no valid value for this attribute here.
"""
2015-05-17 21:24:41 +00:00
def test_password(self, password):
2016-07-20 16:28:23 +00:00
"""
Tests ``password`` agains the user password.
:param unicode password: a clear text password as submited by the user.
:return: always ``False``
:rtype: bool
"""
2015-05-17 21:24:41 +00:00
return False
def attributs(self):
2016-07-20 16:28:23 +00:00
"""
The user attributes.
:return: en empty :class:`dict`.
:rtype: dict
"""
2015-05-17 21:24:41 +00:00
return {}
2015-12-12 16:26:19 +00:00
class TestAuthUser(AuthUser):
2016-07-20 16:28:23 +00:00
"""
A test authentication class only working for one unique user.
2015-06-12 16:10:52 +00:00
2016-07-20 16:28:23 +00:00
:param unicode username: A username, stored in the :attr:`username<AuthUser.username>`
class attribute. The uniq valid value is ``settings.CAS_TEST_USER``.
"""
2015-05-17 21:24:41 +00:00
def test_password(self, password):
2016-07-20 16:28:23 +00:00
"""
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
2015-05-17 21:24:41 +00:00
def attributs(self):
2016-07-20 16:28:23 +00:00
"""
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)
2016-07-20 16:28:23 +00:00
return {}
2015-05-17 21:24:41 +00:00
class DBAuthUser(AuthUser): # pragma: no cover
"""base class for databate based auth classes"""
#: DB user attributes as a :class:`dict` if the username is found in the database.
user = None
def attributs(self):
"""
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:
return {}
class MysqlAuthUser(DBAuthUser): # pragma: no cover
2016-07-20 16:28:23 +00:00
"""
DEPRECATED, use :class:`SqlAuthUser` instead.
A mysql authentication class: authenticate user agains a mysql database
2016-07-20 16:28:23 +00:00
: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``.
"""
2015-06-12 16:10:52 +00:00
2015-05-17 21:24:41 +00:00
def __init__(self, username):
warnings.warn(
(
"MysqlAuthUser authentication class is deprecated: "
"use cas_server.auth.SqlAuthUser instead"
),
UserWarning
)
2016-07-20 16:28:23 +00:00
# see the connect function at
# http://mysql-python.sourceforge.net/MySQLdb.html#functions-and-attributes
# for possible mysql config parameters.
2015-05-17 21:24:41 +00:00
mysql_config = {
2015-05-27 19:56:39 +00:00
"user": settings.CAS_SQL_USERNAME,
"passwd": settings.CAS_SQL_PASSWORD,
"db": settings.CAS_SQL_DBNAME,
"host": settings.CAS_SQL_HOST,
2015-06-12 16:10:52 +00:00
"charset": settings.CAS_SQL_DBCHARSET,
"cursorclass": MySQLdb.cursors.DictCursor
2015-05-17 21:24:41 +00:00
}
if not MySQLdb:
raise RuntimeError("Please install MySQLdb before using the MysqlAuthUser backend")
conn = MySQLdb.connect(**mysql_config)
curs = conn.cursor()
if curs.execute(settings.CAS_SQL_USER_QUERY, (username,)) == 1:
self.user = curs.fetchone()
super(MysqlAuthUser, self).__init__(self.user['username'])
else:
super(MysqlAuthUser, self).__init__(username)
2015-05-17 21:24:41 +00:00
def test_password(self, password):
2016-07-20 16:28:23 +00:00
"""
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
"""
2016-06-26 13:34:26 +00:00
if self.user:
2016-06-27 12:01:39 +00:00
return check_password(
settings.CAS_SQL_PASSWORD_CHECK,
password,
self.user["password"],
settings.CAS_SQL_DBCHARSET
)
2015-05-17 21:24:41 +00:00
else:
2016-06-26 13:34:26 +00:00
return False
2015-05-17 21:24:41 +00:00
class SqlAuthUser(DBAuthUser): # pragma: no cover
"""
A SQL authentication class: authenticate user agains a SQL database. The SQL database
must be configures in settings.py as ``settings.DATABASES['cas_server']``.
: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``.
"""
def __init__(self, username):
if "cas_server" not in connections:
raise RuntimeError("Please configure the 'cas_server' database in settings.DATABASES")
for retry_nb in range(3):
try:
with connections["cas_server"].cursor() as curs:
curs.execute(settings.CAS_SQL_USER_QUERY, (username,))
results = dictfetchall(curs)
if len(results) == 1:
self.user = results[0]
super(SqlAuthUser, self).__init__(self.user['username'])
else:
super(SqlAuthUser, self).__init__(username)
break
except DatabaseError:
connections["cas_server"].close()
if retry_nb == 2:
raise
def test_password(self, password):
2016-07-20 16:28:23 +00:00
"""
Tests ``password`` agains the user password.
2016-07-20 16:28:23 +00:00
: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
2016-07-20 16:28:23 +00:00
"""
2016-06-26 13:34:26 +00:00
if self.user:
return check_password(
settings.CAS_SQL_PASSWORD_CHECK,
password,
self.user["password"],
settings.CAS_SQL_PASSWORD_CHARSET
)
2016-06-26 13:34:26 +00:00
else:
return False
class LdapAuthUser(DBAuthUser): # pragma: no cover
"""
A ldap authentication class: authenticate user against a ldap database
:param unicode username: A username, stored in the :attr:`username<AuthUser.username>`
class attribute. Valid value are fetched from the ldap database set with
``settings.CAS_LDAP_*`` settings parameters.
"""
_conn = None
@classmethod
def get_conn(cls):
"""Return a connection object to the ldap database"""
conn = cls._conn
if conn is None or conn.closed:
conn = ldap3.Connection(
settings.CAS_LDAP_SERVER,
settings.CAS_LDAP_USER,
settings.CAS_LDAP_PASSWORD,
auto_bind=True
)
cls._conn = conn
return conn
def __init__(self, username):
if not ldap3:
raise RuntimeError("Please install ldap3 before using the LdapAuthUser backend")
# in case we got deconnected from the database, retry to connect 2 times
for retry_nb in range(3):
try:
conn = self.get_conn()
if conn.search(
settings.CAS_LDAP_BASE_DN,
settings.CAS_LDAP_USER_QUERY % ldap3.utils.conv.escape_bytes(username),
attributes=ldap3.ALL_ATTRIBUTES
) and len(conn.entries) == 1:
user = conn.entries[0].entry_get_attributes_dict()
if user.get(settings.CAS_LDAP_USERNAME_ATTR):
self.user = user
super(LdapAuthUser, self).__init__(user[settings.CAS_LDAP_USERNAME_ATTR][0])
else:
super(LdapAuthUser, self).__init__(username)
else:
super(LdapAuthUser, self).__init__(username)
break
except ldap3.LDAPCommunicationError:
if retry_nb == 2:
raise
def test_password(self, password):
"""
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 and self.user.get(settings.CAS_LDAP_PASSWORD_ATTR):
return check_password(
settings.CAS_LDAP_PASSWORD_CHECK,
password,
self.user[settings.CAS_LDAP_PASSWORD_ATTR][0],
settings.CAS_LDAP_PASSWORD_CHARSET
)
else:
return False
2015-05-17 21:24:41 +00:00
2015-06-12 16:10:52 +00:00
2016-06-26 13:34:26 +00:00
class DjangoAuthUser(AuthUser): # pragma: no cover
2016-07-20 16:28:23 +00:00
"""
A django auth class: authenticate user against django internal users
2016-07-20 16:28:23 +00:00
: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`.
2015-05-17 21:24:41 +00:00
user = None
2015-06-12 16:10:52 +00:00
2015-05-17 21:24:41 +00:00
def __init__(self, username):
2015-12-12 16:26:19 +00:00
User = get_user_model()
2015-05-17 21:24:41 +00:00
try:
self.user = User.objects.get(username=username)
except User.DoesNotExist:
pass
super(DjangoAuthUser, self).__init__(username)
def test_password(self, password):
2016-07-20 16:28:23 +00:00
"""
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
"""
2016-06-26 13:34:26 +00:00
if self.user:
2015-05-17 21:24:41 +00:00
return self.user.check_password(password)
2016-06-26 13:34:26 +00:00
else:
return False
2015-05-17 21:24:41 +00:00
def attributs(self):
2016-07-20 16:28:23 +00:00
"""
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
"""
2016-06-26 13:34:26 +00:00
if self.user:
2015-05-17 21:24:41 +00:00
attr = {}
for field in self.user._meta.fields:
2015-05-27 19:56:39 +00:00
attr[field.attname] = getattr(self.user, field.attname)
2015-05-17 21:24:41 +00:00
return attr
2016-06-26 13:34:26 +00:00
else:
return {}
2016-06-17 17:28:49 +00:00
2016-06-17 17:28:49 +00:00
class CASFederateAuth(AuthUser):
2016-07-20 16:28:23 +00:00
"""
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.
2016-06-17 17:28:49 +00:00
user = None
def __init__(self, username):
try:
self.user = FederatedUser.get_from_federated_username(username)
2016-06-17 17:28:49 +00:00
super(CASFederateAuth, self).__init__(
self.user.federated_username
2016-06-17 17:28:49 +00:00
)
except FederatedUser.DoesNotExist:
super(CASFederateAuth, self).__init__(username)
2016-06-17 17:28:49 +00:00
def test_password(self, ticket):
2016-07-20 16:28:23 +00:00
"""
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
"""
2016-06-17 17:28:49 +00:00
if not self.user or not self.user.ticket:
return False
else:
return (
ticket == self.user.ticket and
self.user.last_update >
(timezone.now() - timedelta(seconds=settings.CAS_TICKET_VALIDITY))
)
def attributs(self):
2016-07-20 16:28:23 +00:00
"""
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)
2016-06-17 17:28:49 +00:00
return {}
else:
return self.user.attributs