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),