Skip to content

Commit 7f4ff24

Browse files
author
Matthew Fisher
committed
Merge pull request #2136 from bacongobbler/remove_csrf_token
ref(controller): remove csrf token; switch to token-based authn
2 parents ecf7c72 + 65a78c3 commit 7f4ff24

18 files changed

Lines changed: 634 additions & 505 deletions

client/deis.py

Lines changed: 17 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@
4141
from __future__ import print_function
4242
from collections import namedtuple
4343
from collections import OrderedDict
44-
from cookielib import MozillaCookieJar
4544
from datetime import datetime
4645
from getpass import getpass
4746
from itertools import cycle
@@ -82,21 +81,14 @@ class Session(requests.Session):
8281
def __init__(self):
8382
super(Session, self).__init__()
8483
self.trust_env = False
85-
cookie_file = os.path.expanduser('~/.deis/cookies.txt')
86-
cookie_dir = os.path.dirname(cookie_file)
87-
self.cookies = MozillaCookieJar(cookie_file)
84+
config_dir = os.path.expanduser('~/.deis')
8885
self.proxies = {
8986
"http": os.getenv("http_proxy"),
9087
"https": os.getenv("https_proxy")
9188
}
9289
# Create the $HOME/.deis dir if it doesn't exist
93-
if not os.path.isdir(cookie_dir):
94-
os.mkdir(cookie_dir, 0700)
95-
# Load existing cookies if the cookies.txt exists
96-
if os.path.isfile(cookie_file):
97-
self.cookies.load()
98-
self.cookies.clear_expired_cookies()
99-
self.cookies.save()
90+
if not os.path.isdir(config_dir):
91+
os.mkdir(config_dir, 0700)
10092

10193
@property
10294
def app(self):
@@ -152,25 +144,14 @@ def _get_name_from_git_remote(self, git_root):
152144

153145
def request(self, *args, **kwargs):
154146
"""
155-
Issue an HTTP request with proper cookie handling including
156-
`Django CSRF tokens <https://docs.djangoproject.com/en/dev/ref/contrib/csrf/>`
147+
Issue an HTTP request
157148
"""
158-
for cookie in self.cookies:
159-
if cookie.name == 'csrftoken':
160-
if 'headers' in kwargs:
161-
kwargs['headers']['X-CSRFToken'] = cookie.value
162-
else:
163-
kwargs['headers'] = {'X-CSRFToken': cookie.value}
164-
break
165149
url = args[1]
166150
if 'headers' in kwargs:
167151
kwargs['headers']['Referer'] = url
168152
else:
169153
kwargs['headers'] = {'Referer': url}
170154
response = super(Session, self).request(*args, **kwargs)
171-
self.cookies.save()
172-
# set ~/.deis/cookies.txt readable only by its owner
173-
os.chmod(self.cookies.filename, 0600)
174155
return response
175156

176157

@@ -395,16 +376,18 @@ def _dispatch(self, method, path, body=None, **kwargs):
395376
"""
396377
Dispatch an API request to the active Deis controller
397378
"""
398-
headers = {
399-
'content-type': 'application/json',
400-
'X-Deis-Version': __version__.rsplit('.', 1)[0],
401-
}
402379
func = getattr(self._session, method.lower())
403380
controller = self._settings.get('controller')
404-
if not controller:
381+
token = self._settings.get('token')
382+
if not token:
405383
raise EnvironmentError(
406-
'No active controller. Use `deis login` or `deis register` to get started.')
384+
'Could not find token. Use `deis login` or `deis register` to get started.')
407385
url = urlparse.urljoin(controller, path, **kwargs)
386+
headers = {
387+
'content-type': 'application/json',
388+
'X-Deis-Version': __version__.rsplit('.', 1)[0],
389+
'Authorization': 'token {}'.format(token)
390+
}
408391
response = func(url, data=body, headers=headers)
409392
return response
410393

@@ -718,9 +701,6 @@ def auth_register(self, args):
718701
email = raw_input('email: ')
719702
url = urlparse.urljoin(controller, '/api/auth/register')
720703
payload = {'username': username, 'password': password, 'email': email}
721-
# Clear any existing cookies
722-
self._session.cookies.clear()
723-
self._session.cookies.save()
724704
response = self._session.post(url, data=payload, allow_redirects=False)
725705
if response.status_code == requests.codes.created: # @UndefinedVariable
726706
self._settings['controller'] = controller
@@ -751,9 +731,8 @@ def auth_cancel(self, args):
751731
confirm = raw_input("Cancel account \"{}\" at {}? (y/n) ".format(username, controller))
752732
if confirm == 'y':
753733
self._dispatch('delete', '/api/auth/cancel')
754-
self._session.cookies.clear()
755-
self._session.cookies.save()
756734
self._settings['controller'] = None
735+
self._settings['token'] = None
757736
self._settings.save()
758737
self._logger.info('Account cancelled')
759738
else:
@@ -787,16 +766,13 @@ def auth_login(self, args):
787766
password = getpass('password: ')
788767
url = urlparse.urljoin(controller, '/api/auth/login/')
789768
payload = {'username': username, 'password': password}
790-
# clear any existing cookies
791-
self._session.cookies.clear()
792-
self._session.cookies.save()
793-
# prime cookies for login
794-
self._session.get(url, headers=headers)
795769
# post credentials to the login URL
796770
response = self._session.post(url, data=payload, allow_redirects=False)
797-
if response.status_code == requests.codes.found: # @UndefinedVariable
771+
if response.status_code == requests.codes.ok: # @UndefinedVariable
772+
# retrieve and save the API token for future requests
798773
self._settings['controller'] = controller
799774
self._settings['username'] = username
775+
self._settings['token'] = response.json()['token']
800776
self._settings.save()
801777
self._logger.info("Logged in as {}".format(username))
802778
return username
@@ -809,16 +785,9 @@ def auth_logout(self, args):
809785
810786
Usage: deis auth:logout
811787
"""
812-
controller = self._settings.get('controller')
813-
if controller:
814-
try:
815-
self._dispatch('get', '/api/auth/logout/')
816-
except requests.exceptions.ConnectionError:
817-
pass
818-
self._session.cookies.clear()
819-
self._session.cookies.save()
820788
self._settings['controller'] = None
821789
self._settings['username'] = None
790+
self._settings['token'] = None
822791
self._settings.save()
823792
self._logger.info('Logged out')
824793

controller/api/models.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,19 @@
1515
import threading
1616

1717
from django.conf import settings
18-
from django.contrib.auth.models import User
18+
from django.contrib.auth import get_user_model
1919
from django.core.exceptions import ValidationError
2020
from django.db import models
2121
from django.db.models import Max
22-
from django.db.models.signals import post_delete
23-
from django.db.models.signals import post_save
22+
from django.db.models.signals import post_delete, post_save
23+
from django.dispatch import receiver
2424
from django.utils.encoding import python_2_unicode_compatible
2525
from django_fsm import FSMField, transition
2626
from django_fsm.signals import post_transition
2727
from docker.utils import utils
2828
from json_field.fields import JSONField
2929
import requests
30+
from rest_framework.authtoken.models import Token
3031

3132
from api import fields
3233
from registry import publish_release
@@ -831,6 +832,13 @@ def _etcd_purge_domains(**kwargs):
831832
post_delete.connect(_log_domain_removed, sender=Domain, dispatch_uid='api.models.log')
832833

833834

835+
# automatically generate a new token on creation
836+
@receiver(post_save, sender=get_user_model())
837+
def create_auth_token(sender, instance=None, created=False, **kwargs):
838+
if created:
839+
Token.objects.create(user=instance)
840+
841+
834842
# save FSM transitions as they happen
835843
def _save_transition(**kwargs):
836844
kwargs['instance'].save()
@@ -852,7 +860,7 @@ def _save_transition(**kwargs):
852860
if _etcd_client:
853861
post_save.connect(_etcd_publish_key, sender=Key, dispatch_uid='api.models')
854862
post_delete.connect(_etcd_purge_key, sender=Key, dispatch_uid='api.models')
855-
post_delete.connect(_etcd_purge_user, sender=User, dispatch_uid='api.models')
863+
post_delete.connect(_etcd_purge_user, sender=get_user_model(), dispatch_uid='api.models')
856864
post_save.connect(_etcd_publish_domains, sender=Domain, dispatch_uid='api.models')
857865
post_delete.connect(_etcd_purge_domains, sender=Domain, dispatch_uid='api.models')
858866
post_save.connect(_etcd_create_app, sender=App, dispatch_uid='api.models')

controller/api/tests/test_api_middleware.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
"""
66
from __future__ import unicode_literals
77

8+
from django.contrib.auth.models import User
89
from django.test import TestCase
10+
from rest_framework.authtoken.models import Token
911

1012
from deis import __version__
1113

@@ -17,16 +19,17 @@ class APIMiddlewareTest(TestCase):
1719
fixtures = ['tests.json']
1820

1921
def setUp(self):
20-
self.assertTrue(
21-
self.client.login(username='autotest', password='password'))
22+
self.user = User.objects.get(username='autotest')
23+
self.token = Token.objects.get(user=self.user).key
2224

2325
def test_x_deis_version_header_good(self):
2426
"""
2527
Test that when the version header is sent, the request is accepted.
2628
"""
2729
response = self.client.get(
2830
'/api/apps',
29-
HTTP_X_DEIS_VERSION=__version__.rsplit('.', 1)[0]
31+
HTTP_X_DEIS_VERSION=__version__.rsplit('.', 1)[0],
32+
HTTP_AUTHORIZATION='token {}'.format(self.token),
3033
)
3134
self.assertEqual(response.status_code, 200)
3235

@@ -36,13 +39,15 @@ def test_x_deis_version_header_bad(self):
3639
"""
3740
response = self.client.get(
3841
'/api/apps',
39-
HTTP_X_DEIS_VERSION='1234.5678'
42+
HTTP_X_DEIS_VERSION='1234.5678',
43+
HTTP_AUTHORIZATION='token {}'.format(self.token),
4044
)
4145
self.assertEqual(response.status_code, 405)
4246

4347
def test_x_deis_version_header_not_present(self):
4448
"""
4549
Test that when the version header is not present, the request is accepted.
4650
"""
47-
response = self.client.get('/api/apps')
51+
response = self.client.get('/api/apps',
52+
HTTP_AUTHORIZATION='token {}'.format(self.token))
4853
self.assertEqual(response.status_code, 200)

0 commit comments

Comments
 (0)