Commit d45e4e76 authored by Lukas Burgey's avatar Lukas Burgey
Browse files

Implement backchannel from the client to the backend

parent 0cc4ef92
...@@ -55,7 +55,7 @@ class OIDCConfig(db_models.Model): ...@@ -55,7 +55,7 @@ class OIDCConfig(db_models.Model):
args = { args = {
'client_id': self.client_id, 'client_id': self.client_id,
'response_type': 'code', 'response_type': 'code',
'scope': ['openid', 'profile', 'email'], 'scope': ['openid', 'profile', 'email', 'credentials'],
'redirect_uri': self.redirect_uri, 'redirect_uri': self.redirect_uri,
'state': state, 'state': state,
} }
...@@ -79,33 +79,45 @@ def default_idp(): ...@@ -79,33 +79,45 @@ def default_idp():
class OIDCTokenAuthBackend(object): class OIDCTokenAuthBackend(object):
def get_userinfo(self, request, access_token, token_type='Bearer'): def get_userinfo(self, request, oidc_client, access_token='', state=None):
idp_id = utils.get_session(request, 'idp_id', None) user_info = None
idp = OIDCConfig.objects.get(id=idp_id)
req = Request( if access_token is not None:
idp.oidc_client.provider_info['userinfo_endpoint'] req = Request(
) oidc_client.provider_info['userinfo_endpoint']+'?scope=openid&scope=profile',
auth = (token_type + ' ' + access_token) )
req.add_header('Authorization', auth) 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): def authenticate(self, request, token=None):
if token is None: if token is None:
return 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_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: try:
return models.User.get_user( return models.User.get_user(
userinfo, userinfo,
idp, oidc_client,
) )
except Exception as exception: except Exception as exception:
LOGGER.error('OIDCTokenAuthBackend: error constructing user: %s', exception) LOGGER.error('OIDCTokenAuthBackend: error constructing user: %s', exception)
return None return None
......
...@@ -64,9 +64,10 @@ class AuthCallback(View): ...@@ -64,9 +64,10 @@ class AuthCallback(View):
idp_id = utils.get_session(request, 'idp_id', default_idp().id) idp_id = utils.get_session(request, 'idp_id', default_idp().id)
oidc_config = OIDCConfig.objects.get(id=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) LOGGER.debug('AuthCallback: %s returned from IdP %s', state, oidc_config)
aresp = oidc_config.oidc_client.parse_response( aresp = oidc_client.parse_response(
AuthorizationResponse, AuthorizationResponse,
info=json.dumps(request.GET), info=json.dumps(request.GET),
) )
...@@ -79,17 +80,22 @@ class AuthCallback(View): ...@@ -79,17 +80,22 @@ class AuthCallback(View):
raise AuthException('AuthCallbackStates do not match') raise AuthException('AuthCallbackStates do not match')
ac_token_response = ( ac_token_response = (
oidc_config.oidc_client.do_access_token_request( oidc_client.do_access_token_request(
state=aresp['state'], state=state,
request_args={ request_args={
'code': aresp['code'] 'code': aresp['code']
}, },
) )
) )
# does fail with 'invalid_token' # FIXME 'email_verified' in user info is no boolean
# user_info = OIDC_CLIENT.do_user_info_request( # but oic expects it to be
# statearesp['state']) #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']) # user_info = self.get_user_info(ac_token_response['access_token'])
# try: # try:
# u = models.User.objects.get(sub=user_info['sub']) # u = models.User.objects.get(sub=user_info['sub'])
......
...@@ -5,4 +5,5 @@ URLPATTERNS = [ ...@@ -5,4 +5,5 @@ URLPATTERNS = [
url(r'^deployments', views.DeploymentsView.as_view()), url(r'^deployments', views.DeploymentsView.as_view()),
url(r'^config', views.ConfigurationView.as_view()), url(r'^config', views.ConfigurationView.as_view()),
url(r'^ack/(?P<task_id>\d+)', views.AckView.as_view()), url(r'^ack/(?P<task_id>\d+)', views.AckView.as_view()),
url(r'^response', views.ResponseView.as_view()),
] ]
...@@ -26,7 +26,6 @@ class ConfigurationView(views.APIView): ...@@ -26,7 +26,6 @@ class ConfigurationView(views.APIView):
authentication_classes = AUTHENTICATION_CLASSES authentication_classes = AUTHENTICATION_CLASSES
def get(self, request): def get(self, request):
response = { response = {
'services': ServiceSerializer( 'services': ServiceSerializer(
request.user.site.services.all(), request.user.site.services.all(),
...@@ -36,7 +35,6 @@ class ConfigurationView(views.APIView): ...@@ -36,7 +35,6 @@ class ConfigurationView(views.APIView):
RabbitMQInstance.load(), RabbitMQInstance.load(),
).data, ).data,
} }
LOGGER.debug('Config: %s', response)
return Response(response) return Response(response)
...@@ -48,8 +46,41 @@ class AckView(views.APIView): ...@@ -48,8 +46,41 @@ class AckView(views.APIView):
for item in request.user.site.task_items.all(): for item in request.user.site.task_items.all():
if item.task.id == int(task_id): if item.task.id == int(task_id):
item.task.item_finished(request.user.site) item.task.item_finished(request.user.site)
LOGGER.debug('Got acknowledgement for task %s', task_id)
return Response({'ok': True}) return Response({'ok': True})
# this is no critical # this is no critical
LOGGER.info('%s executed the obsolete task#%s', request.user, task_id) LOGGER.info('%s executed the obsolete task#%s', request.user, task_id)
return Response({'ok': True}) 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})
...@@ -32,6 +32,7 @@ class DeploymentTaskItemSerializer(serializers.ModelSerializer): ...@@ -32,6 +32,7 @@ class DeploymentTaskItemSerializer(serializers.ModelSerializer):
'key', 'key',
'service', 'service',
'site', 'site',
'state',
] ]
......
...@@ -102,7 +102,7 @@ class RabbitMQInstance(SingletonModel): ...@@ -102,7 +102,7 @@ class RabbitMQInstance(SingletonModel):
def _init_connection(self): def _init_connection(self):
global RABBITMQ_CONNECTION global RABBITMQ_CONNECTION
LOGGER.info('Opening new BlockingConnection') #LOGGER.debug('Opening new BlockingConnection')
RABBITMQ_CONNECTION = pika.BlockingConnection( RABBITMQ_CONNECTION = pika.BlockingConnection(
pika.ConnectionParameters( pika.ConnectionParameters(
host=self.host, host=self.host,
...@@ -141,7 +141,7 @@ class RabbitMQInstance(SingletonModel): ...@@ -141,7 +141,7 @@ class RabbitMQInstance(SingletonModel):
def _publish(self, exchange, routing_key, body): def _publish(self, exchange, routing_key, body):
channel = self._channel channel = self._channel
self._channel.basic_publish( channel.basic_publish(
exchange=exchange, exchange=exchange,
routing_key=routing_key, routing_key=routing_key,
body=body, body=body,
...@@ -246,17 +246,16 @@ class User(AbstractUser): ...@@ -246,17 +246,16 @@ class User(AbstractUser):
sub = userinfo['sub'] sub = userinfo['sub']
if 'email' not in userinfo: if 'email' not in userinfo:
if 'name' not in userinfo: username = sub
raise Exception('Missing attributes in userinfo: email and name')
username = userinfo['name']
else: else:
username = userinfo['email'] username = userinfo['email']
email = userinfo['email']
user = cls( user = cls(
user_type='oidcuser', user_type='oidcuser',
username=username, username=username,
sub=sub, sub=sub,
email=email,
idp=idp, idp=idp,
userinfo=userinfo, userinfo=userinfo,
) )
...@@ -715,6 +714,17 @@ class DeploymentTask(models.Model): ...@@ -715,6 +714,17 @@ class DeploymentTask(models.Model):
msg, 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 # the client acked the receipt and execution of the task for his site
def item_finished(self, site): def item_finished(self, site):
...@@ -726,14 +736,31 @@ class DeploymentTask(models.Model): ...@@ -726,14 +736,31 @@ class DeploymentTask(models.Model):
# finished sends its own message # finished sends its own message
self._finished() self._finished()
else: else:
from .frontend.views import user_state_dict self.send_state_update()
content = {
'user_state': user_state_dict(self.user), # the client failed to execute the item
} # the client can try again later
RabbitMQInstance.load().publish_to_webpage( # we signal the user about the failure
self.user, def item_failed(self, site):
content, 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 # maintenance after all task items are done
def _finished(self): def _finished(self):
...@@ -778,6 +805,18 @@ class DeploymentTaskItem(models.Model): ...@@ -778,6 +805,18 @@ class DeploymentTaskItem(models.Model):
related_name='deployment_task_items', related_name='deployment_task_items',
on_delete=models.CASCADE, 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 @property
def service(self): def service(self):
......
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