Skip to content
GitLab
Projects
Groups
Snippets
Help
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
django-cas-server
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Service Desk
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Operations
Operations
Incidents
Environments
Analytics
Analytics
CI / CD
Repository
Value Stream
Wiki
Wiki
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Valentin Samir
django-cas-server
Commits
f1fed48b
Commit
f1fed48b
authored
Oct 07, 2016
by
Valentin Samir
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add ldap bind auth method and CAS_TGT_VALIDITY parameter. Fix #18
parent
e77dbbcd
Pipeline
#603
failed with stage
in 49 minutes and 40 seconds
Changes
12
Pipelines
1
Hide whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
289 additions
and
9 deletions
+289
-9
CHANGELOG.rst
CHANGELOG.rst
+3
-0
README.rst
README.rst
+17
-0
cas_server/admin.py
cas_server/admin.py
+31
-2
cas_server/auth.py
cas_server/auth.py
+51
-2
cas_server/default_settings.py
cas_server/default_settings.py
+4
-0
cas_server/management/commands/cas_clean_sessions.py
cas_server/management/commands/cas_clean_sessions.py
+1
-0
cas_server/migrations/0011_auto_20161007_1258.py
cas_server/migrations/0011_auto_20161007_1258.py
+38
-0
cas_server/models.py
cas_server/models.py
+49
-5
cas_server/tests/auth.py
cas_server/tests/auth.py
+29
-0
cas_server/tests/mixin.py
cas_server/tests/mixin.py
+11
-0
cas_server/tests/test_models.py
cas_server/tests/test_models.py
+54
-0
cas_server/views.py
cas_server/views.py
+1
-0
No files found.
CHANGELOG.rst
View file @
f1fed48b
...
...
@@ -12,6 +12,9 @@ Unreleased
Added
-----
* Add a test for login with missing parameter (username or password or both)
* Add ldap auth using bind method (use the user credentials to bind the the ldap server and let the
server check the credentials)
* Add CAS_TGT_VALIDITY parameter: Max time after with the user MUST reauthenticate.
Fixed
-----
...
...
README.rst
View file @
f1fed48b
...
...
@@ -268,6 +268,11 @@ Authentication settings
which inactive users are logged out. The default is ``1209600`` (2 weeks). You probably should
reduce it to something like ``86400`` seconds (1 day).
* ``CAS_TGT_VALIDITY``: Max time after with the user MUST reauthenticate. Let it to `None` for no
max time.This can be used to force refreshing cached informations only available upon user
authentication like the user attributes in federation mode or with the ldap auth in bind mode.
The default is ``None``.
* ``CAS_PROXY_CA_CERTIFICATE_PATH``: Path to certificate authorities file. Usually on linux
the local CAs are in ``/etc/ssl/certs/ca-certificates.crt``. The default is ``True`` which
tell requests to use its internal certificat authorities. Settings it to ``False`` should
...
...
@@ -416,6 +421,14 @@ Only usefull if you are using the ldap authentication backend:
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.
* ``"bind``, the user credentials are used to bind to the ldap database and retreive the user
attribute. In this mode, the settings ``CAS_LDAP_PASSWORD_ATTR`` and ``CAS_LDAP_PASSWORD_CHARSET``
are ignored, and it is the ldap server that perform password check. The counterpart is that
the user attributes are only available upon user password check and so are cached for later
use. All the other modes directly fetch the user attributes from the database whenever there
are needed. This mean that is you use this mode, they can be some difference between the
attributes in database and the cached ones if changes happend in the database after the user
authentiate. See the parameter ``CAS_TGT_VALIDITY`` to force user to reauthenticate periodically.
The default is ``"ldap"``.
* ``CAS_LDAP_PASSWORD_CHARSET``: Charset the LDAP users passwords was hash with. This is needed to
...
...
@@ -585,6 +598,10 @@ to the provider CAS to authenticate. This provider transmit to ``django-cas-serv
username and attributes. The user is now logged in on ``django-cas-server`` and can use
services using ``django-cas-server`` as CAS.
In federation mode, the user attributes are cached upon user authentication. See the settings
``CAS_TGT_VALIDITY`` to force users to reauthenticate periodically and allow ``django-cas-server``
to refresh cached attributes.
The list of allowed identity providers is defined using the django admin application.
With the development server started, visit http://127.0.0.1:8000/admin/ to add identity providers.
...
...
cas_server/admin.py
View file @
f1fed48b
...
...
@@ -9,10 +9,12 @@
#
# (c) 2015-2016 Valentin Samir
"""module for the admin interface of the app"""
from
.default_settings
import
settings
from
django.contrib
import
admin
from
.models
import
ServiceTicket
,
ProxyTicket
,
ProxyGrantingTicket
,
User
,
ServicePattern
from
.models
import
Username
,
ReplaceAttributName
,
ReplaceAttributValue
,
FilterAttributValue
from
.models
import
FederatedIendityProvider
from
.models
import
FederatedIendityProvider
,
FederatedUser
,
UserAttributes
from
.forms
import
TicketForm
...
...
@@ -167,6 +169,33 @@ class FederatedIendityProviderAdmin(admin.ModelAdmin):
list_display
=
(
'verbose_name'
,
'suffix'
,
'display'
)
admin
.
site
.
register
(
User
,
UserAdmin
)
class
FederatedUserAdmin
(
admin
.
ModelAdmin
):
"""
Bases: :class:`django.contrib.admin.ModelAdmin`
:class:`FederatedUser<cas_server.models.FederatedUser>` in admin
interface
"""
#: Fields to display on a object.
fields
=
(
'username'
,
'provider'
,
'last_update'
)
#: Fields to display on the list of class:`FederatedUserAdmin` objects.
list_display
=
(
'username'
,
'provider'
,
'last_update'
)
class
UserAttributesAdmin
(
admin
.
ModelAdmin
):
"""
Bases: :class:`django.contrib.admin.ModelAdmin`
:class:`UserAttributes<cas_server.models.UserAttributes>` in admin
interface
"""
#: Fields to display on a object.
fields
=
(
'username'
,
'_attributs'
)
admin
.
site
.
register
(
ServicePattern
,
ServicePatternAdmin
)
admin
.
site
.
register
(
FederatedIendityProvider
,
FederatedIendityProviderAdmin
)
if
settings
.
DEBUG
:
# pragma: no branch (we always test with DEBUG True)
admin
.
site
.
register
(
User
,
UserAdmin
)
admin
.
site
.
register
(
FederatedUser
,
FederatedUserAdmin
)
admin
.
site
.
register
(
UserAttributes
,
UserAttributesAdmin
)
cas_server/auth.py
View file @
f1fed48b
...
...
@@ -30,7 +30,7 @@ try: # pragma: no cover
except
ImportError
:
ldap3
=
None
from
.models
import
FederatedUser
from
.models
import
FederatedUser
,
UserAttributes
from
.utils
import
check_password
,
dictfetchall
...
...
@@ -284,6 +284,10 @@ class LdapAuthUser(DBAuthUser): # pragma: no cover
def
__init__
(
self
,
username
):
if
not
ldap3
:
raise
RuntimeError
(
"Please install ldap3 before using the LdapAuthUser backend"
)
if
not
settings
.
CAS_LDAP_BASE_DN
:
raise
ValueError
(
"You must define CAS_LDAP_BASE_DN for using the ldap authentication backend"
)
# in case we got deconnected from the database, retry to connect 2 times
for
retry_nb
in
range
(
3
):
try
:
...
...
@@ -294,6 +298,8 @@ class LdapAuthUser(DBAuthUser): # pragma: no cover
attributes
=
ldap3
.
ALL_ATTRIBUTES
)
and
len
(
conn
.
entries
)
==
1
:
user
=
conn
.
entries
[
0
].
entry_get_attributes_dict
()
# store the user dn
user
[
"dn"
]
=
conn
.
entries
[
0
].
entry_get_dn
()
if
user
.
get
(
settings
.
CAS_LDAP_USERNAME_ATTR
):
self
.
user
=
user
super
(
LdapAuthUser
,
self
).
__init__
(
user
[
settings
.
CAS_LDAP_USERNAME_ATTR
][
0
])
...
...
@@ -315,7 +321,34 @@ class LdapAuthUser(DBAuthUser): # pragma: no cover
correct, ``False`` otherwise.
:rtype: bool
"""
if
self
.
user
and
self
.
user
.
get
(
settings
.
CAS_LDAP_PASSWORD_ATTR
):
if
settings
.
CAS_LDAP_PASSWORD_CHECK
==
"bind"
:
try
:
conn
=
ldap3
.
Connection
(
settings
.
CAS_LDAP_SERVER
,
self
.
user
[
"dn"
],
password
,
auto_bind
=
True
)
try
:
# fetch the user attribute
if
conn
.
search
(
settings
.
CAS_LDAP_BASE_DN
,
settings
.
CAS_LDAP_USER_QUERY
%
ldap3
.
utils
.
conv
.
escape_bytes
(
self
.
username
),
attributes
=
ldap3
.
ALL_ATTRIBUTES
)
and
len
(
conn
.
entries
)
==
1
:
attributes
=
conn
.
entries
[
0
].
entry_get_attributes_dict
()
attributes
[
"dn"
]
=
conn
.
entries
[
0
].
entry_get_dn
()
# cache the attributes locally as we wont have access to the user password
# later.
user
=
UserAttributes
.
objects
.
get_or_create
(
username
=
self
.
username
)[
0
]
user
.
attributs
=
attributes
user
.
save
()
finally
:
conn
.
unbind
()
return
True
except
(
ldap3
.
LDAPBindError
,
ldap3
.
LDAPCommunicationError
):
return
False
elif
self
.
user
and
self
.
user
.
get
(
settings
.
CAS_LDAP_PASSWORD_ATTR
):
return
check_password
(
settings
.
CAS_LDAP_PASSWORD_CHECK
,
password
,
...
...
@@ -325,6 +358,22 @@ class LdapAuthUser(DBAuthUser): # pragma: no cover
else
:
return
False
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
:raises NotImplementedError: if the password check method in `CAS_LDAP_PASSWORD_CHECK`
do not allow to fetch the attributes without the user credentials.
"""
if
settings
.
CAS_LDAP_PASSWORD_CHECK
==
"bind"
:
raise
NotImplementedError
()
else
:
return
super
(
LdapAuthUser
,
self
).
attributs
()
class
DjangoAuthUser
(
AuthUser
):
# pragma: no cover
"""
...
...
cas_server/default_settings.py
View file @
f1fed48b
...
...
@@ -58,6 +58,10 @@ CAS_SLO_MAX_PARALLEL_REQUESTS = 10
CAS_SLO_TIMEOUT
=
5
#: Shared to transmit then using the view :class:`cas_server.views.Auth`
CAS_AUTH_SHARED_SECRET
=
''
#: Max time after with the user MUST reauthenticate. Let it to `None` for no max time.
#: This can be used to force refreshing cached informations only available upon user authentication
#: like the user attributes in federation mode or with the ldap auth in bind mode.
CAS_TGT_VALIDITY
=
None
#: Number of seconds the service tickets and proxy tickets are valid. This is the maximal time
...
...
cas_server/management/commands/cas_clean_sessions.py
View file @
f1fed48b
...
...
@@ -23,4 +23,5 @@ class Command(BaseCommand):
def
handle
(
self
,
*
args
,
**
options
):
models
.
User
.
clean_deleted_sessions
()
models
.
UserAttributes
.
clean_old_entries
()
models
.
NewVersionWarning
.
send_mails
()
cas_server/migrations/0011_auto_20161007_1258.py
0 → 100644
View file @
f1fed48b
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2016-10-07 12:58
from
__future__
import
unicode_literals
from
django.db
import
migrations
,
models
import
django.utils.timezone
class
Migration
(
migrations
.
Migration
):
dependencies
=
[
(
'cas_server'
,
'0010_auto_20160824_2112'
),
]
operations
=
[
migrations
.
CreateModel
(
name
=
'UserAttributes'
,
fields
=
[
(
'id'
,
models
.
AutoField
(
auto_created
=
True
,
primary_key
=
True
,
serialize
=
False
,
verbose_name
=
'ID'
)),
(
'_attributs'
,
models
.
TextField
(
blank
=
True
,
default
=
None
,
null
=
True
)),
(
'username'
,
models
.
CharField
(
max_length
=
155
,
unique
=
True
)),
],
options
=
{
'verbose_name'
:
'User attributes cache'
,
'verbose_name_plural'
:
'User attributes caches'
,
},
),
migrations
.
AlterModelOptions
(
name
=
'federateduser'
,
options
=
{
'verbose_name'
:
'Federated user'
,
'verbose_name_plural'
:
'Federated users'
},
),
migrations
.
AddField
(
model_name
=
'user'
,
name
=
'last_login'
,
field
=
models
.
DateTimeField
(
auto_now_add
=
True
,
default
=
django
.
utils
.
timezone
.
now
),
preserve_default
=
False
,
),
]
cas_server/models.py
View file @
f1fed48b
...
...
@@ -163,6 +163,8 @@ class FederatedUser(JsonAttributes):
"""
class
Meta
:
unique_together
=
(
"username"
,
"provider"
)
verbose_name
=
_
(
"Federated user"
)
verbose_name_plural
=
_
(
"Federated users"
)
#: The user username returned by the CAS backend on successful ticket validation
username
=
models
.
CharField
(
max_length
=
124
)
#: A foreign key to :class:`FederatedIendityProvider`
...
...
@@ -233,6 +235,30 @@ class FederateSLO(models.Model):
federate_slo
.
delete
()
@
python_2_unicode_compatible
class
UserAttributes
(
JsonAttributes
):
"""
Bases: :class:`JsonAttributes`
Local cache of the user attributes, used then needed
"""
class
Meta
:
verbose_name
=
_
(
"User attributes cache"
)
verbose_name_plural
=
_
(
"User attributes caches"
)
#: The username of the user for which we cache attributes
username
=
models
.
CharField
(
max_length
=
155
,
unique
=
True
)
def
__str__
(
self
):
return
self
.
username
@
classmethod
def
clean_old_entries
(
cls
):
"""Remove :class:`UserAttributes` for which no more :class:`User` exists."""
for
user
in
cls
.
objects
.
all
():
if
User
.
objects
.
filter
(
username
=
user
.
username
).
count
()
==
0
:
user
.
delete
()
@
python_2_unicode_compatible
class
User
(
models
.
Model
):
"""
...
...
@@ -250,6 +276,8 @@ class User(models.Model):
username
=
models
.
CharField
(
max_length
=
30
)
#: Last time the authenticated user has do something (auth, fetch ticket, etc…)
date
=
models
.
DateTimeField
(
auto_now
=
True
)
#: last time the user logged
last_login
=
models
.
DateTimeField
(
auto_now_add
=
True
)
def
delete
(
self
,
*
args
,
**
kwargs
):
"""
...
...
@@ -269,9 +297,12 @@ class User(models.Model):
Remove :class:`User` objects inactive since more that
:django:setting:`SESSION_COOKIE_AGE` and send corresponding SingleLogOut requests.
"""
users
=
cls
.
objects
.
filter
(
date__lt
=
(
timezone
.
now
()
-
timedelta
(
seconds
=
settings
.
SESSION_COOKIE_AGE
))
)
filter
=
Q
(
date__lt
=
(
timezone
.
now
()
-
timedelta
(
seconds
=
settings
.
SESSION_COOKIE_AGE
)))
if
settings
.
CAS_TGT_VALIDITY
is
not
None
:
filter
|=
Q
(
last_login__lt
=
(
timezone
.
now
()
-
timedelta
(
seconds
=
settings
.
CAS_TGT_VALIDITY
))
)
users
=
cls
.
objects
.
filter
(
filter
)
for
user
in
users
:
user
.
logout
()
users
.
delete
()
...
...
@@ -288,9 +319,22 @@ class User(models.Model):
def
attributs
(
self
):
"""
Property.
A fresh :class:`dict` for the user attributes, using ``settings.CAS_AUTH_CLASS``
A fresh :class:`dict` for the user attributes, using ``settings.CAS_AUTH_CLASS`` if
possible, and if not, try to fallback to cached attributes (actually only used for ldap
auth class with bind password check mthode).
"""
return
utils
.
import_attr
(
settings
.
CAS_AUTH_CLASS
)(
self
.
username
).
attributs
()
try
:
return
utils
.
import_attr
(
settings
.
CAS_AUTH_CLASS
)(
self
.
username
).
attributs
()
except
NotImplementedError
:
try
:
user
=
UserAttributes
.
objects
.
get
(
username
=
self
.
username
)
attributes
=
user
.
attributs
if
attributes
is
not
None
:
return
attributes
else
:
return
{}
except
UserAttributes
.
DoesNotExist
:
return
{}
def
__str__
(
self
):
return
u
"%s - %s"
%
(
self
.
username
,
self
.
session_key
)
...
...
cas_server/tests/auth.py
0 → 100644
View file @
f1fed48b
# -*- coding: utf-8 -*-
# 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.
#
# (c) 2016 Valentin Samir
from
cas_server
import
auth
class
TestCachedAttributesAuthUser
(
auth
.
TestAuthUser
):
"""
A test authentication class only working for one unique user.
:param unicode username: A username, stored in the :attr:`username<AuthUser.username>`
class attribute. The uniq valid value is ``settings.CAS_TEST_USER``.
"""
def
attributs
(
self
):
"""
The user attributes.
:raises NotImplementedError: as this class do not support fetching user attributes
"""
raise
NotImplementedError
()
cas_server/tests/mixin.py
View file @
f1fed48b
...
...
@@ -185,6 +185,17 @@ class UserModels(object):
).
update
(
date
=
new_date
)
return
client
@
staticmethod
def
tgt_expired_user
(
sec
):
"""return a user logged since sec seconds"""
client
=
get_auth_client
()
new_date
=
timezone
.
now
()
-
timedelta
(
seconds
=
(
sec
))
models
.
User
.
objects
.
filter
(
username
=
settings
.
CAS_TEST_USER
,
session_key
=
client
.
session
.
session_key
).
update
(
last_login
=
new_date
)
return
client
@
staticmethod
def
get_user
(
client
):
"""return the user associated with an authenticated client"""
...
...
cas_server/tests/test_models.py
View file @
f1fed48b
...
...
@@ -114,6 +114,24 @@ class FederateSLOTestCase(TestCase, UserModels):
models
.
FederateSLO
.
objects
.
get
(
username
=
"test1@example.com"
)
@
override_settings
(
CAS_AUTH_CLASS
=
'cas_server.auth.TestAuthUser'
)
class
UserAttributesTestCase
(
TestCase
,
UserModels
):
"""test for the user attributes cache model"""
def
test_clean_old_entries
(
self
):
"""test the clean_old_entries methode"""
client
=
get_auth_client
()
user
=
self
.
get_user
(
client
)
models
.
UserAttributes
.
objects
.
create
(
username
=
settings
.
CAS_TEST_USER
)
# test that attribute cache is removed for non existant users
self
.
assertEqual
(
len
(
models
.
UserAttributes
.
objects
.
all
()),
1
)
models
.
UserAttributes
.
clean_old_entries
()
self
.
assertEqual
(
len
(
models
.
UserAttributes
.
objects
.
all
()),
1
)
user
.
delete
()
models
.
UserAttributes
.
clean_old_entries
()
self
.
assertEqual
(
len
(
models
.
UserAttributes
.
objects
.
all
()),
0
)
@
override_settings
(
CAS_AUTH_CLASS
=
'cas_server.auth.TestAuthUser'
)
class
UserTestCase
(
TestCase
,
UserModels
):
"""tests for the user models"""
...
...
@@ -144,6 +162,24 @@ class UserTestCase(TestCase, UserModels):
# assert the user has being well delete
self
.
assertEqual
(
len
(
models
.
User
.
objects
.
all
()),
0
)
@
override_settings
(
CAS_TGT_VALIDITY
=
3600
)
def
test_clean_old_entries_tgt_expired
(
self
):
"""test clean_old_entiers with CAS_TGT_VALIDITY set"""
# get an authenticated client
client
=
self
.
tgt_expired_user
(
settings
.
CAS_TGT_VALIDITY
+
60
)
# assert the user exists before being cleaned
self
.
assertEqual
(
len
(
models
.
User
.
objects
.
all
()),
1
)
# assert the last lofin date is before the expiry date
self
.
assertTrue
(
self
.
get_user
(
client
).
last_login
<
(
timezone
.
now
()
-
timedelta
(
seconds
=
settings
.
CAS_TGT_VALIDITY
)
)
)
# delete old inactive users
models
.
User
.
clean_old_entries
()
# assert the user has being well delete
self
.
assertEqual
(
len
(
models
.
User
.
objects
.
all
()),
0
)
def
test_clean_deleted_sessions
(
self
):
"""test clean_deleted_sessions"""
# get an authenticated client
...
...
@@ -177,6 +213,24 @@ class UserTestCase(TestCase, UserModels):
self
.
assertFalse
(
models
.
ServiceTicket
.
objects
.
all
())
self
.
assertTrue
(
client2
.
session
.
get
(
"authenticated"
))
@
override_settings
(
CAS_AUTH_CLASS
=
'cas_server.tests.auth.TestCachedAttributesAuthUser'
)
def
test_cached_attributs
(
self
):
"""
Test gettting user attributes from cache for auth method that do not support direct
fetch (link the ldap bind auth methode)
"""
client
=
get_auth_client
()
user
=
self
.
get_user
(
client
)
# if no cache is defined, the attributes are empty
self
.
assertEqual
(
user
.
attributs
,
{})
user_attr
=
models
.
UserAttributes
.
objects
.
create
(
username
=
settings
.
CAS_TEST_USER
)
# if a cache is defined but without atrributes, also empty
self
.
assertEqual
(
user
.
attributs
,
{})
user_attr
.
attributs
=
settings
.
CAS_TEST_ATTRIBUTES
user_attr
.
save
()
# attributes are what is found in the cache
self
.
assertEqual
(
user
.
attributs
,
settings
.
CAS_TEST_ATTRIBUTES
)
@
override_settings
(
CAS_AUTH_CLASS
=
'cas_server.auth.TestAuthUser'
)
class
TicketTestCase
(
TestCase
,
UserModels
,
BaseServicePattern
):
...
...
cas_server/views.py
View file @
f1fed48b
...
...
@@ -506,6 +506,7 @@ class LoginView(View, LogoutMixin):
username
=
self
.
request
.
session
[
'username'
],
session_key
=
self
.
request
.
session
.
session_key
)[
0
]
self
.
user
.
last_login
=
timezone
.
now
()
self
.
user
.
save
()
elif
ret
==
self
.
USER_LOGIN_FAILURE
:
# bad user login
if
settings
.
CAS_FEDERATE
:
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment