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

Refactor API towards webpage

parent 56172289
......@@ -96,7 +96,7 @@ class OIDCTokenAuthBackend(object):
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
def authenticate(self, request, token=None):
......
......@@ -13,6 +13,8 @@ from .models import OIDCConfig, default_idp
from .serializers import AuthInfoSerializer
from . import utils
from ...frontend.views import state_view_data
LOGGER = logging.getLogger(__name__)
IDP_COOKIE_NAME = 'idp_id'
......@@ -141,7 +143,7 @@ class LogoutView(views.APIView):
def post(self, request):
LOGGER.debug('logged out %s', request.user)
logout(request)
return Response({})
return Response(state_view_data(request))
class AuthInfo(generics.RetrieveAPIView):
......
......@@ -19,12 +19,7 @@ class DeploymentsView(generics.ListAPIView):
serializer_class = DeploymentStateSerializer
def get_queryset(self):
items = self.request.user.site.state_items.filter(
state='deployment_pending',
) | self.request.user.site.state_items.filter(
state='removal_pending',
)
return [item.tasks for item in items]
return self.request.user.site.pending_tasks
# the client has to fetch the configuration (like services etc.) here
......@@ -56,11 +51,13 @@ class AckView(views.APIView):
item.client_deployed()
else:
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)
return Response({'ok': True})
return Response(
data={'error': 'obsolete_state'},
status=500,
)
class ResponseView(views.APIView):
......
......@@ -17,31 +17,39 @@ class ServiceSerializer(serializers.ModelSerializer):
class Meta:
model = models.Service
exclude = []
fields = [
'id',
'name',
'site',
'groups',
]
class DeploymentStateItemSerializer(serializers.ModelSerializer):
service = ServiceSerializer()
key = backend_serializers.SSHPublicKeySerializerB()
key = backend_serializers.SSHPublicKeyRefSerializer()
site = SiteSerializer()
questionnaire = serializers.JSONField()
credentials = serializers.JSONField()
class Meta:
model = models.DeploymentStateItem
fields = [
'state_target',
'key',
'service',
'id',
'site',
'state',
'questionnaire',
'id',
'credentials',
'key',
'service',
]
class DeploymentStateSerializer(serializers.ModelSerializer):
key = backend_serializers.SSHPublicKeySerializerB()
key = backend_serializers.SSHPublicKeyRefSerializer()
service = ServiceSerializer()
state_items = DeploymentStateItemSerializer(many=True)
class Meta:
model = models.DeploymentState
fields = [
......@@ -49,60 +57,55 @@ class DeploymentStateSerializer(serializers.ModelSerializer):
'key',
'service',
'id',
'state_items',
]
class DeploymentSerializer(serializers.Serializer):
class DeploymentSerializer(serializers.ModelSerializer):
service = ServiceSerializer()
ssh_keys = backend_serializers.SSHPublicKeySerializer(many=True)
ssh_keys_to_withdraw = backend_serializers.SSHPublicKeySerializer(many=True)
# deploys = DeploymentStateSerializer(many=True)
# removals = DeploymentStateSerializer(many=True)
class Meta:
model = models.Deployment
exclude = ['user']
class DeploymentSerializerB(serializers.Serializer):
service = ServiceSerializer()
states = DeploymentStateSerializer(many=True)
class Meta:
model = models.Deployment
fields = [
'service',
'ssh_keys',
'ssh_keys_to_withdraw',
'states',
'id',
]
# contains properties which change less often
class UserSerializer(serializers.ModelSerializer):
auth_groups = backend_serializers.AuthGroupSerializer(many=True)
groups = backend_serializers.GroupSerializer(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:
model = models.User
fields = [
'id',
'auth_groups',
'email',
'userinfo',
'ssh_keys',
'groups',
'deployments',
'auth_groups',
'deployment_states',
'deployment_state_items',
'id',
'ssh_keys',
'userinfo',
]
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:
model = models.User
fields = ['name', 'site']
class StateSerializer(serializers.Serializer):
services = ServiceSerializer(many=True)
class ClientViewSerializer(serializers.Serializer):
deployments = serializers.JSONField()
fields = [
'deployment_state_items',
'deployment_states',
'deployments',
]
......@@ -25,5 +25,7 @@ class ViewTest(TestCase):
response = client.get(
'/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)
......@@ -2,7 +2,7 @@ import logging
from django.shortcuts import get_object_or_404
from django.core.exceptions import ObjectDoesNotExist
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.response import Response
from rest_framework import status
......@@ -22,30 +22,27 @@ def user_services(user):
return []
def _api_error_response():
return Response({'ok': False}, status=status.HTTP_400_BAD_REQUEST)
def _api_error_response(error):
return Response({'error': error}, status=status.HTTP_400_BAD_REQUEST)
def user_state_dict(user):
return serializers.UserSerializer(user).data
def user_state(user):
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
def _api_state_response(request):
if not request.user.is_authenticated:
return Response(
{
'logged_in': False,
}
)
return Response({
'user_state': None,
})
response = {
'logged_in': True,
'user': user_state_dict(request.user),
'services': serializers.ServiceSerializer(
user_services(request.user),
many=True,
).data,
}
response = _api_state_response_data(request)
if 'error' in request.session:
response['error'] = request.session['error']
......@@ -54,18 +51,29 @@ def _api_state_response(request):
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):
permission_classes = (AllowAny,)
def get(self, request):
return _api_state_response(request)
# pylint: disable=too-many-ancestors
class ServiceViewSet(viewsets.ModelViewSet):
serializer_class = serializers.ServiceSerializer
queryset = models.Service.objects.all()
return Response(state_view_data(request))
class SSHPublicKeyView(views.APIView):
......@@ -92,7 +100,7 @@ class SSHPublicKeyView(views.APIView):
return _api_state_response(request)
LOGGER.error('SSHPublicKeyView: malformed request %s', request)
return _api_error_response()
return _api_error_response("malformed request")
class DeploymentView(views.APIView):
......@@ -103,7 +111,9 @@ class DeploymentView(views.APIView):
'service' not in 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_service = models.Service.objects.get(
......@@ -123,19 +133,23 @@ class DeploymentView(views.APIView):
elif request_type == 'remove':
deployment.remove_key(request_key)
else:
return _api_error_response()
return _api_error_response("invalid value of field 'type'")
deployment.save()
return _api_state_response(request)
return Response(
serializers.DeploymentSerializer(deployment).data
)
class QuestionnaireView(views.APIView):
def post(self, request):
state_item_id = request.query_params.get('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():
item.first().questionnaire_answered(
item.first().user_answers(
answers=request.data,
)
else:
......
......@@ -196,9 +196,7 @@ class RabbitMQInstance(SingletonModel):
msg,
)
def publish_to_webpage(self, user, msg):
# noise
# LOGGER.debug('Signalling webpage of user %s', user)
def publish_to_user(self, user, msg):
self._publish(
'update',
str(user.id),
......@@ -421,6 +419,10 @@ class Site(models.Model):
blank=True,
)
@property
def pending_tasks(self):
return [item.parent for item in self.state_items.all()]
def __str__(self):
return self.name
......@@ -484,7 +486,7 @@ class SSHPublicKey(models.Model):
# the receiver 'delete_withdrawn_ssh_key' does the actual deletion
def delete_key(self):
# 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'))
self.delete()
return
......@@ -641,7 +643,7 @@ class Deployment(models.Model):
return '{}:{}'.format(self.service, self.user)
def msg(self, msg):
return '[Deployment:{}] {}'.format(self, msg)
return '[Depl.m:{}] {}'.format(self, msg)
# DeploymentState: knows:
......@@ -728,14 +730,14 @@ class DeploymentState(models.Model):
for item in self.state_items.all():
item.user_deploy()
self.publish_to_client()
self.publish_to_user()
# each state item publishes its state to the user
def remove(self):
self._set_target('not_deployed')
for item in self.state_items.all():
item.user_remove()
self.publish_to_client()
self.publish_to_user()
# each state item publishes its state to the user
def publish_to_client(self):
# mitigating circular dependencies here
......@@ -748,11 +750,11 @@ class DeploymentState(models.Model):
)
# update the state of the remote webpage
def publish_to_user(self):
from .frontend.views import user_state_dict
from .frontend.views import user_state
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,
content,
)
......@@ -766,8 +768,9 @@ class DeploymentState(models.Model):
self.save()
def __str__(self):
return "{}#{}".format(
return "{}:{}#{}".format(
self.deployment.service,
self.key,
self.id,
)
......@@ -822,26 +825,15 @@ class DeploymentStateItem(models.Model):
def key(self):
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
# user: deployment requested
def user_deploy(self):
if self.state == 'removal_pending':
self._set_state('deployed')
return
if self.state == 'deployed':
LOGGER.info(self.msg('ignoring invalid state transition user_deploy'))
return
self._set_state('deployment_pending')
......@@ -854,12 +846,17 @@ class DeploymentStateItem(models.Model):
self._set_state('not_deployed')
return
if self.state == 'not_deployed':
LOGGER.info(self.msg('ignoring invalid state transition user_remove'))
return
self._set_state('removal_pending')
# user: questionnaire answered
def user_answers(self, answers=None):
self.questionnaire = answers
self._set_state('deployment_pending')
self.parent.publish_to_client()
# client: deployed
def client_deployed(self, credentials=None):
......@@ -875,18 +872,6 @@ class DeploymentStateItem(models.Model):
self.questionnaire = questionnaire
self._set_state('questionnaire')
# only used when we got a questionnaire_answered
def publish(self):
# mitigating circular dependencies here
from .clientapi.serializers import DeploymentStateSerializer
data = DeploymentStateSerializer(self.state).data
data['questionnaire'] = self.questionnaire
RabbitMQInstance.load().publish_by_site(
self.site,
json.dumps(data),
)
def msg(self, msg):
return '[DSItem:{}] {}'.format(self, msg)
......@@ -898,8 +883,9 @@ class DeploymentStateItem(models.Model):
def __str__(self):
return "{}@{}#{}".format(
return "{}:{}@{}#{}".format(
self.parent.service,
self.parent.key,
self.site,
self.id,
)
......
......@@ -24,7 +24,7 @@ class SSHPublicKeySerializer(serializers.ModelSerializer):
fields = ['id', 'name', 'key']
class SSHPublicKeySerializerB(serializers.ModelSerializer):
class SSHPublicKeyRefSerializer(serializers.ModelSerializer):
class Meta:
model = SSHPublicKey
fields = ['id', 'name']
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment