Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions django-cloudlaunch/cloudlaunch/authentication.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import rest_framework.authentication

from .models import AuthToken


# Override drf.authtoken's default create token and add a default token name.
# This requires the following setting:
# REST_AUTH_TOKEN_CREATOR = 'cloudlaunch.authentication.default_create_token'
def default_create_token(token_model, user, serializer):
token, _ = token_model.objects.get_or_create(user=user, name="default")
return token


# This is linked in from settings.py through DEFAULT_AUTHENTICATION_CLASSES
# and will override DRF's default token auth.
# Also requires setting REST_AUTH_TOKEN_MODEL = 'cloudlaunch.models.AuthToken'
class TokenAuthentication(rest_framework.authentication.TokenAuthentication):
model = AuthToken
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Generated by Django 2.1.2 on 2018-10-08 14:40

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('cloudlaunch', '0005_change_public_key_pk_relation'),
]

operations = [
migrations.CreateModel(
name='AuthToken',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')),
('key', models.CharField(db_index=True, max_length=40, unique=True, verbose_name='Key')),
('name', models.CharField(max_length=64, verbose_name='Name', blank=False)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='auth_tokens', to=settings.AUTH_USER_MODEL, verbose_name='User')),
],
),
migrations.AlterUniqueTogether(
name='authtoken',
unique_together={('user', 'name')},
),
]
28 changes: 23 additions & 5 deletions django-cloudlaunch/cloudlaunch/models.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
from celery.result import AsyncResult
from django.conf import settings
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.db import models
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.template.defaultfilters import slugify
from rest_framework.authtoken.models import Token
import rest_framework.authtoken.models as drf_models

from djcloudbridge import models as cb_models

Expand All @@ -15,6 +14,7 @@
import json
import jsonmerge
import djcloudbridge
from django.core.exceptions import ObjectDoesNotExist


class Image(cb_models.DateNameAwareModel):
Expand Down Expand Up @@ -172,7 +172,7 @@ def compute_merged_config(self):
class ApplicationDeployment(cb_models.DateNameAwareModel):
"""Application deployment details."""

owner = models.ForeignKey(User, on_delete=models.CASCADE, null=False)
owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=False)
archived = models.BooleanField(blank=True, default=False)
application_version = models.ForeignKey(ApplicationVersion, on_delete=models.CASCADE, null=False)
target_cloud = models.ForeignKey(cb_models.Cloud, on_delete=models.CASCADE, null=False)
Expand Down Expand Up @@ -325,7 +325,7 @@ class Usage(models.Model):
related_name="app_version_cloud_config",
null=True)
app_config = models.TextField(max_length=1024 * 16, blank=True, null=True)
user = models.ForeignKey(User, on_delete=models.CASCADE, null=False)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=False)

class Meta:
ordering = ['added']
Expand Down Expand Up @@ -365,9 +365,27 @@ class UserProfile(models.Model):

# Link UserProfile to a User model instance
user = models.OneToOneField(
User, models.CASCADE, related_name="cloudlaunch_user_profile")
settings.AUTH_USER_MODEL, models.CASCADE, related_name="cloudlaunch_user_profile")

def __str__(self):
"""Set default display for objects."""
return "{0} ({1} {2})".format(self.user.username, self.user.first_name,
self.user.last_name)


# Based on: https://consideratecode.com/2016/10/06/multiple-authentication-toke
# ns-per-user-with-django-rest-framework/
# TODO: Consider using knox if this PR is merged:
# https://github.com/Tivix/django-rest-auth/pull/307
class AuthToken(drf_models.Token):
# key is no longer primary key, but still indexed and unique
key = models.CharField("Key", max_length=40, db_index=True, unique=True)
# relation to user is a ForeignKey, so each user can have more than one token
user = models.ForeignKey(
settings.AUTH_USER_MODEL, related_name='auth_tokens',
on_delete=models.CASCADE, verbose_name="User"
)
name = models.CharField("Name", max_length=64)

class Meta:
unique_together = (('user', 'name'),)
13 changes: 13 additions & 0 deletions django-cloudlaunch/cloudlaunch/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,3 +308,16 @@ def create(self, validated_data):
user=self.context.get('view').request.user)
return models.PublicKey.objects.create(
user_profile=user_profile, **validated_data)


class AuthTokenSerializer(serializers.ModelSerializer):
name = serializers.CharField()
key = serializers.CharField(read_only=True)

class Meta:
model = models.AuthToken
fields = ('id', 'name', 'key')

def create(self, validated_data):
user = self.context.get('view').request.user
return models.AuthToken.objects.create(user=user, **validated_data)
16 changes: 13 additions & 3 deletions django-cloudlaunch/cloudlaunch/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,11 @@
router.register(r'applications', views.ApplicationViewSet)
# router.register(r'images', views.ImageViewSet)
router.register(r'deployments', views.DeploymentViewSet, base_name='deployments')

router.register(r'auth', views.AuthView, base_name='auth')
router.register(r'auth/tokens', views.AuthTokenViewSet,
base_name='auth_token')

router.register(r'cors_proxy', views.CorsProxyView, base_name='corsproxy')
deployments_router = HybridNestedRouter(router, r'deployments',
lookup='deployment')
Expand All @@ -51,17 +55,23 @@
schema_view = get_schema_view(title='CloudLaunch API', url=settings.REST_SCHEMA_BASE_URL,
urlconf='cloudlaunch.urls')

registration_urls = [
url(r'^$', views.CustomRegisterView.as_view(), name='rest_register'),
url(r'', include(('rest_auth.registration.urls', 'rest_auth_reg'),
namespace='rest_auth_reg'))
]

urlpatterns = [
url(r'%sapi-token-auth/' % auth_regex_pattern, views.AuthTokenView.as_view()),
url(r'api/v1/', include(router.urls)),
url(r'api/v1/', include(deployments_router.urls)),
# This generates a duplicate url set with the cloudman url included
# get_urls() must be called or a cached set of urls will be returned.
url(infrastructure_regex_pattern, include(cloud_router.get_urls())),
url(infrastructure_regex_pattern, include('djcloudbridge.urls')),
url(auth_regex_pattern, include(('rest_auth.urls', 'rest_auth'), namespace='rest_auth')),
url(r'%sregistration' % auth_regex_pattern, include(('rest_auth.registration.urls', 'rest_auth_reg'),
namespace='rest_auth_reg')),

# Override default register view
url(r'%sregistration' % auth_regex_pattern, include((registration_urls, 'rest_auth_reg'), namespace='rest_auth_reg')),
url(r'%suser/public-keys/$' %
auth_regex_pattern, views.PublicKeyList.as_view()),
url(r'%suser/public-keys/(?P<pk>[0-9]+)/$' %
Expand Down
73 changes: 53 additions & 20 deletions django-cloudlaunch/cloudlaunch/views.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from django.http import HttpResponse
from django_filters import rest_framework as dj_filters
from rest_auth.registration.views import RegisterView
from rest_framework import authentication
from rest_framework import filters
from rest_framework import generics
Expand All @@ -8,7 +9,6 @@
from rest_framework import renderers
from rest_framework import status
from rest_framework import viewsets
from rest_framework.authtoken.models import Token
from rest_framework.pagination import PageNumberPagination
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
Expand All @@ -20,6 +20,7 @@
from . import models
from . import serializers
from . import view_helpers
from django.core.exceptions import ObjectDoesNotExist


class CustomApplicationPagination(PageNumberPagination):
Expand Down Expand Up @@ -63,6 +64,8 @@ def get(self, request, format=None):
reverse('rest_auth:rest_user_details')),
'registration': request.build_absolute_uri(
reverse('rest_auth_reg:rest_register')),
'tokens': request.build_absolute_uri(
reverse('auth_token-list')),
'password/reset': request.build_absolute_uri(
reverse('rest_auth:rest_password_reset')),
'password/reset/confirm': request.build_absolute_uri(
Expand All @@ -73,25 +76,6 @@ def get(self, request, format=None):
return Response(data)


class AuthTokenView(APIView):
"""
Return an auth token for a user that is already logged in.
"""
permission_classes = (IsAuthenticated,)
authentication_classes = (authentication.SessionAuthentication,)

def get(self, request, format=None):
try:
token = Token.objects.get(user=request.user)
return Response({'token': token.key})
except Token.DoesNotExist:
return Response({'token': None})

def post(self, request, format=None):
token, _ = Token.objects.get_or_create(user=request.user)
return Response({'token': token.key})


class CorsProxyView(APIView):
"""
API endpoint that allows applications to be viewed or edited.
Expand All @@ -105,6 +89,25 @@ def get(self, request, format=None):
content_type=response.headers.get('content-type'))


class AuthTokenViewSet(viewsets.ModelViewSet):
"""
Return an auth token for a user that is already logged in.
"""
permission_classes = (IsAuthenticated,)
authentication_classes = (authentication.SessionAuthentication,)
serializer_class = serializers.AuthTokenSerializer
filter_backends = (dj_filters.DjangoFilterBackend,)
filter_fields = ('name',)

def get_queryset(self):
"""
This view should return a list of all the tokens
for the currently authenticated user.
"""
user = self.request.user
return models.AuthToken.objects.filter(user=user)


class CloudManViewSet(drf_helpers.CustomReadOnlySingleViewSet):
"""
List CloudMan related urls.
Expand Down Expand Up @@ -171,3 +174,33 @@ class PublicKeyDetail(generics.RetrieveUpdateDestroyAPIView):
def get_queryset(self):
return models.PublicKey.objects.filter(
user_profile__user=self.request.user)


# Override registration view so that it supports multiple tokens
from django.conf import settings
from allauth.account import app_settings as allauth_settings
from rest_auth.app_settings import TokenSerializer

class CustomRegisterView(RegisterView):

def get_default_user_token(self, user):
"""
Returns the default token or None. The default token is
created for the user in
cloudlaunch/authentication.py:default_create_token
"""
return user.auth_tokens.filter(name="default").first()

def get_response_data(self, user):
if allauth_settings.EMAIL_VERIFICATION == \
allauth_settings.EmailVerificationMethod.MANDATORY:
return {"detail": _("Verification e-mail sent.")}

if getattr(settings, 'REST_USE_JWT', False):
data = {
'user': user,
'token': self.token
}
return JWTSerializer(data).data
else:
return TokenSerializer(self.get_default_user_token(user)).data
6 changes: 2 additions & 4 deletions django-cloudlaunch/cloudlaunchserver/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,10 @@ def ready(self):
# https://stackoverflow.com/questions/22233680/in-memory-broker-for-celery-unit-tests
# Also refer: https://github.com/celery/celerytest
app.control.purge()
celery_worker = app.Worker(app=app, pool='solo', concurrency=1)
#connections.close_all()
worker_thread = threading.Thread(target=celery_worker.start)
worker_thread = threading.Thread(target=app.worker_main)
worker_thread.daemon = True
worker_thread.start()

@receiver(django_server_shutdown)
def on_shutdown(sender, **kwargs):
# Do nothing. Calling stop results in celery hanging waiting for keyboard input
Expand Down
6 changes: 4 additions & 2 deletions django-cloudlaunch/cloudlaunchserver/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,6 @@
'nested_admin',
'smart_selects',
'corsheaders',
'rest_framework.authtoken',
'rest_auth',
'allauth',
'allauth.account',
Expand Down Expand Up @@ -210,13 +209,16 @@
'DEFAULT_AUTHENTICATION_CLASSES': (
# 'rest_framework.authentication.BasicAuthentication',
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.TokenAuthentication'
'cloudlaunch.authentication.TokenAuthentication'
)
}

REST_AUTH_SERIALIZERS = {
'USER_DETAILS_SERIALIZER': 'djcloudbridge.serializers.UserSerializer'
}
REST_AUTH_TOKEN_MODEL = 'cloudlaunch.models.AuthToken'
REST_AUTH_TOKEN_CREATOR = 'cloudlaunch.authentication.default_create_token'

REST_SESSION_LOGIN = True

REST_SCHEMA_BASE_URL = CLOUDLAUNCH_PATH_PREFIX + '/'
Expand Down
4 changes: 3 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,9 @@ def get_version(*file_paths):
'raven',
# For CloudMan2 plugin
'gitpython',
'ansible'
'ansible',
# Utility package for retrying operations
'retrying'
]

REQS_PROD = ([
Expand Down
6 changes: 4 additions & 2 deletions tests/fixtures/initial_test_data.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@
}
},
{
"model": "authtoken.token",
"pk": "272f075f152e59fd5ea55ca2d21728d2bfe37077",
"model": "cloudlaunch.authtoken",
"pk": "1",
"fields": {
"user": 3000,
"name": "CLI",
"key": "272f075f152e59fd5ea55ca2d21728d2bfe37077",
"created": "2018-04-09T19:05:41.077Z"
}
},
Expand Down