Commit 7fd2ac35 authored by Lukas Burgey's avatar Lukas Burgey
Browse files

Refactor API towards webpage

parent 56172289
...@@ -96,7 +96,7 @@ class OIDCTokenAuthBackend(object): ...@@ -96,7 +96,7 @@ class OIDCTokenAuthBackend(object):
LOGGER.error("Invalid parameters for get_userinfo") LOGGER.error("Invalid parameters for get_userinfo")
LOGGER.debug("Got user info:\n%s\n", user_info) #LOGGER.debug("Got user info:\n%s\n", user_info)
return user_info return user_info
def authenticate(self, request, token=None): def authenticate(self, request, token=None):
......
...@@ -13,6 +13,8 @@ from .models import OIDCConfig, default_idp ...@@ -13,6 +13,8 @@ from .models import OIDCConfig, default_idp
from .serializers import AuthInfoSerializer from .serializers import AuthInfoSerializer
from . import utils from . import utils
from ...frontend.views import state_view_data
LOGGER = logging.getLogger(__name__) LOGGER = logging.getLogger(__name__)
IDP_COOKIE_NAME = 'idp_id' IDP_COOKIE_NAME = 'idp_id'
...@@ -141,7 +143,7 @@ class LogoutView(views.APIView): ...@@ -141,7 +143,7 @@ class LogoutView(views.APIView):
def post(self, request): def post(self, request):
LOGGER.debug('logged out %s', request.user) LOGGER.debug('logged out %s', request.user)
logout(request) logout(request)
return Response({}) return Response(state_view_data(request))
class AuthInfo(generics.RetrieveAPIView): class AuthInfo(generics.RetrieveAPIView):
......
...@@ -19,12 +19,7 @@ class DeploymentsView(generics.ListAPIView): ...@@ -19,12 +19,7 @@ class DeploymentsView(generics.ListAPIView):
serializer_class = DeploymentStateSerializer serializer_class = DeploymentStateSerializer
def get_queryset(self): def get_queryset(self):
items = self.request.user.site.state_items.filter( return self.request.user.site.pending_tasks
state='deployment_pending',
) | self.request.user.site.state_items.filter(
state='removal_pending',
)
return [item.tasks for item in items]
# the client has to fetch the configuration (like services etc.) here # the client has to fetch the configuration (like services etc.) here
...@@ -56,11 +51,13 @@ class AckView(views.APIView): ...@@ -56,11 +51,13 @@ class AckView(views.APIView):
item.client_deployed() item.client_deployed()
else: else:
item.client_removed() item.client_removed()
return Response({'ok': True}) return Response({})
# this is no critical
LOGGER.info('%s executed the obsolete state#%s', request.user, state_id) LOGGER.info('%s executed the obsolete state#%s', request.user, state_id)
return Response({'ok': True}) return Response(
data={'error': 'obsolete_state'},
status=500,
)
class ResponseView(views.APIView): class ResponseView(views.APIView):
......
...@@ -17,31 +17,39 @@ class ServiceSerializer(serializers.ModelSerializer): ...@@ -17,31 +17,39 @@ class ServiceSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = models.Service model = models.Service
exclude = [] fields = [
'id',
'name',
'site',
'groups',
]
class DeploymentStateItemSerializer(serializers.ModelSerializer): class DeploymentStateItemSerializer(serializers.ModelSerializer):
service = ServiceSerializer() service = ServiceSerializer()
key = backend_serializers.SSHPublicKeySerializerB() key = backend_serializers.SSHPublicKeyRefSerializer()
site = SiteSerializer() site = SiteSerializer()
questionnaire = serializers.JSONField() questionnaire = serializers.JSONField()
credentials = serializers.JSONField()
class Meta: class Meta:
model = models.DeploymentStateItem model = models.DeploymentStateItem
fields = [ fields = [
'state_target', 'id',
'key',
'service',
'site', 'site',
'state', 'state',
'questionnaire', 'questionnaire',
'id', 'credentials',
'key',
'service',
] ]
class DeploymentStateSerializer(serializers.ModelSerializer): class DeploymentStateSerializer(serializers.ModelSerializer):
key = backend_serializers.SSHPublicKeySerializerB() key = backend_serializers.SSHPublicKeyRefSerializer()
service = ServiceSerializer() service = ServiceSerializer()
state_items = DeploymentStateItemSerializer(many=True)
class Meta: class Meta:
model = models.DeploymentState model = models.DeploymentState
fields = [ fields = [
...@@ -49,60 +57,55 @@ class DeploymentStateSerializer(serializers.ModelSerializer): ...@@ -49,60 +57,55 @@ class DeploymentStateSerializer(serializers.ModelSerializer):
'key', 'key',
'service', 'service',
'id', 'id',
'state_items',
] ]
class DeploymentSerializer(serializers.Serializer): class DeploymentSerializer(serializers.ModelSerializer):
service = ServiceSerializer() service = ServiceSerializer()
ssh_keys = backend_serializers.SSHPublicKeySerializer(many=True) ssh_keys = backend_serializers.SSHPublicKeySerializer(many=True)
ssh_keys_to_withdraw = backend_serializers.SSHPublicKeySerializer(many=True) ssh_keys_to_withdraw = backend_serializers.SSHPublicKeySerializer(many=True)
# deploys = DeploymentStateSerializer(many=True) states = DeploymentStateSerializer(many=True)
# removals = DeploymentStateSerializer(many=True)
class Meta:
model = models.Deployment
exclude = ['user']
class DeploymentSerializerB(serializers.Serializer):
service = ServiceSerializer()
class Meta: class Meta:
model = models.Deployment model = models.Deployment
fields = [
'service',
'ssh_keys',
'ssh_keys_to_withdraw',
'states',
'id',
]
# contains properties which change less often
class UserSerializer(serializers.ModelSerializer): class UserSerializer(serializers.ModelSerializer):
auth_groups = backend_serializers.AuthGroupSerializer(many=True)
groups = backend_serializers.GroupSerializer(many=True) groups = backend_serializers.GroupSerializer(many=True)
ssh_keys = backend_serializers.SSHPublicKeySerializer(many=True) ssh_keys = backend_serializers.SSHPublicKeySerializer(many=True)
deployments = DeploymentSerializer(many=True)
auth_groups = backend_serializers.AuthGroupSerializer(many=True)
deployment_states = DeploymentStateSerializer(many=True)
deployment_state_items = DeploymentStateItemSerializer(many=True)
class Meta: class Meta:
model = models.User model = models.User
fields = [ fields = [
'id', 'auth_groups',
'email', 'email',
'userinfo',
'ssh_keys',
'groups', 'groups',
'deployments', 'id',
'auth_groups', 'ssh_keys',
'deployment_states', 'userinfo',
'deployment_state_items',
] ]
class ClientSerializer(serializers.HyperlinkedModelSerializer): # contains properties which change a lot
class UserStateSerializer(serializers.ModelSerializer):
deployment_state_items = DeploymentStateItemSerializer(many=True)
deployment_states = DeploymentStateSerializer(many=True)
deployments = DeploymentSerializer(many=True)
class Meta: class Meta:
model = models.User model = models.User
fields = ['name', 'site'] fields = [
'deployment_state_items',
'deployment_states',
class StateSerializer(serializers.Serializer): 'deployments',
services = ServiceSerializer(many=True) ]
class ClientViewSerializer(serializers.Serializer):
deployments = serializers.JSONField()
...@@ -25,5 +25,7 @@ class ViewTest(TestCase): ...@@ -25,5 +25,7 @@ class ViewTest(TestCase):
response = client.get( response = client.get(
'/backend/api/state/', '/backend/api/state/',
) )
self.assertTrue(response.json()['logged_in']) self.assertTrue('services' in response.json())
self.assertTrue('user_state' in response.json())
self.assertTrue('user' in response.json())
self.assertEqual(response.json()['user']['userinfo']['email'], test_models.TEST_EMAIL) self.assertEqual(response.json()['user']['userinfo']['email'], test_models.TEST_EMAIL)
...@@ -2,7 +2,7 @@ import logging ...@@ -2,7 +2,7 @@ import logging
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.contrib.auth import logout from django.contrib.auth import logout
from rest_framework import views, viewsets from rest_framework import views
from rest_framework.permissions import AllowAny from rest_framework.permissions import AllowAny
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework import status from rest_framework import status
...@@ -22,30 +22,27 @@ def user_services(user): ...@@ -22,30 +22,27 @@ def user_services(user):
return [] return []
def _api_error_response(): def _api_error_response(error):
return Response({'ok': False}, status=status.HTTP_400_BAD_REQUEST) return Response({'error': error}, status=status.HTTP_400_BAD_REQUEST)
def user_state_dict(user): def user_state(user):
return serializers.UserSerializer(user).data return serializers.UserStateSerializer(
user,
).data
def _api_state_response_data(request):
return {
'user_state': user_state(request.user),
}
# Response for StateView, LogoutView, and all post requests # Response for StateView, LogoutView, and all post requests
def _api_state_response(request): def _api_state_response(request):
if not request.user.is_authenticated: if not request.user.is_authenticated:
return Response( return Response({
{ 'user_state': None,
'logged_in': False, })
}
)
response = { response = _api_state_response_data(request)
'logged_in': True,
'user': user_state_dict(request.user),
'services': serializers.ServiceSerializer(
user_services(request.user),
many=True,
).data,
}
if 'error' in request.session: if 'error' in request.session:
response['error'] = request.session['error'] response['error'] = request.session['error']
...@@ -54,18 +51,29 @@ def _api_state_response(request): ...@@ -54,18 +51,29 @@ def _api_state_response(request):
return Response(response) return Response(response)
def state_view_data(request):
data = {}
if request.user.is_authenticated:
data = _api_state_response_data(request)
data['user'] = serializers.UserSerializer(
request.user,
).data
data['services'] = serializers.ServiceSerializer(
user_services(request.user),
many=True,
).data
else:
data['user'] = None
data['user_state'] = None
data['services'] = []
return data
class StateView(views.APIView): class StateView(views.APIView):
permission_classes = (AllowAny,) permission_classes = (AllowAny,)
def get(self, request): def get(self, request):
return _api_state_response(request) return Response(state_view_data(request))
# pylint: disable=too-many-ancestors
class ServiceViewSet(viewsets.ModelViewSet):
serializer_class = serializers.ServiceSerializer
queryset = models.Service.objects.all()
class SSHPublicKeyView(views.APIView): class SSHPublicKeyView(views.APIView):
...@@ -92,7 +100,7 @@ class SSHPublicKeyView(views.APIView): ...@@ -92,7 +100,7 @@ class SSHPublicKeyView(views.APIView):
return _api_state_response(request) return _api_state_response(request)
LOGGER.error('SSHPublicKeyView: malformed request %s', request) LOGGER.error('SSHPublicKeyView: malformed request %s', request)
return _api_error_response() return _api_error_response("malformed request")
class DeploymentView(views.APIView): class DeploymentView(views.APIView):
...@@ -103,7 +111,9 @@ class DeploymentView(views.APIView): ...@@ -103,7 +111,9 @@ class DeploymentView(views.APIView):
'service' not in request.data 'service' not in request.data
): ):
LOGGER.error('Deployment api got invalid request %s', request.data) LOGGER.error('Deployment api got invalid request %s', request.data)
return _api_error_response() return _api_error_response(
"request misses fields (should have: 'type', 'key', and 'service')"
)
request_type = request.data['type'] request_type = request.data['type']
request_service = models.Service.objects.get( request_service = models.Service.objects.get(
...@@ -123,19 +133,23 @@ class DeploymentView(views.APIView): ...@@ -123,19 +133,23 @@ class DeploymentView(views.APIView):
elif request_type == 'remove': elif request_type == 'remove':
deployment.remove_key(request_key) deployment.remove_key(request_key)
else: else:
return _api_error_response() return _api_error_response("invalid value of field 'type'")
deployment.save() deployment.save()
return _api_state_response(request) return Response(
serializers.DeploymentSerializer(deployment).data
)
class QuestionnaireView(views.APIView): class QuestionnaireView(views.APIView):
def post(self, request): def post(self, request):
state_item_id = request.query_params.get('id', '') state_item_id = request.query_params.get('id', '')
if state_item_id != '': if state_item_id != '':
item = models.DeploymentStateItem.objects.filter(id=int(state_item_id)) item = models.DeploymentStateItem.objects.filter(
id=int(state_item_id),
)
if item.exists(): if item.exists():
item.first().questionnaire_answered( item.first().user_answers(
answers=request.data, answers=request.data,
) )
else: else:
......
...@@ -196,9 +196,7 @@ class RabbitMQInstance(SingletonModel): ...@@ -196,9 +196,7 @@ class RabbitMQInstance(SingletonModel):
msg, msg,
) )
def publish_to_webpage(self, user, msg): def publish_to_user(self, user, msg):
# noise
# LOGGER.debug('Signalling webpage of user %s', user)
self._publish( self._publish(
'update', 'update',
str(user.id), str(user.id),
...@@ -421,6 +419,10 @@ class Site(models.Model): ...@@ -421,6 +419,10 @@ class Site(models.Model):
blank=True, blank=True,
) )
@property
def pending_tasks(self):
return [item.parent for item in self.state_items.all()]
def __str__(self): def __str__(self):
return self.name return self.name
...@@ -484,7 +486,7 @@ class SSHPublicKey(models.Model): ...@@ -484,7 +486,7 @@ class SSHPublicKey(models.Model):
# the receiver 'delete_withdrawn_ssh_key' does the actual deletion # the receiver 'delete_withdrawn_ssh_key' does the actual deletion
def delete_key(self): def delete_key(self):
# if this key is not deployed anywhere we delete it now # if this key is not deployed anywhere we delete it now
if analyze_states(self.states) == 'not_deployed': if analyze_states(self.states.all()) == 'not_deployed':
LOGGER.info(self.msg('Direct deletion of key')) LOGGER.info(self.msg('Direct deletion of key'))
self.delete() self.delete()
return return
...@@ -641,7 +643,7 @@ class Deployment(models.Model): ...@@ -641,7 +643,7 @@ class Deployment(models.Model):
return '{}:{}'.format(self.service, self.user) return '{}:{}'.format(self.service, self.user)
def msg(self, msg): def msg(self, msg):
return '[Deployment:{}] {}'.format(self, msg) return '[Depl.m:{}] {}'.format(self, msg)
# DeploymentState: knows: # DeploymentState: knows:
...@@ -728,14 +730,14 @@ class DeploymentState(models.Model): ...@@ -728,14 +730,14 @@ class DeploymentState(models.Model):
for item in self.state_items.all(): for item in self.state_items.all():
item.user_deploy() item.user_deploy()
self.publish_to_client() self.publish_to_client()
self.publish_to_user() # each state item publishes its state to the user
def remove(self): def remove(self):
self._set_target('not_deployed') self._set_target('not_deployed')
for item in self.state_items.all(): for item in self.state_items.all():
item.user_remove() item.user_remove()
self.publish_to_client() self.publish_to_client()
self.publish_to_user() # each state item publishes its state to the user
def publish_to_client(self): def publish_to_client(self):
# mitigating circular dependencies here # mitigating circular dependencies here
...@@ -748,11 +750,11 @@ class DeploymentState(models.Model): ...@@ -748,11 +750,11 @@ class DeploymentState(models.Model):
) )
# update the state of the remote webpage # update the state of the remote webpage
def publish_to_user(self): def publish_to_user(self):
from .frontend.views import user_state_dict from .frontend.views import user_state
content = { content = {
'user_state': user_state_dict(self.user), 'user_state': user_state(self.user),
} }
RabbitMQInstance.load().publish_to_webpage( RabbitMQInstance.load().publish_to_user(
self.user, self.user,
content, content,
) )
...@@ -766,8 +768,9 @@ class DeploymentState(models.Model): ...@@ -766,8 +768,9 @@ class DeploymentState(models.Model):
self.save() self.save()
def __str__(self): def __str__(self):
return "{}#{}".format( return "{}:{}#{}".format(
self.deployment.service, self.deployment.service,
self.key,
self.id, self.id,
) )
...@@ -822,26 +825,15 @@ class DeploymentStateItem(models.Model): ...@@ -822,26 +825,15 @@ class DeploymentStateItem(models.Model):
def key(self): def key(self):
return self.parent.key return self.parent.key
@property
def state_target(self):
if (
self.state == 'deployment_pending'
or self.state == 'questionnaire'
):
return 'deploy'
elif self.state == 'removal_pending':
return 'withdraw'
# no state_target is executed
return 'none'
# STATE transitions # STATE transitions
# user: deployment requested # user: deployment requested
def user_deploy(self): def user_deploy(self):
if self.state == 'removal_pending': if self.state == 'removal_pending':
self._set_state('deployed') self._set_state('deployed')
return return
if self.state == 'deployed':
LOGGER.info(self.msg('ignoring invalid state transition user_deploy'))
return
self._set_state('deployment_pending') self._