Repository: rucio/rucio

Issue:
OIDC authentication endpoint on Flask
Motivation
----------
Currently the OIDC authentication endpoint on Flask returns simple one-liner texts. Since the webpy version returned HTML via an HTML template, this needs to be evaluated for necessity and ported accordingly. This is probably related to #4041 since it uses templates as well. This is a leftover task from #3975 since the OIDC endpoints weren't created for Flask before.


Modification
------------
Create HTML templates for the Flask OIDC authentication endpoints and render them instead of returning simple text.
OIDC authentication endpoint on Flask
Motivation
----------
Currently the OIDC authentication endpoint on Flask returns simple one-liner texts. Since the webpy version returned HTML via an HTML template, this needs to be evaluated for necessity and ported accordingly. This is probably related to #4041 since it uses templates as well. This is a leftover task from #3975 since the OIDC endpoints weren't created for Flask before.


Modification
------------
Create HTML templates for the Flask OIDC authentication endpoints and render them instead of returning simple text.


Discussion:
👍 yes we will still need these html renders.
@gabrielefronze there is also this, which is on a smaller scale than #4041 but essentially quite similar.
👍 yes we will still need these html renders.
@gabrielefronze there is also this, which is on a smaller scale than #4041 but essentially quite similar.

Resolving patch:
diff --git a/lib/rucio/web/rest/flaskapi/v1/auth.py b/lib/rucio/web/rest/flaskapi/v1/auth.py
--- a/lib/rucio/web/rest/flaskapi/v1/auth.py
+++ b/lib/rucio/web/rest/flaskapi/v1/auth.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2018-2020 CERN
+# Copyright 2018-2021 CERN
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -19,7 +19,7 @@
 # - Hannes Hansen <hannes.jakob.hansen@cern.ch>, 2018-2019
 # - Joaquín Bogado <jbogado@linti.unlp.edu.ar>, 2019
 # - Andrew Lister <andrew.lister@stfc.ac.uk>, 2019
-# - Benedikt Ziemons <benedikt.ziemons@cern.ch>, 2020
+# - Benedikt Ziemons <benedikt.ziemons@cern.ch>, 2020-2021
 
 from __future__ import print_function
 
@@ -29,7 +29,7 @@
 import time
 from re import search
 
-from flask import Flask, Blueprint, request, Response, redirect
+from flask import Flask, Blueprint, request, Response, redirect, render_template
 from flask.views import MethodView
 from werkzeug.datastructures import Headers
 
@@ -39,7 +39,7 @@
 from rucio.common.exception import AccessDenied, IdentityError, RucioException
 from rucio.common.utils import date_to_str, urlparse
 from rucio.web.rest.flaskapi.v1.common import check_accept_header_wrapper_flask
-from rucio.web.rest.utils import generate_http_error_flask
+from rucio.web.rest.utils import generate_http_error_flask, error_headers
 
 # Extra modules: Only imported if available
 EXTRA_MODULES = {'onelogin': False}
@@ -267,18 +267,18 @@ def get(self):
             fetchtoken = (request.headers.get('X-Rucio-Client-Fetch-Token', default=None) == 'True')
             query_string = request.query_string.decode(encoding='utf-8')
             result = redirect_auth_oidc(query_string, fetchtoken)
-
-            # FIXME: render auth template on error
         except AccessDenied:
-            return generate_http_error_flask(401, 'CannotAuthenticate', 'Cannot get authentication URL from Rucio Authentication Server for account %(account)s' % locals(), headers=headers)
-        except RucioException as error:
-            return generate_http_error_flask(500, error.__class__.__name__, error.args[0], headers=headers)
+            headers.extend(error_headers('CannotAuthenticate', 'Cannot authorize your access, please check your access credentials'))
+            return render_template('auth_crash.html', crashtype='contact'), 401, headers
         except Exception as error:
             logging.exception("Internal Error")
-            return str(error), 500, headers
+            headers.extend(error_headers(error.__class__.__name__, str(error.args[0])))
+            return render_template('auth_crash.html', crashtype='internal_error'), 500, headers
 
         if not result:
-            return generate_http_error_flask(401, 'CannotAuthenticate', 'Cannot get authentication URL from Rucio Authentication Server for account %(account)s' % locals(), headers=headers)
+            headers.extend(error_headers('CannotAuthenticate', 'Cannot finalize your token request, no authorization content returned from the auth server'))
+            return render_template('auth_crash.html', crashtype='no_result'), 401, headers
+
         if fetchtoken:
             # this is only a case of returning the final token to the Rucio Client polling
             # or requesting token after copy-pasting the Rucio code from the web page page
@@ -341,22 +341,24 @@ def get(self):
         try:
             result = get_token_oidc(query_string, ip)
         except AccessDenied:
-            return generate_http_error_flask(401, 'CannotAuthenticate', 'Cannot get authentication code from Rucio Authentication Server for account %(account)s' % locals(), headers=headers)
-        except RucioException as error:
-            return generate_http_error_flask(500, error.__class__.__name__, error.args[0], headers=headers)
+            headers.extend(error_headers('CannotAuthenticate', 'Cannot authorize your access, please check your access credentials'))
+            return render_template('auth_crash.html', crashtype='contact'), 401, headers
         except Exception as error:
             logging.exception("Internal Error")
-            return str(error), 500, headers
+            headers.extend(error_headers(error.__class__.__name__, str(error.args[0])))
+            return render_template('auth_crash.html', crashtype='internal_error'), 500, headers
 
         if not result:
-            return generate_http_error_flask(401, 'CannotAuthenticate', 'Cannot get authentication code from Rucio Authentication Server for account %(account)s' % locals(), headers=headers)
+            headers.extend(error_headers('CannotAuthenticate', 'Cannot finalize your token request, no authorization content returned from the auth server'))
+            return render_template('auth_crash.html', crashtype='no_result'), 401, headers
+
         if 'fetchcode' in result:
-            msg = 'Please copy-paste the following code to the open terminal session with Rucio Client in order to get your access token: <b>' + result['fetchcode'] + '</b>'
-            return msg, 200, headers
+            return render_template('auth_granted.html', authcode=result['fetchcode']), 200, headers
         elif 'polling' in result and result['polling'] is True:
-            return 'Rucio Client should now be able to fetch your token automatically.', 200, headers
+            return render_template('auth_granted.html', authcode='allok'), 200, headers
         else:
-            return generate_http_error_flask(401, 'CannotAuthenticate', 'Cannot get authentication code from Rucio Authentication Server for account %(account)s' % locals(), headers=headers)
+            headers.extend(error_headers('InvalidRequest', 'Cannot recognize and process your request'))
+            return render_template('auth_crash.html', crashtype='bad_request'), 400, headers
 
 
 class TokenOIDC(MethodView):
@@ -422,7 +424,9 @@ def get(self):
         elif 'webhome' in result:
             webhome = result['webhome']
             if webhome is None:
-                return generate_http_error_flask(401, 'CannotAuthorize', 'Unknown identity.', headers=headers)
+                headers.extend(error_headers('CannotAuthenticate', 'Cannot find your OIDC identity linked to any Rucio account'))
+                headers.set('Content-Type', 'text/html')
+                return render_template('auth_crash.html', crashtype='unknown_identity'), 401, headers
             # domain setting is necessary so that the token gets distributed also to the webui server
             domain = '.'.join(urlparse.urlparse(webhome).netloc.split('.')[1:])
             response = redirect(webhome, code=303)
diff --git a/lib/rucio/web/rest/utils.py b/lib/rucio/web/rest/utils.py
--- a/lib/rucio/web/rest/utils.py
+++ b/lib/rucio/web/rest/utils.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2020 CERN
+# Copyright 2020-2021 CERN
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
 # limitations under the License.
 #
 # Authors:
-# - Benedikt Ziemons <benedikt.ziemons@cern.ch>, 2020
+# - Benedikt Ziemons <benedikt.ziemons@cern.ch>, 2020-2021
 
 import six
 from flask import Response
@@ -24,7 +24,7 @@
 from rucio.common.utils import codes, render_json
 
 
-def _error_response(exc_cls, exc_msg):
+def error_headers(exc_cls, exc_msg):
     def strip_newlines(msg):
         if msg is None:
             return None
@@ -35,9 +35,6 @@ def strip_newlines(msg):
             msg = six.ensure_text(str(msg), errors='replace')
         return msg.replace('\n', ' ').replace('\r', ' ')
 
-    data = {'ExceptionClass': exc_cls,
-            'ExceptionMessage': exc_msg}
-
     exc_msg = strip_newlines(exc_msg)
     if exc_msg:
         # Truncate too long exc_msg
@@ -45,10 +42,17 @@ def strip_newlines(msg):
         exc_msg = exc_msg[:min(oldlen, 125)]
         if len(exc_msg) != oldlen:
             exc_msg = exc_msg + '...'
-    headers = {'Content-Type': 'application/octet-stream',
-               'ExceptionClass': strip_newlines(exc_cls),
-               'ExceptionMessage': exc_msg}
+    return {
+        'ExceptionClass': strip_newlines(exc_cls),
+        'ExceptionMessage': exc_msg
+    }
 
+
+def _error_response(exc_cls, exc_msg):
+    data = {'ExceptionClass': exc_cls,
+            'ExceptionMessage': exc_msg}
+    headers = {'Content-Type': 'application/octet-stream'}
+    headers.update(error_headers(exc_cls=exc_cls, exc_msg=exc_msg))
     return data, headers
 
 


Test patch: