Skip to content

Commit ecc0b1f

Browse files
committed
Merge pull request #47 from kstateome/develop
Release 1.2.0
2 parents 1ca4443 + b754437 commit ecc0b1f

19 files changed

+792
-33
lines changed

.travis.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ env:
99
- DJANGO_VERSION=Django==1.5
1010
- DJANGO_VERSION=Django==1.6
1111
- DJANGO_VERSION=Django==1.7
12+
- DJANGO_VERSION=Django==1.8
1213

1314
# command to install dependencies
1415
install:

CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1+
### 1.2.0
2+
3+
- Allow opt out of time delay caused by fetching PGT tickets
4+
- Add support for gateway not returning a response
5+
- Allow forcing service URL over HTTPS (https://github.com/kstateome/django-cas/pull/48)
6+
- Allow user creation on first login to be optional (https://github.com/kstateome/django-cas/pull/49)
7+
18
### 1.1.1
29

310
- Add a few logging statements
4-
- Add official change log.
11+
- Add official change log.

CONTRIBUTORS

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
epicserve
22
rlmv
3-
bryankaplan
3+
bryankaplan
4+
cordmata

README.md

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
CAS client for Django. This library requires Django 1.5 or above, and Python 2.6, 2.7, 3.4
44

5-
Current version: 1.1.1
5+
Current version: 1.2.0
66

77
This is [K-State's fork](https://github.com/kstateome/django-cas) of [the original](https://bitbucket.org/cpcc/django-cas/overview) and includes [several additional features](https://github.com/kstateome/django-cas/#additional-features) as well as features merged from
88

@@ -14,9 +14,9 @@ This is [K-State's fork](https://github.com/kstateome/django-cas) of [the or
1414

1515
This project is registered on PyPi as django-cas-client. To install::
1616

17-
pip install django-cas-client==1.1.1
18-
19-
17+
pip install django-cas-client==1.2.0
18+
19+
2020
### Add to URLs
2121

2222
Add the login and logout patterns to your main URLS conf.
@@ -34,9 +34,9 @@ Set your CAS server URL
3434
Add cas to middleware classes
3535

3636
'cas.middleware.CASMiddleware',
37-
3837

39-
### Add authentication backends
38+
39+
### Add authentication backends
4040

4141
AUTHENTICATION_BACKENDS = (
4242
'django.contrib.auth.backends.ModelBackend',
@@ -138,8 +138,21 @@ Then, add the ``gateway`` decorator to a view:
138138
To show a custom forbidden page, set ``CAS_CUSTOM_FORBIDDEN`` to a ``path.to.some_view``. Otherwise,
139139
a generic ``HttpResponseForbidden`` will be returned.
140140

141+
## Require SSL Login
142+
143+
To force the service url to always target HTTPS, set ``CAS_FORCE_SSL_SERVICE_URL`` to ``True``.
144+
145+
## Automatically Create Users on First Login
146+
147+
By default, a stub user record will be created on the first successful CAS authentication
148+
using the username in the response. If this behavior is not desired set
149+
``CAS_AUTO_CREATE_USER`` to ``Flase``.
141150

142151
## Proxy Tickets
143152

144153
This fork also includes
145154
[Edmund Crewe's proxy ticket patch](http://code.google.com/r/edmundcrewe-proxypatch/source/browse/django-cas-proxy.patch).
155+
156+
You can opt out of the time delay sometimes caused by proxy ticket validation by setting:
157+
158+
CAS_PGT_FETCH_WAIT = False

cas/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
'CAS_PROXY_CALLBACK': None,
1818
'CAS_RESPONSE_CALLBACKS': None,
1919
'CAS_CUSTOM_FORBIDDEN': None,
20+
'CAS_PGT_FETCH_WAIT': True,
21+
'CAS_FORCE_SSL_SERVICE_URL': False,
22+
'CAS_AUTO_CREATE_USER': True,
2023
}
2124

2225
for key, value in _DEFAULTS.items():

cas/backends.py

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -112,9 +112,14 @@ def _internal_verify_cas(ticket, service, suffix):
112112
pgtIou.delete()
113113
except Tgt.DoesNotExist:
114114
Tgt.objects.create(username=username, tgt=pgtIou.tgt)
115+
logger.info('Creating TGT ticket for {user}'.format(
116+
user=username
117+
))
115118
pgtIou.delete()
116-
except Exception:
117-
logger.error('Failed to do proxy authentication.')
119+
except Exception as e:
120+
logger.warning('Failed to do proxy authentication. {message}'.format(
121+
message=e
122+
))
118123

119124
else:
120125
failure = document.getElementsByTagName('cas:authenticationFailure')
@@ -123,7 +128,9 @@ def _internal_verify_cas(ticket, service, suffix):
123128
failure[0].firstChild.nodeValue)
124129

125130
except Exception as e:
126-
logger.error('Failed to verify CAS authentication: %s', e)
131+
logger.error('Failed to verify CAS authentication: {message}'.format(
132+
message=e
133+
))
127134

128135
finally:
129136
page.close()
@@ -181,17 +188,28 @@ def _get_pgtiou(pgt):
181188
ticket is retried for up to 5 seconds. This should be handled some
182189
better way.
183190
191+
Users can opt out of this waiting period by setting CAS_PGT_FETCH_WAIT = False
192+
184193
:param: pgt
185194
186195
"""
196+
187197
pgtIou = None
188198
retries_left = 5
199+
200+
if not settings.CAS_PGT_FETCH_WAIT:
201+
retries_left = 1
202+
189203
while not pgtIou and retries_left:
190204
try:
191205
return PgtIOU.objects.get(tgt=pgt)
192206
except PgtIOU.DoesNotExist:
193-
time.sleep(1)
207+
if settings.CAS_PGT_FETCH_WAIT:
208+
time.sleep(1)
194209
retries_left -= 1
210+
logger.info('Did not fetch ticket, trying again. {tries} tries left.'.format(
211+
tries=retries_left
212+
))
195213
raise CasTicketException("Could not find pgtIou for pgt %s" % pgt)
196214

197215

@@ -219,8 +237,11 @@ def authenticate(self, ticket, service):
219237
user = User.objects.get(username__iexact=username)
220238
except User.DoesNotExist:
221239
# user will have an "unusable" password
222-
user = User.objects.create_user(username, '')
223-
user.save()
240+
if settings.CAS_AUTO_CREATE_USER:
241+
user = User.objects.create_user(username, '')
242+
user.save()
243+
else:
244+
user = None
224245
return user
225246

226247
def get_user(self, user_id):

cas/decorators.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,22 +66,27 @@ def wrapped_f(*args):
6666
request = args[0]
6767

6868
if request.user.is_authenticated():
69-
#Is Authed, fine
69+
# Is Authed, fine
7070
pass
7171
else:
7272
path_with_params = request.path + '?' + urlencode(request.GET.copy())
7373
if request.GET.get('ticket'):
74-
#Not Authed, but have a ticket!
75-
#Try to authenticate
76-
return login(request, path_with_params, False, True)
74+
# Not Authed, but have a ticket!
75+
# Try to authenticate
76+
response = login(request, path_with_params, False, True)
77+
if isinstance(response, HttpResponseRedirect):
78+
# For certain instances where a forbidden occurs, we need to pass instead of return a response.
79+
return response
7780
else:
7881
#Not Authed, but no ticket
7982
gatewayed = request.GET.get('gatewayed')
8083
if gatewayed == 'true':
8184
pass
8285
else:
83-
#Not Authed, try to authenticate
84-
return login(request, path_with_params, False, True)
86+
# Not Authed, try to authenticate
87+
response = login(request, path_with_params, False, True)
88+
if isinstance(response, HttpResponseRedirect):
89+
return response
8590

8691
return func(*args)
8792
return wrapped_f

cas/tests/test_backend.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,18 @@ def setUp(self):
1313
def test_get_user(self):
1414
backend = CASBackend()
1515

16-
self.assertEqual(backend.get_user(self.user.pk), self.user)
16+
self.assertEqual(backend.get_user(self.user.pk), self.user)
17+
18+
@mock.patch('cas.backends._verify')
19+
def test_user_auto_create(self, verify):
20+
username = 'faker'
21+
verify.return_value = username
22+
backend = CASBackend()
23+
24+
with self.settings(CAS_AUTO_CREATE_USER=False):
25+
user = backend.authenticate('fake', 'fake')
26+
self.assertIsNone(user)
27+
28+
with self.settings(CAS_AUTO_CREATE_USER=True):
29+
user = backend.authenticate('fake', 'fake')
30+
self.assertEquals(user.username, username)

cas/tests/test_views.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from django.test import TestCase, RequestFactory
2+
from django.test.utils import override_settings
23

34
from cas.views import _redirect_url, _login_url, _logout_url, _service_url
45

@@ -24,6 +25,10 @@ def setUp(self):
2425
def test_service_url(self):
2526
self.assertEqual(_service_url(self.request), 'http://signin.k-state.edu/')
2627

28+
@override_settings(CAS_FORCE_SSL_SERVICE_URL=True)
29+
def test_service_url_forced_ssl(self):
30+
self.assertEqual(_service_url(self.request), 'https://signin.k-state.edu/')
31+
2732
def test_redirect_url(self):
2833
self.assertEqual(_redirect_url(self.request), '/')
2934

@@ -38,4 +43,4 @@ def test_login_url(self):
3843
'http://signin.cas.com/login?service=http%3A%2F%2Flocalhost%3A8000%2Faccounts%2Flogin%2F')
3944

4045
def test_logout_url(self):
41-
self.assertEqual(_logout_url(self.request), 'http://signin.cas.com/logout')
46+
self.assertEqual(_logout_url(self.request), 'http://signin.cas.com/logout')

cas/views.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,10 @@ def _service_url(request, redirect_to=None, gateway=False):
3535
3636
"""
3737

38-
protocol = ('http://', 'https://')[request.is_secure()]
38+
if settings.CAS_FORCE_SSL_SERVICE_URL:
39+
protocol = 'https://'
40+
else:
41+
protocol = ('http://', 'https://')[request.is_secure()]
3942
host = request.get_host()
4043
service = protocol + host + request.path
4144
if redirect_to:
@@ -188,6 +191,11 @@ def login(request, next_page=None, required=False, gateway=False):
188191
else:
189192
logger.warning('User has a valid ticket but not a valid session')
190193
# Has ticket, not session
194+
195+
if gateway:
196+
# Gatewayed responses should nto redirect.
197+
return False
198+
191199
if getattr(settings, 'CAS_CUSTOM_FORBIDDEN'):
192200
return HttpResponseRedirect(reverse(settings.CAS_CUSTOM_FORBIDDEN) + "?" + request.META['QUERY_STRING'])
193201
else:
@@ -233,13 +241,17 @@ def proxy_callback(request):
233241
tgt = request.GET.get('pgtId')
234242

235243
if not (pgtIou and tgt):
244+
logger.info('No pgtIou or tgt found in request.GET')
236245
return HttpResponse('No pgtIOO', content_type="text/plain")
237246

238247
try:
239248
PgtIOU.objects.create(tgt=tgt, pgtIou=pgtIou, created=datetime.datetime.now())
240-
request.session['pgt-TICKET'] = ticket
241-
return HttpResponse('PGT ticket is: %s' % str(ticket, content_type="text/plain"))
242-
except:
243-
logger.warning('PGT storage failed.')
244-
return HttpResponse('PGT storage failed for %s' % str(request.GET), content_type="text/plain")
249+
request.session['pgt-TICKET'] = pgtIou
250+
return HttpResponse('PGT ticket is: {ticket}'.format(ticket=pgtIou), content_type="text/plain")
251+
except Exception as e:
252+
logger.warning('PGT storage failed. {message}'.format(
253+
message=e
254+
))
255+
return HttpResponse('PGT storage failed for {request}'.format(request=str(request.GET)),
256+
content_type="text/plain")
245257

0 commit comments

Comments
 (0)