Skip to content

Commit

Permalink
feat(controller): Add token regeneration endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
Joshua-Anderson authored and Joshua Anderson committed Jun 9, 2015
1 parent 1cc6021 commit 46829d3
Show file tree
Hide file tree
Showing 11 changed files with 1,522 additions and 30 deletions.
33 changes: 32 additions & 1 deletion client/deis.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@
__version__ = '1.8.0-dev'

# what version of the API is this client compatible with?
__api_version__ = '1.4'
__api_version__ = '1.5'


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

def auth_regenerate(self, args):
"""
Regenerates auth token, defaults to regenerating token for the current user.
Usage: deis auth:regenerate [options]
Options:
-u --username=<username>
specify user to regenerate. Requires admin privilages.
--all
regenerate token for every user. Requires admin privilages.
"""
payload = {}

if args.get('--all'):
payload = {'all': True}
elif args.get('--username'):
payload = {'username': args.get('--username')}

response = self._dispatch('post', '/v1/auth/tokens/', json.dumps(payload))

if response.status_code == requests.codes.ok:
if '--username' not in args or '--all' not in args:
self._settings['token'] = response.json()['token']
self._settings.save()
self._logger.info('Token regenerated.')
else:
self._logger.info("Token regeneration failed: {}".format(response.text))
sys.exit(1)

def builds(self, args):
"""
Valid commands for builds:
Expand Down
2 changes: 1 addition & 1 deletion controller/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
The **api** Django app presents a RESTful web API for interacting with the **deis** system.
"""

__version__ = '1.4.0'
__version__ = '1.5.0'
15 changes: 15 additions & 0 deletions controller/api/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,18 @@ def has_permission(self, request, view):
if not auth_header:
return False
return auth_header == settings.BUILDER_KEY


class CanRegenerateToken(permissions.BasePermission):
"""
Checks if a user can regenerate a token
"""

def has_permission(self, request, view):
"""
Return `True` if permission is granted, `False` otherwise.
"""
if 'username' in request.data or 'all' in request.data:
return request.user.is_superuser
else:
return True
30 changes: 30 additions & 0 deletions controller/api/tests/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,3 +278,33 @@ def test_change_user_passwd(self):
response = self.client.post(url, json.dumps(submit), content_type='application/json',
HTTP_AUTHORIZATION='token {}'.format(self.user1_token))
self.assertEqual(response.status_code, 200)

def test_regenerate(self):
""" Test that token regeneration works"""

url = '/v1/auth/tokens/'

response = self.client.post(url, '{}', content_type='application/json',
HTTP_AUTHORIZATION='token {}'.format(self.admin_token))

self.assertEqual(response.status_code, 200)
self.assertNotEqual(response.data['token'], self.admin_token)

self.admin_token = Token.objects.get(user=self.admin)

response = self.client.post(url, '{"username" : "autotest2"}',
content_type='application/json',
HTTP_AUTHORIZATION='token {}'.format(self.admin_token))

self.assertEqual(response.status_code, 200)
self.assertNotEqual(response.data['token'], self.user1_token)

response = self.client.post(url, '{"all" : "true"}',
content_type='application/json',
HTTP_AUTHORIZATION='token {}'.format(self.admin_token))
self.assertEqual(response.status_code, 200)

response = self.client.post(url, '{}', content_type='application/json',
HTTP_AUTHORIZATION='token {}'.format(self.admin_token))

self.assertEqual(response.status_code, 401)
2 changes: 2 additions & 0 deletions controller/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@
views.UserManagementViewSet.as_view({'post': 'passwd'})),
url(r'^auth/login/',
'rest_framework.authtoken.views.obtain_auth_token'),
url(r'^auth/tokens/',
views.TokenManagementViewSet.as_view({'post': 'regenerate'})),
# admin sharing
url(r'^admin/perms/(?P<username>[-_\w]+)/?',
views.AdminPermsViewSet.as_view({'delete': 'destroy'})),
Expand Down
34 changes: 34 additions & 0 deletions controller/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet
from rest_framework.authtoken.models import Token

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

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


class TokenManagementViewSet(GenericViewSet,
mixins.DestroyModelMixin):
serializer_class = serializers.UserSerializer
permission_classes = [permissions.CanRegenerateToken]

def get_queryset(self):
return User.objects.filter(pk=self.request.user.pk)

def get_object(self):
return self.get_queryset()[0]

def regenerate(self, request, **kwargs):
obj = self.get_object()

if 'all' in request.data:
for user in User.objects.all():
if not user.is_anonymous():
token = Token.objects.get(user=user)
token.delete()
Token.objects.create(user=user)
return Response("")

if 'username' in request.data:
obj = get_object_or_404(User,
username=request.data['username'])
self.check_object_permissions(self.request, obj)

token = Token.objects.get(user=obj)
token.delete()
token = Token.objects.create(user=obj)
return Response({'token': token.key})


class BaseDeisViewSet(viewsets.OwnerViewSet):
"""
A generic ViewSet for objects related to Deis.
Expand Down
34 changes: 12 additions & 22 deletions docs/managing_deis/operational_tasks.rst
Original file line number Diff line number Diff line change
Expand Up @@ -52,28 +52,22 @@ Re-issuing User Authentication Tokens
The controller API uses a simple token-based HTTP Authentication scheme. Token authentication is
appropriate for client-server setups, such as native desktop and mobile clients. Each user of the
platform is issued a token the first time that they sign up on the platform. If this token is
compromised, you'll need to manually intervene to re-issue a new authentication token for the user.
To do this, SSH into the node running the controller and drop into a Django shell:
compromised, it will need to be regenerated.

A user can regenerate their own token like this:

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

.. code-block:: console
>>> from django.contrib.auth.models import User
>>> from rest_framework.authtoken.models import Token
>>> bob = User.objects.get(username='bob')
>>> token = Token.objects.get(user=bob)
>>> token.delete()
>>> exit()
$ deis auth:regenerate -u test-user
At this point, Bob will no longer be able to authenticate against the controller with his auth
At this point, the user will no longer be able to authenticate against the controller with his auth
token:

.. code-block:: console
Expand All @@ -83,14 +77,10 @@ token:
Detail:
Invalid token
For Bob to be able to use the API again, he will have to authenticate against the controller to be
re-issued a new token:
They will need to log back in to use their new auth token.

If there is a cluster wide security breach, an administrator can regenerate everybody's auth token like this:

.. code-block:: console
$ deis login http://deis.example.com
username: bob
password:
Logged in as bob
$ deis apps
=== Apps
$ deis auth:regenerate --all=true
2 changes: 0 additions & 2 deletions docs/reference/api-v1.4.rst
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
:title: Controller API v1.4
:description: The v1.4 REST API for Deis' Controller

.. _controller_api_v1:

Controller API v1.4
===================

Expand Down
Loading

0 comments on commit 46829d3

Please sign in to comment.