diff --git a/lib/main/models/__init__.py b/lib/main/models/__init__.py index 03d1c5cc5d..f5323f542a 100644 --- a/lib/main/models/__init__.py +++ b/lib/main/models/__init__.py @@ -14,8 +14,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible Commander. If not, see . -from django.db import models +from django.db import models, DatabaseError from django.db.models import CASCADE, SET_NULL, PROTECT +from django.db.models.signals import post_save +from django.dispatch import receiver from django.utils.translation import ugettext_lazy as _ from django.core.urlresolvers import reverse from django.contrib.auth.models import User @@ -23,6 +25,7 @@ from django.utils.timezone import now import exceptions from jsonfield import JSONField from djcelery.models import TaskMeta +from rest_framework.authtoken.models import Token # TODO: jobs and events model TBD # TODO: reporting model TBD @@ -814,3 +817,13 @@ class LaunchJobStatusEvent(models.Model): # FIXME: Connect host based on event_data. # TODO: reporting (MPD) + +@receiver(post_save, sender=User) +def create_auth_token_for_user(sender, **kwargs): + instance = kwargs.get('instance', None) + if instance: + try: + Token.objects.get_or_create(user=instance) + except DatabaseError: + pass # Only fails when creating a new superuser from syncdb on a + # new database (before migrate has been called). diff --git a/lib/main/tests/base.py b/lib/main/tests/base.py index 6ba02e9473..55bcf73e71 100644 --- a/lib/main/tests/base.py +++ b/lib/main/tests/base.py @@ -38,6 +38,7 @@ class BaseTestMixin(object): django_user = DjangoUser.objects.create_superuser(username, "%s@example.com", password) else: django_user = DjangoUser.objects.create_user(username, "%s@example.com", password) + self.assertTrue(django_user.auth_token) return django_user def make_organizations(self, created_by, count=1): @@ -98,7 +99,10 @@ class BaseTestMixin(object): assert data is not None client = Client() if auth: - client.login(username=auth[0], password=auth[1]) + if isinstance(auth, (list, tuple)): + client.login(username=auth[0], password=auth[1]) + elif isinstance(auth, basestring): + client = Client(HTTP_AUTHORIZATION='Token %s' % auth) method = getattr(client,method) response = None if data is not None: diff --git a/lib/main/tests/users.py b/lib/main/tests/users.py index d200396210..6f87c0e7e7 100644 --- a/lib/main/tests/users.py +++ b/lib/main/tests/users.py @@ -47,6 +47,33 @@ class UsersTest(BaseTest): self.post(url, expect=201, data=new_user2, auth=self.get_normal_credentials()) self.post(url, expect=400, data=new_user2, auth=self.get_normal_credentials()) + def test_auth_token_login(self): + auth_token_url = '/api/v1/authtoken/' + + # Always returns a 405 for any GET request, regardless of credentials. + self.get(auth_token_url, expect=405, auth=None) + self.get(auth_token_url, expect=405, auth=self.get_invalid_credentials()) + self.get(auth_token_url, expect=405, auth=self.get_normal_credentials()) + + # Posting without username/password fields or invalid username/password + # returns a 400 error. + data = {} + self.post(auth_token_url, data, expect=400) + data = dict(zip(('username', 'password'), self.get_invalid_credentials())) + self.post(auth_token_url, data, expect=400) + + # A valid username/password should give us an auth token. + data = dict(zip(('username', 'password'), self.get_normal_credentials())) + result = self.post(auth_token_url, data, expect=200, auth=None) + self.assertTrue('token' in result) + self.assertEqual(result['token'], self.normal_django_user.auth_token.key) + auth_token = result['token'] + + # Verify we can access our own user information with the auth token. + data = self.get('/api/v1/me/', expect=200, auth=auth_token) + self.assertEquals(data['results'][0]['username'], 'normal') + self.assertEquals(data['count'], 1) + def test_ordinary_user_can_modify_some_fields_about_himself_but_not_all_and_passwords_work(self): detail_url = '/api/v1/users/%s/' % self.other_django_user.pk diff --git a/lib/main/views.py b/lib/main/views.py index edd8664323..7837b9d552 100644 --- a/lib/main/views.py +++ b/lib/main/views.py @@ -26,10 +26,18 @@ from rest_framework import generics from rest_framework import permissions from rest_framework.response import Response from rest_framework import status +from rest_framework.settings import api_settings +from rest_framework.authtoken.views import ObtainAuthToken import exceptions import datetime from base_views import * +class AuthTokenView(ObtainAuthToken): + + renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES + # FIXME: Show a better form for HTML view + # FIXME: How to make this view discoverable? + class OrganizationsList(BaseList): model = Organization diff --git a/lib/settings/defaults.py b/lib/settings/defaults.py index 6060f916bf..47ceb8c5af 100644 --- a/lib/settings/defaults.py +++ b/lib/settings/defaults.py @@ -43,6 +43,7 @@ REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework.authentication.BasicAuthentication', 'rest_framework.authentication.SessionAuthentication', + 'rest_framework.authentication.TokenAuthentication', ) } @@ -138,6 +139,7 @@ INSTALLED_APPS = ( 'django.contrib.staticfiles', 'south', 'rest_framework', + 'rest_framework.authtoken', 'django_extensions', 'djcelery', 'kombu.transport.django', diff --git a/lib/urls.py b/lib/urls.py index 80f3aad6ef..0ae9fca028 100644 --- a/lib/urls.py +++ b/lib/urls.py @@ -18,6 +18,9 @@ from django.conf import settings from django.conf.urls import * import lib.main.views as views +# auth token +views_AuthTokenView = views.AuthTokenView.as_view() + # organizations service views_OrganizationsList = views.OrganizationsList.as_view() views_OrganizationsDetail = views.OrganizationsDetail.as_view() @@ -87,6 +90,9 @@ views_CredentialsDetail = views.CredentialsDetail.as_view() urlpatterns = patterns('', + # obtain auth token + url(r'^api/v1/authtoken/$', views_AuthTokenView), + # organizations vice url(r'^api/v1/organizations/$', views_OrganizationsList), url(r'^api/v1/organizations/(?P[0-9]+)/$', views_OrganizationsDetail),