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):
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)
def get_userinfo(self, request, oidc_client, access_token='', state=None):
user_info = None
if access_token is not None:
req = Request(
idp.oidc_client.provider_info['userinfo_endpoint']
oidc_client.provider_info['userinfo_endpoint']+'?scope=openid&scope=profile',
)
auth = (token_type + ' ' + access_token)
auth = ('Bearer ' + access_token)
req.add_header('Authorization', auth)
userinfo_bytes = urlopen(req).read()
return json.loads(userinfo_bytes.decode('UTF-8'))
user_info = json.loads(userinfo_bytes.decode('UTF-8'))
else:
LOGGER.error("Invalid parameters for get_userinfo")
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
......
......@@ -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'])
......
......@@ -5,4 +5,5 @@ URLPATTERNS = [
url(r'^deployments', views.DeploymentsView.as_view()),
url(r'^config', views.ConfigurationView.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):
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})
......@@ -32,6 +32,7 @@ class DeploymentTaskItemSerializer(serializers.ModelSerializer):
'key',
'service',
'site',
'state',
]
......
......@@ -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):
......
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