From d45e4e76ddb4d6ac0a4415a0e6ad040e29da39ee Mon Sep 17 00:00:00 2001 From: Lukas Burgey Date: Tue, 12 Jun 2018 16:46:46 +0200 Subject: [PATCH] Implement backchannel from the client to the backend --- django_backend/backend/auth/v1/models.py | 44 +++++++----- django_backend/backend/auth/v1/views.py | 18 +++-- django_backend/backend/clientapi/urls.py | 1 + django_backend/backend/clientapi/views.py | 35 +++++++++- .../backend/frontend/serializers.py | 1 + django_backend/backend/models.py | 67 +++++++++++++++---- 6 files changed, 128 insertions(+), 38 deletions(-) diff --git a/django_backend/backend/auth/v1/models.py b/django_backend/backend/auth/v1/models.py index 0ad9f4d..70a9009 100644 --- a/django_backend/backend/auth/v1/models.py +++ b/django_backend/backend/auth/v1/models.py @@ -55,7 +55,7 @@ class OIDCConfig(db_models.Model): args = { 'client_id': self.client_id, 'response_type': 'code', - 'scope': ['openid', 'profile', 'email'], + 'scope': ['openid', 'profile', 'email', 'credentials'], 'redirect_uri': self.redirect_uri, 'state': state, } @@ -79,33 +79,45 @@ def default_idp(): class OIDCTokenAuthBackend(object): - def get_userinfo(self, request, access_token, token_type='Bearer'): - idp_id = utils.get_session(request, 'idp_id', None) - idp = OIDCConfig.objects.get(id=idp_id) - req = Request( - idp.oidc_client.provider_info['userinfo_endpoint'] - ) - auth = (token_type + ' ' + access_token) - req.add_header('Authorization', auth) + def get_userinfo(self, request, oidc_client, access_token='', state=None): + user_info = None + + if access_token is not None: + req = Request( + oidc_client.provider_info['userinfo_endpoint']+'?scope=openid&scope=profile', + ) + auth = ('Bearer ' + access_token) + req.add_header('Authorization', auth) + + userinfo_bytes = urlopen(req).read() + user_info = json.loads(userinfo_bytes.decode('UTF-8')) + + else: + LOGGER.error("Invalid parameters for get_userinfo") - userinfo_bytes = urlopen(req).read() - return json.loads(userinfo_bytes.decode('UTF-8')) + + LOGGER.debug("Got user info:\n%s\n", user_info) + return user_info def authenticate(self, request, token=None): if token is None: return None - # get the user info from the idp - userinfo = self.get_userinfo(request, token) idp_id = utils.get_session(request, 'idp_id', None) - idp = OIDCConfig.objects.get(id=idp_id) + oidc_client = OIDCConfig.objects.get(id=idp_id) + + # get the user info from the idp + userinfo = self.get_userinfo( + request, + oidc_client, + access_token=token, + ) try: return models.User.get_user( userinfo, - idp, + oidc_client, ) - except Exception as exception: LOGGER.error('OIDCTokenAuthBackend: error constructing user: %s', exception) return None diff --git a/django_backend/backend/auth/v1/views.py b/django_backend/backend/auth/v1/views.py index 3bad619..4db3e3b 100644 --- a/django_backend/backend/auth/v1/views.py +++ b/django_backend/backend/auth/v1/views.py @@ -64,9 +64,10 @@ class AuthCallback(View): idp_id = utils.get_session(request, 'idp_id', default_idp().id) oidc_config = OIDCConfig.objects.get(id=idp_id) + oidc_client = oidc_config.oidc_client LOGGER.debug('AuthCallback: %s returned from IdP %s', state, oidc_config) - aresp = oidc_config.oidc_client.parse_response( + aresp = oidc_client.parse_response( AuthorizationResponse, info=json.dumps(request.GET), ) @@ -79,17 +80,22 @@ class AuthCallback(View): raise AuthException('AuthCallbackStates do not match') ac_token_response = ( - oidc_config.oidc_client.do_access_token_request( - state=aresp['state'], + oidc_client.do_access_token_request( + state=state, request_args={ 'code': aresp['code'] }, ) ) - # does fail with 'invalid_token' - # user_info = OIDC_CLIENT.do_user_info_request( - # statearesp['state']) + # FIXME 'email_verified' in user info is no boolean + # but oic expects it to be + #user_info = oidc_client.do_user_info_request( + # method="GET", + # state=state, + #) + #LOGGER.debug("EXPERIMENT: %s", user_info) + # user_info = self.get_user_info(ac_token_response['access_token']) # try: # u = models.User.objects.get(sub=user_info['sub']) diff --git a/django_backend/backend/clientapi/urls.py b/django_backend/backend/clientapi/urls.py index 974ce1c..8d00dfa 100644 --- a/django_backend/backend/clientapi/urls.py +++ b/django_backend/backend/clientapi/urls.py @@ -5,4 +5,5 @@ URLPATTERNS = [ url(r'^deployments', views.DeploymentsView.as_view()), url(r'^config', views.ConfigurationView.as_view()), url(r'^ack/(?P\d+)', views.AckView.as_view()), + url(r'^response', views.ResponseView.as_view()), ] diff --git a/django_backend/backend/clientapi/views.py b/django_backend/backend/clientapi/views.py index 64fae64..0cea0c2 100644 --- a/django_backend/backend/clientapi/views.py +++ b/django_backend/backend/clientapi/views.py @@ -26,7 +26,6 @@ class ConfigurationView(views.APIView): authentication_classes = AUTHENTICATION_CLASSES def get(self, request): - response = { 'services': ServiceSerializer( request.user.site.services.all(), @@ -36,7 +35,6 @@ class ConfigurationView(views.APIView): RabbitMQInstance.load(), ).data, } - LOGGER.debug('Config: %s', response) return Response(response) @@ -48,8 +46,41 @@ class AckView(views.APIView): for item in request.user.site.task_items.all(): if item.task.id == int(task_id): item.task.item_finished(request.user.site) + LOGGER.debug('Got acknowledgement for task %s', task_id) return Response({'ok': True}) # this is no critical LOGGER.info('%s executed the obsolete task#%s', request.user, task_id) return Response({'ok': True}) + + +class ResponseView(views.APIView): + authentication_classes = AUTHENTICATION_CLASSES + + def post(self, request): + status = request.data['output']['status'] + task_id = request.data['id'] + + LOGGER.debug('%s responded to task %s:\n%s', request.user, task_id, request.data) + + # find the corresponding task for this item + task_item = None + for item in request.user.site.task_items.all(): + if item.task.id == int(task_id): + task_item = item + + if task_item is not None: + if status == 'success': + task_item.task.item_finished(request.user.site) + return Response({'ok': True}) + + elif status == 'fail': + task_item.task.item_failed(request.user.site) + return Response({'ok': True}) + + elif status == 'reject': + task_item.task.item_rejected(request.user.site) + return Response({'ok': True}) + + LOGGER.info('%s executed the obsolete task#%s', request.user, task_id) + return Response({'ok': False}) diff --git a/django_backend/backend/frontend/serializers.py b/django_backend/backend/frontend/serializers.py index 858945a..5238501 100644 --- a/django_backend/backend/frontend/serializers.py +++ b/django_backend/backend/frontend/serializers.py @@ -32,6 +32,7 @@ class DeploymentTaskItemSerializer(serializers.ModelSerializer): 'key', 'service', 'site', + 'state', ] diff --git a/django_backend/backend/models.py b/django_backend/backend/models.py index e4c1c7e..0f741c3 100644 --- a/django_backend/backend/models.py +++ b/django_backend/backend/models.py @@ -102,7 +102,7 @@ class RabbitMQInstance(SingletonModel): def _init_connection(self): global RABBITMQ_CONNECTION - LOGGER.info('Opening new BlockingConnection') + #LOGGER.debug('Opening new BlockingConnection') RABBITMQ_CONNECTION = pika.BlockingConnection( pika.ConnectionParameters( host=self.host, @@ -141,7 +141,7 @@ class RabbitMQInstance(SingletonModel): def _publish(self, exchange, routing_key, body): channel = self._channel - self._channel.basic_publish( + channel.basic_publish( exchange=exchange, routing_key=routing_key, body=body, @@ -246,17 +246,16 @@ class User(AbstractUser): sub = userinfo['sub'] if 'email' not in userinfo: - if 'name' not in userinfo: - raise Exception('Missing attributes in userinfo: email and name') - - username = userinfo['name'] + username = sub else: username = userinfo['email'] + email = userinfo['email'] user = cls( user_type='oidcuser', username=username, sub=sub, + email=email, idp=idp, userinfo=userinfo, ) @@ -715,6 +714,17 @@ class DeploymentTask(models.Model): msg, ) + # update the state of the remote webpage + def send_state_update(self): + from .frontend.views import user_state_dict + content = { + 'user_state': user_state_dict(self.user), + } + RabbitMQInstance.load().publish_to_webpage( + self.user, + content, + ) + # the client acked the receipt and execution of the task for his site def item_finished(self, site): @@ -726,14 +736,31 @@ class DeploymentTask(models.Model): # finished sends its own message self._finished() else: - from .frontend.views import user_state_dict - content = { - 'user_state': user_state_dict(self.user), - } - RabbitMQInstance.load().publish_to_webpage( - self.user, - content, - ) + self.send_state_update() + + # the client failed to execute the item + # the client can try again later + # we signal the user about the failure + def item_failed(self, site): + item = self.task_items.get(site=site) + item.state = 'failed' + item.save() + + self.send_state_update() + + # TODO implement + + # the client failed to execute the item + # the client needs additional information from the user to try again + # we have to ask the user for data + def item_rejected(self, site): + item = self.task_items.get(site=site) + item.state = 'rejected' + item.save() + + self.send_state_update() + + # TODO implement # maintenance after all task items are done def _finished(self): @@ -778,6 +805,18 @@ class DeploymentTaskItem(models.Model): related_name='deployment_task_items', on_delete=models.CASCADE, ) + STATE_CHOICES = ( + ('pending', 'Pending'), + ('done', 'Done'), + ('chanceled', 'Chanceled'), + ('failed', 'Failed'), + ('rejected', 'Rejected'), + ) + state = models.CharField( + max_length=20, + choices=STATE_CHOICES, + default='pending', + ) @property def service(self): -- GitLab