Add SqlAuthUser and LdapAuthUser auth classes. Deprecate the usage of SqlAuthUser in favor of SqlAuthUser.

SqlAuthUser use django databases management, and thus is compatible with all SQL databases supported
by django: postgresql, mysql, sqlite3 and oracle.

LdapAuthUser use the full pythonic ldap3 module
This commit is contained in:
Valentin Samir 2016-07-31 16:52:19 +02:00
parent f0922e0300
commit 2298b94f78
4 changed files with 271 additions and 26 deletions

View File

@ -195,6 +195,8 @@ Authentication settings
* ``CAS_AUTH_CLASS``: A dotted path to a class or a class implementing * ``CAS_AUTH_CLASS``: A dotted path to a class or a class implementing
``cas_server.auth.AuthUser``. The default is ``"cas_server.auth.DjangoAuthUser"`` ``cas_server.auth.AuthUser``. The default is ``"cas_server.auth.DjangoAuthUser"``
Available classes bundled with ``django-cas-server`` are listed below in the
`Authentication backend`_ section.
* ``SESSION_COOKIE_AGE``: This is a django settings. Here, it control the delay in seconds after * ``SESSION_COOKIE_AGE``: This is a django settings. Here, it control the delay in seconds after
which inactive users are logged out. The default is ``1209600`` (2 weeks). You probably should which inactive users are logged out. The default is ``1209600`` (2 weeks). You probably should
@ -214,8 +216,8 @@ Authentication settings
Federation settings Federation settings
------------------- -------------------
* ``CAS_FEDERATE``: A boolean for activating the federated mode (see the federate section below). * ``CAS_FEDERATE``: A boolean for activating the federated mode (see the `Federation mode`_
The default is ``False``. section below). The default is ``False``.
* ``CAS_FEDERATE_REMEMBER_TIMEOUT``: Time after witch the cookie use for "remember my identity * ``CAS_FEDERATE_REMEMBER_TIMEOUT``: Time after witch the cookie use for "remember my identity
provider" expire. The default is ``604800``, one week. The cookie is called provider" expire. The default is ``604800``, one week. The cookie is called
``_remember_provider``. ``_remember_provider``.
@ -269,6 +271,7 @@ Tickets miscellaneous settings
Mysql backend settings Mysql backend settings
---------------------- ----------------------
Deprecated, see the Sql backend settings.
Only usefull if you are using the mysql authentication backend: Only usefull if you are using the mysql authentication backend:
* ``CAS_SQL_HOST``: Host for the SQL server. The default is ``"localhost"``. * ``CAS_SQL_HOST``: Host for the SQL server. The default is ``"localhost"``.
@ -295,6 +298,64 @@ Only usefull if you are using the mysql authentication backend:
The default is ``"crypt"``. The default is ``"crypt"``.
Sql backend settings
--------------------
Only usefull if you are using the sql authentication backend. You must add a ``"cas_server"``
database to `settings.DATABASES <https://docs.djangoproject.com/fr/1.9/ref/settings/#std:setting-DATABASES>`__
as defined in the django documentation. It is then the database
use by the sql backend.
* ``CAS_SQL_USER_QUERY``: The query performed upon user authentication.
The username must be in field ``username``, the password in ``password``,
additional fields are used as the user attributes.
The default is ``"SELECT user AS username, pass AS password, users.* FROM users WHERE user = %s"``
* ``CAS_SQL_PASSWORD_CHECK``: The method used to check the user password. Must be one of the following:
* ``"crypt"`` (see <https://en.wikipedia.org/wiki/Crypt_(C)>), the password in the database
should begin this $
* ``"ldap"`` (see https://tools.ietf.org/id/draft-stroeder-hashed-userpassword-values-01.html)
the password in the database must begin with one of {MD5}, {SMD5}, {SHA}, {SSHA}, {SHA256},
{SSHA256}, {SHA384}, {SSHA384}, {SHA512}, {SSHA512}, {CRYPT}.
* ``"hex_HASH_NAME"`` with ``HASH_NAME`` in md5, sha1, sha224, sha256, sha384, sha512.
The hashed password in the database is compare to the hexadecimal digest of the clear
password hashed with the corresponding algorithm.
* ``"plain"``, the password in the database must be in clear.
The default is ``"crypt"``.
* ``CAS_SQL_PASSWORD_CHARSET``: Charset the SQL users passwords was hash with. This is needed to
encode the user sended password before hashing it for comparison. The default is ``"utf-8"``.
Ldap backend settings
---------------------
Only usefull if you are using the ldap authentication backend:
* ``CAS_LDAP_SERVER``: Address of the LDAP server. The default is ``"localhost"``.
* ``CAS_LDAP_USER``: User bind address, for example ``"cn=admin,dc=crans,dc=org"`` for
connecting to the LDAP server.
* ``CAS_LDAP_PASSWORD``: Password for connecting to the LDAP server.
* ``CAS_LDAP_BASE_DN``: LDAP search base DN, for example ``"ou=data,dc=crans,dc=org"``.
* ``CAS_LDAP_USER_QUERY``: Search filter for searching user by username. User inputed usernames are
escaped using ``ldap3.utils.conv.escape_bytes``. The default is ``"(uid=%s)"``
* ``CAS_LDAP_USERNAME_ATTR``: Attribute used for users usernames. The default is ``"uid"``
* ``CAS_LDAP_PASSWORD_ATTR``: Attribute used for users passwords. The default is ``"userPassword"``
* ``CAS_LDAP_PASSWORD_CHECK``: The method used to check the user password. Must be one of the following:
* ``"crypt"`` (see <https://en.wikipedia.org/wiki/Crypt_(C)>), the password in the database
should begin this $
* ``"ldap"`` (see https://tools.ietf.org/id/draft-stroeder-hashed-userpassword-values-01.html)
the password in the database must begin with one of {MD5}, {SMD5}, {SHA}, {SSHA}, {SHA256},
{SSHA256}, {SHA384}, {SSHA384}, {SHA512}, {SSHA512}, {CRYPT}.
* ``"hex_HASH_NAME"`` with ``HASH_NAME`` in md5, sha1, sha224, sha256, sha384, sha512.
The hashed password in the database is compare to the hexadecimal digest of the clear
password hashed with the corresponding algorithm.
* ``"plain"``, the password in the database must be in clear.
The default is ``"ldap"``.
* ``CAS_LDAP_PASSWORD_CHARSET``: Charset the LDAP users passwords was hash with. This is needed to
encode the user sended password before hashing it for comparison. The default is ``"utf-8"``.
Test backend settings Test backend settings
--------------------- ---------------------
Only usefull if you are using the test authentication backend: Only usefull if you are using the test authentication backend:
@ -316,11 +377,17 @@ Authentication backend
for the user are defined by the ``CAS_TEST_*`` settings. for the user are defined by the ``CAS_TEST_*`` settings.
* django backend ``cas_server.auth.DjangoAuthUser``: Users are authenticated against django users system. * django backend ``cas_server.auth.DjangoAuthUser``: Users are authenticated against django users system.
This is the default backend. The returned attributes are the fields available on the user model. This is the default backend. The returned attributes are the fields available on the user model.
* mysql backend ``cas_server.auth.MysqlAuthUser``: see the 'Mysql backend settings' section. * mysql backend ``cas_server.auth.MysqlAuthUser``: Deprecated, use the sql backend instead.
see the `Mysql backend settings`_ section. The returned attributes are those return by sql query
``CAS_SQL_USER_QUERY``.
* sql backend ``cas_server.auth.SqlAuthUser``: see the `Sql backend settings`_ section.
The returned attributes are those return by sql query ``CAS_SQL_USER_QUERY``. The returned attributes are those return by sql query ``CAS_SQL_USER_QUERY``.
* ldap backend ``cas_server.auth.LdapAuthUser``: see the `Ldap backend settings`_ section.
The returned attributes are those of the ldap node returned by the query filter ``CAS_LDAP_USER_QUERY``.
* federated backend ``cas_server.auth.CASFederateAuth``: It is automatically used then ``CAS_FEDERATE`` is ``True``. * federated backend ``cas_server.auth.CASFederateAuth``: It is automatically used then ``CAS_FEDERATE`` is ``True``.
You should not set it manually without setting ``CAS_FEDERATE`` to ``True``. You should not set it manually without setting ``CAS_FEDERATE`` to ``True``.
Logs Logs
==== ====

View File

@ -13,16 +13,25 @@
from django.conf import settings from django.conf import settings
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.utils import timezone from django.utils import timezone
from django.db import connections, DatabaseError
import warnings
from datetime import timedelta from datetime import timedelta
from six.moves import range
try: # pragma: no cover try: # pragma: no cover
import MySQLdb import MySQLdb
import MySQLdb.cursors import MySQLdb.cursors
from utils import check_password
except ImportError: except ImportError:
MySQLdb = None MySQLdb = None
try: # pragma: no cover
import ldap3
except ImportError:
ldap3 = None
from .models import FederatedUser from .models import FederatedUser
from .utils import check_password, dictfetchall
class AuthUser(object): class AuthUser(object):
@ -116,19 +125,46 @@ class TestAuthUser(AuthUser):
return {} return {}
class MysqlAuthUser(AuthUser): # pragma: no cover 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):
""" """
A mysql authentication class: authentication user agains a mysql database 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
"""
DEPRECATED, use :class:`SqlAuthUser` instead.
A mysql authentication class: authenticate user agains a mysql database
:param unicode username: A username, stored in the :attr:`username<AuthUser.username>` :param unicode username: A username, stored in the :attr:`username<AuthUser.username>`
class attribute. Valid value are fetched from the MySQL database set with class attribute. Valid value are fetched from the MySQL database set with
``settings.CAS_SQL_*`` settings parameters using the query ``settings.CAS_SQL_*`` settings parameters using the query
``settings.CAS_SQL_USER_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): def __init__(self, username):
warnings.warn(
(
"MysqlAuthUser authentication class is deprecated: "
"use cas_server.auth.SqlAuthUser instead"
),
UserWarning
)
# see the connect function at # see the connect function at
# http://mysql-python.sourceforge.net/MySQLdb.html#functions-and-attributes # http://mysql-python.sourceforge.net/MySQLdb.html#functions-and-attributes
# for possible mysql config parameters. # for possible mysql config parameters.
@ -169,24 +205,130 @@ class MysqlAuthUser(AuthUser): # pragma: no cover
else: else:
return False return False
def attributs(self):
"""
The user attributes.
:return: a :class:`dict` with the user attributes. Attributes may be :func:`unicode` class SqlAuthUser(DBAuthUser): # pragma: no cover
or :class:`list` of :func:`unicode`. If the user do not exists, the returned """
:class:`dict` is empty. A SQL authentication class: authenticate user agains a SQL database. The SQL database
:rtype: dict 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):
"""
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: if self.user:
return self.user return check_password(
settings.CAS_SQL_PASSWORD_CHECK,
password,
self.user["password"],
settings.CAS_SQL_PASSWORD_CHARSET
)
else: else:
return {} 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
class DjangoAuthUser(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 against django internal users
:param unicode username: A username, stored in the :attr:`username<AuthUser.username>` :param unicode username: A username, stored in the :attr:`username<AuthUser.username>`
class attribute. Valid value are usernames of django internal users. class attribute. Valid value are usernames of django internal users.

View File

@ -112,12 +112,39 @@ CAS_SQL_PASSWORD = ''
CAS_SQL_DBNAME = '' CAS_SQL_DBNAME = ''
#: Database charset. #: Database charset.
CAS_SQL_DBCHARSET = 'utf8' CAS_SQL_DBCHARSET = 'utf8'
#: The query performed upon user authentication. #: The query performed upon user authentication.
CAS_SQL_USER_QUERY = 'SELECT user AS usersame, pass AS password, users.* FROM users WHERE user = %s' CAS_SQL_USER_QUERY = 'SELECT user AS username, pass AS password, users.* FROM users WHERE user = %s'
#: The method used to check the user password. Must be one of ``"crypt"``, ``"ldap"``, #: The method used to check the user password. Must be one of ``"crypt"``, ``"ldap"``,
#: ``"hex_md5"``, ``"hex_sha1"``, ``"hex_sha224"``, ``"hex_sha256"``, ``"hex_sha384"``, #: ``"hex_md5"``, ``"hex_sha1"``, ``"hex_sha224"``, ``"hex_sha256"``, ``"hex_sha384"``,
#: ``"hex_sha512"``, ``"plain"``. #: ``"hex_sha512"``, ``"plain"``.
CAS_SQL_PASSWORD_CHECK = 'crypt' # crypt or plain CAS_SQL_PASSWORD_CHECK = 'crypt'
#: charset the SQL users passwords was hash with
CAS_SQL_PASSWORD_CHARSET = "utf-8"
#: Address of the LDAP server
CAS_LDAP_SERVER = 'localhost'
#: LDAP user bind address, for example ``"cn=admin,dc=crans,dc=org"`` for connecting to the LDAP
#: server.
CAS_LDAP_USER = None
#: LDAP connection password
CAS_LDAP_PASSWORD = None
#: LDAP seach base DN, for example ``"ou=data,dc=crans,dc=org"``.
CAS_LDAP_BASE_DN = None
#: LDAP search filter for searching user by username. User inputed usernames are escaped using
#: :func:`ldap3.utils.conv.escape_bytes`.
CAS_LDAP_USER_QUERY = "(uid=%s)"
#: LDAP attribute used for users usernames
CAS_LDAP_USERNAME_ATTR = "uid"
#: LDAP attribute used for users passwords
CAS_LDAP_PASSWORD_ATTR = "userPassword"
#: The method used to check the user password. Must be one of ``"crypt"``, ``"ldap"``,
#: ``"hex_md5"``, ``"hex_sha1"``, ``"hex_sha224"``, ``"hex_sha256"``, ``"hex_sha384"``,
#: ``"hex_sha512"``, ``"plain"``.
CAS_LDAP_PASSWORD_CHECK = "ldap"
#: charset the LDAP users passwords was hash with
CAS_LDAP_PASSWORD_CHARSET = "utf-8"
#: Username of the test user. #: Username of the test user.

View File

@ -582,7 +582,7 @@ def check_password(method, password, hashed_password, charset):
:param hashed_password: The hashed password as stored in the database :param hashed_password: The hashed password as stored in the database
:type hashed_password: :obj:`str` or :obj:`unicode` :type hashed_password: :obj:`str` or :obj:`unicode`
:param str charset: The used char encoding (also used internally, so it must be valid for :param str charset: The used char encoding (also used internally, so it must be valid for
the charset used by ``password`` even if it is inputed as an :obj:`unicode`) the charset used by ``password`` when it was initially )
:return: True if ``password`` match ``hashed_password`` using ``method``, :return: True if ``password`` match ``hashed_password`` using ``method``,
``False`` otherwise ``False`` otherwise
:rtype: bool :rtype: bool
@ -670,3 +670,12 @@ def last_version():
"Unable to fetch %s: %s" % (settings.CAS_NEW_VERSION_JSON_URL, error) "Unable to fetch %s: %s" % (settings.CAS_NEW_VERSION_JSON_URL, error)
) )
last_version._cache = (time.time(), version, False) last_version._cache = (time.time(), version, False)
def dictfetchall(cursor):
"Return all rows from a django cursor as a dict"
columns = [col[0] for col in cursor.description]
return [
dict(zip(columns, row))
for row in cursor.fetchall()
]