Implement OIDC through nginx's ngx_http_auth_request_module module.
This commit is contained in:
parent
1ff9a4d352
commit
85a378691e
12 changed files with 355 additions and 42 deletions
|
|
@ -51,6 +51,7 @@ pytest ./cara
|
||||||
s2i build file://$(pwd) --copy --keep-symlinks --context-dir ./app-config/nginx/ centos/nginx-112-centos7 cara-nginx-app
|
s2i build file://$(pwd) --copy --keep-symlinks --context-dir ./app-config/nginx/ centos/nginx-112-centos7 cara-nginx-app
|
||||||
s2i build file://$(pwd) --copy --keep-symlinks --env APP_NAME=cara-voila --context-dir ./ centos/python-36-centos7 cara-voila-app
|
s2i build file://$(pwd) --copy --keep-symlinks --env APP_NAME=cara-voila --context-dir ./ centos/python-36-centos7 cara-voila-app
|
||||||
s2i build file://$(pwd) --copy --keep-symlinks --env APP_NAME=cara-webservice --context-dir ./ centos/python-36-centos7 cara-webservice
|
s2i build file://$(pwd) --copy --keep-symlinks --env APP_NAME=cara-webservice --context-dir ./ centos/python-36-centos7 cara-webservice
|
||||||
|
s2i build file://$(pwd) --copy --keep-symlinks --context-dir ./app-config/auth-service centos/python-36-centos7 auth-service
|
||||||
cd app-config
|
cd app-config
|
||||||
docker-compose up
|
docker-compose up
|
||||||
```
|
```
|
||||||
|
|
|
||||||
54
app-config/auth-service/README.md
Normal file
54
app-config/auth-service/README.md
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
# auth-service
|
||||||
|
|
||||||
|
A simple auth service using OIDC which can be interrogated by NGINX.
|
||||||
|
|
||||||
|
|
||||||
|
To use, set the following environment variables:
|
||||||
|
|
||||||
|
* COOKIE_SECRET
|
||||||
|
* OIDC_SERVER
|
||||||
|
* OIDC_REALM
|
||||||
|
* CLIENT_ID
|
||||||
|
* CLIENT_SECRET
|
||||||
|
|
||||||
|
Then ``python -m auth_service``.
|
||||||
|
|
||||||
|
Once running, visit http://localhost:8080/auth/probe to find out if you are
|
||||||
|
already authenticated. Since you just started the app, you won't be authenticated,
|
||||||
|
so a 401 will be returned. Now go and visit http://localhost:8080/auth/login, which
|
||||||
|
will take you through the OIDC code authorization flow. Once complete you will eventually
|
||||||
|
be redirected to http://localhost:8080 and get a 404 error. Now go back to
|
||||||
|
http://localhost:8080/auth/probe to observe that your now authenticated.
|
||||||
|
|
||||||
|
To logout, hit http://localhost:8080/auth/logout (you'll be redirected again to
|
||||||
|
a 404) then re-visit http://localhost:8080/auth/probe to confirm you get a 401 again.
|
||||||
|
|
||||||
|
At this point, you may be wondering why would you want so many 401 & 404 errors.
|
||||||
|
The idea of this service is to be able to use it using
|
||||||
|
[nginx's ``ngx_http_auth_request_module``](
|
||||||
|
http://nginx.org/en/docs/http/ngx_http_auth_request_module.html).
|
||||||
|
A nice tutorial of using it inspired the creation of this package and may be
|
||||||
|
interesting to the curious reader
|
||||||
|
(https://redbyte.eu/en/blog/using-the-nginx-auth-request-module/).
|
||||||
|
|
||||||
|
## Integrating into NGINX
|
||||||
|
|
||||||
|
As mentioned, typically nginx has the [``ngx_http_auth_request_module``](
|
||||||
|
http://nginx.org/en/docs/http/ngx_http_auth_request_module.html) built-in, and so
|
||||||
|
we want to be able to profit from its ability to only allow authorized access to
|
||||||
|
certain specified locations.
|
||||||
|
|
||||||
|
In our nginx config we declare ``auth_request /auth/probe;`` for all
|
||||||
|
locations that should be authenticated. This endpoint (as you've already seen)
|
||||||
|
must either return a 200 or a 401 (and no other status!) depending on whether the
|
||||||
|
user is auth-ed or not. If we are authorized then nginx will redirect to the location,
|
||||||
|
otherwise we trigger a specialised 401 error with ``error_page 401 = @error401;`` which
|
||||||
|
essentially has a definition of:
|
||||||
|
|
||||||
|
```
|
||||||
|
location @error401 {
|
||||||
|
return 302 /auth/login;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In English: if the page is not authorized, redirect the browser to the login page.
|
||||||
1
app-config/auth-service/app.sh
Executable file
1
app-config/auth-service/app.sh
Executable file
|
|
@ -0,0 +1 @@
|
||||||
|
python -m auth_service
|
||||||
150
app-config/auth-service/auth_service/__init__.py
Normal file
150
app-config/auth-service/auth_service/__init__.py
Normal file
|
|
@ -0,0 +1,150 @@
|
||||||
|
try:
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
except ImportError:
|
||||||
|
# Python 3.6
|
||||||
|
from asyncio_extras import async_contextmanager as asynccontextmanager
|
||||||
|
import datetime
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
from keycloak.aio.realm import KeycloakRealm
|
||||||
|
import tornado.ioloop
|
||||||
|
import tornado.web
|
||||||
|
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class OIDCClientMixin:
|
||||||
|
@asynccontextmanager
|
||||||
|
async def get_oidc_client(self):
|
||||||
|
"""Async context manager to get hold of a OIDC client."""
|
||||||
|
realm_params = {
|
||||||
|
'server_url': self.settings['oicd_server'],
|
||||||
|
'realm_name': self.settings['oicd_realm'],
|
||||||
|
}
|
||||||
|
oicd_params = {
|
||||||
|
'client_id': self.settings['client_id'],
|
||||||
|
'client_secret': self.settings['client_secret'],
|
||||||
|
}
|
||||||
|
async with KeycloakRealm(**realm_params) as realm:
|
||||||
|
oidc_client = await realm.open_id_connect(**oicd_params)
|
||||||
|
yield oidc_client
|
||||||
|
|
||||||
|
|
||||||
|
class Login(tornado.web.RequestHandler):
|
||||||
|
async def get(self):
|
||||||
|
# Initiate the OICD flow.
|
||||||
|
return self.redirect('/auth/authenticate')
|
||||||
|
|
||||||
|
|
||||||
|
class Authentication(tornado.web.RequestHandler, OIDCClientMixin):
|
||||||
|
async def get(self):
|
||||||
|
async with self.get_oidc_client() as oidc_cli:
|
||||||
|
redirect_uri = f'{self.request.protocol}://{self.request.host}/auth/authorize'
|
||||||
|
LOG.info(f'Redirecting to the authorization url. Will return to {redirect_uri}')
|
||||||
|
return self.redirect(oidc_cli.authorization_url(redirect_uri=redirect_uri))
|
||||||
|
|
||||||
|
|
||||||
|
class Authorization(tornado.web.RequestHandler, OIDCClientMixin):
|
||||||
|
async def get(self):
|
||||||
|
code = self.get_argument('code', None)
|
||||||
|
if code is None:
|
||||||
|
# Somebody is hitting this endpoint without going through the proper
|
||||||
|
# flow. Let's start again.
|
||||||
|
return self.redirect('/auth/authenticate')
|
||||||
|
|
||||||
|
async with self.get_oidc_client() as oidc_cli:
|
||||||
|
redirect_uri = f'{self.request.protocol}://{self.request.host}/auth/authorize'
|
||||||
|
|
||||||
|
try:
|
||||||
|
LOG.info(f'Validating the authorization code')
|
||||||
|
result = await oidc_cli.authorization_code(code, redirect_uri=redirect_uri)
|
||||||
|
except aiohttp.client_exceptions.ClientConnectionError:
|
||||||
|
LOG.error(f'There was a problem validating the authorization code')
|
||||||
|
self.set_status(401)
|
||||||
|
# Happens when the code is no longer valid (e.g. if you re-visit a
|
||||||
|
# url that was tracked in the browser devtools).
|
||||||
|
self.finish('Error logging in. Would you like to <a href="/auth/login">try again?</a>')
|
||||||
|
seconds_per_day = 60 * 60 * 24
|
||||||
|
|
||||||
|
self.set_secure_cookie(
|
||||||
|
'refresh_token', result['refresh_token'],
|
||||||
|
expires_days=result['refresh_expires_in'] / seconds_per_day,
|
||||||
|
)
|
||||||
|
LOG.info(f'Fetching user info')
|
||||||
|
user_info = await oidc_cli.userinfo(result['access_token'] or '')
|
||||||
|
|
||||||
|
self.set_cookie('username', user_info['preferred_username'], expires_days=0.1)
|
||||||
|
LOG.info(f'User {user_info["preferred_username"]} successfully logged in. Redirecting to complete.')
|
||||||
|
return self.redirect(f'/auth/complete')
|
||||||
|
|
||||||
|
|
||||||
|
class LoginComplete(tornado.web.RequestHandler):
|
||||||
|
def get(self):
|
||||||
|
redirect = self.get_cookie('POST_AUTH_REDIRECT')
|
||||||
|
self.clear_cookie('POST_AUTH_REDIRECT')
|
||||||
|
if redirect is None:
|
||||||
|
LOG.info("Login complete. No redirect specified, redirecting to /")
|
||||||
|
self.redirect('/')
|
||||||
|
else:
|
||||||
|
LOG.info(f"Login complete. Redirecting to {redirect} as was initially requested")
|
||||||
|
self.redirect(redirect)
|
||||||
|
|
||||||
|
|
||||||
|
class ProbeAuthentication(tornado.web.RequestHandler):
|
||||||
|
"""A handler to return 200 if the user is logged in, and 401 if not"""
|
||||||
|
def get(self):
|
||||||
|
# Our "session" cookie is effectively the refresh_token.
|
||||||
|
refresh_token = self.get_secure_cookie('refresh_token')
|
||||||
|
self.set_header('Cache-Control', 'no-cache')
|
||||||
|
|
||||||
|
if refresh_token is None:
|
||||||
|
self.set_status(401)
|
||||||
|
else:
|
||||||
|
self.set_status(200)
|
||||||
|
|
||||||
|
username = self.get_cookie('username')
|
||||||
|
if username:
|
||||||
|
self.set_header('x_forwarded_user', username)
|
||||||
|
|
||||||
|
# Return some unique content to prevent tornado returning a 304
|
||||||
|
# (there is probably a better way).
|
||||||
|
self.finish(f'{datetime.datetime.now()}')
|
||||||
|
|
||||||
|
|
||||||
|
class Logout(tornado.web.RequestHandler, OIDCClientMixin):
|
||||||
|
async def get(self):
|
||||||
|
username = self.get_cookie('username')
|
||||||
|
if username:
|
||||||
|
LOG.info(f"Logging user {username} out")
|
||||||
|
self.clear_cookie('username')
|
||||||
|
|
||||||
|
refresh_token = self.get_secure_cookie('refresh_token')
|
||||||
|
if refresh_token:
|
||||||
|
self.clear_cookie('refresh_token')
|
||||||
|
refresh_token = refresh_token.decode()
|
||||||
|
async with self.get_oidc_client() as oicd_cli:
|
||||||
|
await oicd_cli.logout(refresh_token)
|
||||||
|
|
||||||
|
self.redirect('/')
|
||||||
|
|
||||||
|
|
||||||
|
def make_app():
|
||||||
|
return tornado.web.Application(
|
||||||
|
[
|
||||||
|
(r"/auth/probe", ProbeAuthentication),
|
||||||
|
(r'/auth/login', Login),
|
||||||
|
(r'/auth/authenticate', Authentication),
|
||||||
|
(r'/auth/authorize', Authorization),
|
||||||
|
(r'/auth/complete', LoginComplete),
|
||||||
|
(r'/auth/logout', Logout),
|
||||||
|
],
|
||||||
|
cookie_secret=os.environ['COOKIE_SECRET'],
|
||||||
|
debug=True,
|
||||||
|
oicd_server=os.environ['OIDC_SERVER'],
|
||||||
|
oicd_realm=os.environ['OIDC_REALM'],
|
||||||
|
client_id=os.environ['CLIENT_ID'],
|
||||||
|
client_secret=os.environ['CLIENT_SECRET'],
|
||||||
|
)
|
||||||
9
app-config/auth-service/auth_service/__main__.py
Normal file
9
app-config/auth-service/auth_service/__main__.py
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import tornado.ioloop
|
||||||
|
|
||||||
|
from . import make_app
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = make_app()
|
||||||
|
app.listen(8080)
|
||||||
|
tornado.ioloop.IOLoop.current().start()
|
||||||
57
app-config/auth-service/setup.py
Normal file
57
app-config/auth-service/setup.py
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
"""
|
||||||
|
setup.py for auth-service.
|
||||||
|
|
||||||
|
For reference see
|
||||||
|
https://packaging.python.org/guides/distributing-packages-using-setuptools/
|
||||||
|
|
||||||
|
"""
|
||||||
|
from pathlib import Path
|
||||||
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
|
|
||||||
|
HERE = Path(__file__).parent.absolute()
|
||||||
|
with (HERE / 'README.md').open('rt') as fh:
|
||||||
|
LONG_DESCRIPTION = fh.read().strip()
|
||||||
|
|
||||||
|
|
||||||
|
REQUIREMENTS: dict = {
|
||||||
|
'core': [
|
||||||
|
'aiohttp',
|
||||||
|
'asyncio_extras; python_version<"3.7"',
|
||||||
|
'python-keycloak-client',
|
||||||
|
'tornado',
|
||||||
|
],
|
||||||
|
'test': [
|
||||||
|
],
|
||||||
|
'dev': [
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name='auth-service',
|
||||||
|
version="0.0.1",
|
||||||
|
|
||||||
|
author='Phil Elson',
|
||||||
|
author_email='philip.elson@cern.ch',
|
||||||
|
description='A simple auth service that can be interrogated by NGINX',
|
||||||
|
|
||||||
|
packages=find_packages(),
|
||||||
|
python_requires='~=3.6',
|
||||||
|
classifiers=[
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"Operating System :: OS Independent",
|
||||||
|
],
|
||||||
|
|
||||||
|
install_requires=REQUIREMENTS['core'],
|
||||||
|
extras_require={
|
||||||
|
**REQUIREMENTS,
|
||||||
|
# The 'dev' extra is the union of 'test' and 'doc', with an option
|
||||||
|
# to have explicit development dependencies listed.
|
||||||
|
'dev': [req
|
||||||
|
for extra in ['dev', 'test', 'doc']
|
||||||
|
for req in REQUIREMENTS.get(extra, [])],
|
||||||
|
# The 'all' extra is the union of all requirements.
|
||||||
|
'all': [req for reqs in REQUIREMENTS.values() for req in reqs],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
@ -2,11 +2,27 @@ version: "3.8"
|
||||||
services:
|
services:
|
||||||
cara-app:
|
cara-app:
|
||||||
image: cara-voila-app
|
image: cara-voila-app
|
||||||
links:
|
|
||||||
- cara-router
|
cara-webservice:
|
||||||
|
image: cara-webservice
|
||||||
|
|
||||||
|
auth-service:
|
||||||
|
image: auth-service
|
||||||
|
environment:
|
||||||
|
- COOKIE_SECRET
|
||||||
|
- OIDC_SERVER
|
||||||
|
- OIDC_REALM
|
||||||
|
- CLIENT_ID
|
||||||
|
- CLIENT_SECRET
|
||||||
|
|
||||||
cara-router:
|
cara-router:
|
||||||
image: cara-nginx-app
|
image: cara-nginx-app
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
cara-webservice:
|
depends_on:
|
||||||
image: cara-webservice
|
cara-webservice:
|
||||||
|
condition: service_started
|
||||||
|
cara-app:
|
||||||
|
condition: service_started
|
||||||
|
auth-service:
|
||||||
|
condition: service_started
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ http {
|
||||||
'$status $body_bytes_sent "$http_referer" '
|
'$status $body_bytes_sent "$http_referer" '
|
||||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||||
|
|
||||||
|
|
||||||
sendfile on;
|
sendfile on;
|
||||||
tcp_nopush on;
|
tcp_nopush on;
|
||||||
tcp_nodelay on;
|
tcp_nodelay on;
|
||||||
|
|
@ -34,7 +33,6 @@ http {
|
||||||
|
|
||||||
large_client_header_buffers 4 16k;
|
large_client_header_buffers 4 16k;
|
||||||
|
|
||||||
|
|
||||||
error_page 404 /404.html;
|
error_page 404 /404.html;
|
||||||
location = /40x.html {
|
location = /40x.html {
|
||||||
}
|
}
|
||||||
|
|
@ -43,19 +41,43 @@ http {
|
||||||
location = /50x.html {
|
location = /50x.html {
|
||||||
}
|
}
|
||||||
|
|
||||||
location /voila-server/ {
|
proxy_set_header Host $http_host;
|
||||||
# cara-app is the name of the voila server in each of docker-compose,
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
# test-cara.web.cern.ch and cara.web.cern.ch.
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_pass http://cara-app:8080/voila-server/;
|
|
||||||
|
|
||||||
proxy_set_header Host $host;
|
location /auth {
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_pass_request_body off;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
proxy_set_header Content-Length "";
|
||||||
|
proxy_set_header If-Modified-Since "";
|
||||||
|
proxy_pass http://auth-service:8080;
|
||||||
|
}
|
||||||
|
|
||||||
|
location @error401 {
|
||||||
|
add_header Set-Cookie "POST_AUTH_REDIRECT=$request_uri;";
|
||||||
|
return 302 /auth/login;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /voila-server/ {
|
||||||
|
# Anything under voila-server or expert-app is authenticated.
|
||||||
|
auth_request /auth/probe;
|
||||||
|
error_page 401 = @error401;
|
||||||
|
|
||||||
|
# Promote some auth_request response headers to proxy headers.
|
||||||
|
# In the future we could store session data on the auth server, and
|
||||||
|
# use that to store user data such as email, etc.
|
||||||
|
auth_request_set $user $upstream_http_x_forwarded_user;
|
||||||
|
proxy_set_header X-Forwarded-User $user;
|
||||||
|
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Connection "upgrade";
|
proxy_set_header Connection "upgrade";
|
||||||
proxy_read_timeout 86400;
|
proxy_read_timeout 86400;
|
||||||
|
|
||||||
|
# cara-app is the name of the voila server in each of docker-compose,
|
||||||
|
# test-cara.web.cern.ch and cara.web.cern.ch.
|
||||||
|
proxy_pass http://cara-app:8080/voila-server/;
|
||||||
}
|
}
|
||||||
rewrite ^/expert-app$ /voila-server/voila/render/cara.ipynb last;
|
rewrite ^/expert-app$ /voila-server/voila/render/cara.ipynb last;
|
||||||
rewrite ^/(files/static)/(.*)$ /voila-server/voila/$1/$2 last;
|
rewrite ^/(files/static)/(.*)$ /voila-server/voila/$1/$2 last;
|
||||||
|
|
@ -66,18 +88,29 @@ http {
|
||||||
rewrite ^/voila/(.*)$ /voila-server/voila/$1 redirect;
|
rewrite ^/voila/(.*)$ /voila-server/voila/$1 redirect;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
# cara-webservice is the name of the tornado server (for the calculator)
|
# By default we have no authentication.
|
||||||
# in each of docker-compose, test-cara.web.cern.ch and cara.web.cern.ch.
|
proxy_pass http://cara-webservice:8080;
|
||||||
proxy_pass http://cara-webservice:8080/;
|
}
|
||||||
|
|
||||||
proxy_set_header Host $host;
|
location /calculator {
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
# Anything under calculator is authenticated.
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
auth_request /auth/probe;
|
||||||
|
error_page 401 = @error401;
|
||||||
|
|
||||||
|
# Promote some auth_request response headers to proxy headers.
|
||||||
|
# In the future we could store session data on the auth server, and
|
||||||
|
# use that to store user data such as email, etc.
|
||||||
|
auth_request_set $user $upstream_http_x_forwarded_user;
|
||||||
|
proxy_set_header X-Forwarded-User $user;
|
||||||
|
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Connection "upgrade";
|
proxy_set_header Connection "upgrade";
|
||||||
proxy_read_timeout 86400;
|
proxy_read_timeout 86400;
|
||||||
|
|
||||||
|
# cara-webservice is the name of the tornado server (for the calculator)
|
||||||
|
# in each of docker-compose, test-cara.web.cern.ch and cara.web.cern.ch.
|
||||||
|
proxy_pass http://cara-webservice:8080/calculator;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,22 +12,17 @@ from .user import AuthenticatedUser, AnonymousUser
|
||||||
|
|
||||||
|
|
||||||
class BaseRequestHandler(RequestHandler):
|
class BaseRequestHandler(RequestHandler):
|
||||||
|
|
||||||
async def prepare(self):
|
async def prepare(self):
|
||||||
"""Called at the beginning of a request before `get`/`post`/etc."""
|
"""Called at the beginning of a request before `get`/`post`/etc."""
|
||||||
username = self.request.headers.get("X-ADFS-LOGIN", None)
|
|
||||||
|
# For unauthenticated endpoints we have the username cookie if the
|
||||||
|
# user is logged in.
|
||||||
|
# For authenticated endpoints, we can expect X-Forwarded-User to be set.
|
||||||
|
username = self.get_cookie('username')
|
||||||
|
|
||||||
if username:
|
if username:
|
||||||
# the following headers must be set when logged in
|
|
||||||
email = self.request.headers["X-ADFS-EMAIL"]
|
|
||||||
firstname = self.request.headers["X-ADFS-FIRSTNAME"]
|
|
||||||
lastname = self.request.headers["X-ADFS-LASTNAME"]
|
|
||||||
fullname = self.request.headers["X-ADFS-FULLNAME"]
|
|
||||||
self.current_user = AuthenticatedUser(
|
self.current_user = AuthenticatedUser(
|
||||||
username=username,
|
username=html.escape(username),
|
||||||
email=email,
|
|
||||||
firstname=firstname,
|
|
||||||
lastname=lastname,
|
|
||||||
fullname=fullname
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.current_user = AnonymousUser()
|
self.current_user = AnonymousUser()
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
from tornado.ioloop import IOLoop
|
from tornado.ioloop import IOLoop
|
||||||
from tornado.options import define, options
|
|
||||||
|
|
||||||
from . import make_app
|
from . import make_app
|
||||||
|
|
||||||
|
|
@ -16,7 +15,7 @@ def configure_parser(parser):
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
args = configure_parser(parser)
|
configure_parser(parser)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
app = make_app(debug=args.no_debug)
|
app = make_app(debug=args.no_debug)
|
||||||
app.listen(8080)
|
app.listen(8080)
|
||||||
|
|
|
||||||
|
|
@ -15,12 +15,7 @@ class User:
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class AuthenticatedUser(User):
|
class AuthenticatedUser(User):
|
||||||
|
|
||||||
username: str
|
username: str
|
||||||
email: str
|
|
||||||
firstname: str
|
|
||||||
lastname: str
|
|
||||||
fullname: str
|
|
||||||
|
|
||||||
def is_authenticated(self) -> bool:
|
def is_authenticated(self) -> bool:
|
||||||
return True
|
return True
|
||||||
|
|
|
||||||
|
|
@ -68,11 +68,14 @@
|
||||||
<li class="signin">
|
<li class="signin">
|
||||||
<span>
|
<span>
|
||||||
Signed in as:
|
Signed in as:
|
||||||
<a href="https://cern.ch/account" class="account cern-multiple-mobile-signin">
|
{{ user.username }}
|
||||||
{{ user.fullname }}
|
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
|
<!--
|
||||||
|
// Disabled until we tidy up the CSS... ;)
|
||||||
|
<li><a href="/auth/logout">Sign out</a></li>
|
||||||
|
-->
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li><a href="https://cern.ch/directory" class="cern-directory ext"title="Search CERN resources and browse the directory" data-extlink="">Directory</a></li>
|
<li><a href="https://cern.ch/directory" class="cern-directory ext"title="Search CERN resources and browse the directory" data-extlink="">Directory</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue