Skip to content

Commit 70cd4c6

Browse files
author
Matthew Fisher
committed
Merge pull request #3713 from Joshua-Anderson/reissue-token
feat(controller): Add endpoint to regenerate tokens
2 parents 467a07d + 46829d3 commit 70cd4c6

11 files changed

Lines changed: 1522 additions & 30 deletions

File tree

client/deis.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@
8383
__version__ = '1.8.0-dev'
8484

8585
# what version of the API is this client compatible with?
86-
__api_version__ = '1.4'
86+
__api_version__ = '1.5'
8787

8888

8989
locale.setlocale(locale.LC_ALL, '')
@@ -737,6 +737,7 @@ def auth(self, args):
737737
auth:passwd change the password for the current user
738738
auth:whoami display the current user
739739
auth:cancel remove the current user account
740+
auth:regenerate regenerate user tokens
740741
741742
Use `deis help [command]` to learn more.
742743
"""
@@ -963,6 +964,36 @@ def auth_whoami(self, args):
963964
self._logger.info(
964965
'Not logged in. Use `deis login` or `deis register` to get started.')
965966

967+
def auth_regenerate(self, args):
968+
"""
969+
Regenerates auth token, defaults to regenerating token for the current user.
970+
971+
Usage: deis auth:regenerate [options]
972+
973+
Options:
974+
-u --username=<username>
975+
specify user to regenerate. Requires admin privilages.
976+
--all
977+
regenerate token for every user. Requires admin privilages.
978+
"""
979+
payload = {}
980+
981+
if args.get('--all'):
982+
payload = {'all': True}
983+
elif args.get('--username'):
984+
payload = {'username': args.get('--username')}
985+
986+
response = self._dispatch('post', '/v1/auth/tokens/', json.dumps(payload))
987+
988+
if response.status_code == requests.codes.ok:
989+
if '--username' not in args or '--all' not in args:
990+
self._settings['token'] = response.json()['token']
991+
self._settings.save()
992+
self._logger.info('Token regenerated.')
993+
else:
994+
self._logger.info("Token regeneration failed: {}".format(response.text))
995+
sys.exit(1)
996+
966997
def builds(self, args):
967998
"""
968999
Valid commands for builds:

controller/api/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
The **api** Django app presents a RESTful web API for interacting with the **deis** system.
33
"""
44

5-
__version__ = '1.4.0'
5+
__version__ = '1.5.0'

controller/api/permissions.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,3 +129,18 @@ def has_permission(self, request, view):
129129
if not auth_header:
130130
return False
131131
return auth_header == settings.BUILDER_KEY
132+
133+
134+
class CanRegenerateToken(permissions.BasePermission):
135+
"""
136+
Checks if a user can regenerate a token
137+
"""
138+
139+
def has_permission(self, request, view):
140+
"""
141+
Return `True` if permission is granted, `False` otherwise.
142+
"""
143+
if 'username' in request.data or 'all' in request.data:
144+
return request.user.is_superuser
145+
else:
146+
return True

controller/api/tests/test_auth.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,3 +278,33 @@ def test_change_user_passwd(self):
278278
response = self.client.post(url, json.dumps(submit), content_type='application/json',
279279
HTTP_AUTHORIZATION='token {}'.format(self.user1_token))
280280
self.assertEqual(response.status_code, 200)
281+
282+
def test_regenerate(self):
283+
""" Test that token regeneration works"""
284+
285+
url = '/v1/auth/tokens/'
286+
287+
response = self.client.post(url, '{}', content_type='application/json',
288+
HTTP_AUTHORIZATION='token {}'.format(self.admin_token))
289+
290+
self.assertEqual(response.status_code, 200)
291+
self.assertNotEqual(response.data['token'], self.admin_token)
292+
293+
self.admin_token = Token.objects.get(user=self.admin)
294+
295+
response = self.client.post(url, '{"username" : "autotest2"}',
296+
content_type='application/json',
297+
HTTP_AUTHORIZATION='token {}'.format(self.admin_token))
298+
299+
self.assertEqual(response.status_code, 200)
300+
self.assertNotEqual(response.data['token'], self.user1_token)
301+
302+
response = self.client.post(url, '{"all" : "true"}',
303+
content_type='application/json',
304+
HTTP_AUTHORIZATION='token {}'.format(self.admin_token))
305+
self.assertEqual(response.status_code, 200)
306+
307+
response = self.client.post(url, '{}', content_type='application/json',
308+
HTTP_AUTHORIZATION='token {}'.format(self.admin_token))
309+
310+
self.assertEqual(response.status_code, 401)

controller/api/urls.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@
8787
views.UserManagementViewSet.as_view({'post': 'passwd'})),
8888
url(r'^auth/login/',
8989
'rest_framework.authtoken.views.obtain_auth_token'),
90+
url(r'^auth/tokens/',
91+
views.TokenManagementViewSet.as_view({'post': 'regenerate'})),
9092
# admin sharing
9193
url(r'^admin/perms/(?P<username>[-_\w]+)/?',
9294
views.AdminPermsViewSet.as_view({'delete': 'destroy'})),

controller/api/views.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from rest_framework.permissions import IsAuthenticated
1414
from rest_framework.response import Response
1515
from rest_framework.viewsets import GenericViewSet
16+
from rest_framework.authtoken.models import Token
1617

1718
from api import authentication, models, permissions, serializers, viewsets
1819

@@ -51,6 +52,39 @@ def passwd(self, request, **kwargs):
5152
return Response({'status': 'password set'})
5253

5354

55+
class TokenManagementViewSet(GenericViewSet,
56+
mixins.DestroyModelMixin):
57+
serializer_class = serializers.UserSerializer
58+
permission_classes = [permissions.CanRegenerateToken]
59+
60+
def get_queryset(self):
61+
return User.objects.filter(pk=self.request.user.pk)
62+
63+
def get_object(self):
64+
return self.get_queryset()[0]
65+
66+
def regenerate(self, request, **kwargs):
67+
obj = self.get_object()
68+
69+
if 'all' in request.data:
70+
for user in User.objects.all():
71+
if not user.is_anonymous():
72+
token = Token.objects.get(user=user)
73+
token.delete()
74+
Token.objects.create(user=user)
75+
return Response("")
76+
77+
if 'username' in request.data:
78+
obj = get_object_or_404(User,
79+
username=request.data['username'])
80+
self.check_object_permissions(self.request, obj)
81+
82+
token = Token.objects.get(user=obj)
83+
token.delete()
84+
token = Token.objects.create(user=obj)
85+
return Response({'token': token.key})
86+
87+
5488
class BaseDeisViewSet(viewsets.OwnerViewSet):
5589
"""
5690
A generic ViewSet for objects related to Deis.

docs/managing_deis/operational_tasks.rst

Lines changed: 12 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -52,28 +52,22 @@ Re-issuing User Authentication Tokens
5252
The controller API uses a simple token-based HTTP Authentication scheme. Token authentication is
5353
appropriate for client-server setups, such as native desktop and mobile clients. Each user of the
5454
platform is issued a token the first time that they sign up on the platform. If this token is
55-
compromised, you'll need to manually intervene to re-issue a new authentication token for the user.
56-
To do this, SSH into the node running the controller and drop into a Django shell:
55+
compromised, it will need to be regenerated.
56+
57+
A user can regenerate their own token like this:
5758

5859
.. code-block:: console
5960
60-
$ fleetctl ssh deis-controller
61-
$ docker exec -it deis-controller python manage.py shell
62-
>>>
61+
$ deis auth:regenerate
6362
64-
At this point, let's re-issue an auth token for this user. Let's assume that the name for the user
65-
is Bob (poor Bob):
63+
An administrator can also regenerate the token of another user like this:
6664

6765
.. code-block:: console
6866
69-
>>> from django.contrib.auth.models import User
70-
>>> from rest_framework.authtoken.models import Token
71-
>>> bob = User.objects.get(username='bob')
72-
>>> token = Token.objects.get(user=bob)
73-
>>> token.delete()
74-
>>> exit()
67+
$ deis auth:regenerate -u test-user
68+
7569
76-
At this point, Bob will no longer be able to authenticate against the controller with his auth
70+
At this point, the user will no longer be able to authenticate against the controller with his auth
7771
token:
7872

7973
.. code-block:: console
@@ -83,14 +77,10 @@ token:
8377
Detail:
8478
Invalid token
8579
86-
For Bob to be able to use the API again, he will have to authenticate against the controller to be
87-
re-issued a new token:
80+
They will need to log back in to use their new auth token.
81+
82+
If there is a cluster wide security breach, an administrator can regenerate everybody's auth token like this:
8883

8984
.. code-block:: console
9085
91-
$ deis login http://deis.example.com
92-
username: bob
93-
password:
94-
Logged in as bob
95-
$ deis apps
96-
=== Apps
86+
$ deis auth:regenerate --all=true

docs/reference/api-v1.4.rst

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
:title: Controller API v1.4
22
:description: The v1.4 REST API for Deis' Controller
33

4-
.. _controller_api_v1:
5-
64
Controller API v1.4
75
===================
86

0 commit comments

Comments
 (0)