[UI] Remove manual authentication for login user (#2635)

* Remove manual authentication
* update python libraries
* remove psycopg2 and bring back wal-e
* remove unused vars
This commit is contained in:
Ida Novindasari 2024-05-23 10:51:46 +02:00 committed by GitHub
parent 1b08ee1acf
commit 1839baaad3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 22 additions and 198 deletions

View File

@ -16,15 +16,11 @@ from flask import (
Flask, Flask,
Response, Response,
abort, abort,
redirect,
render_template, render_template,
request, request,
send_from_directory, send_from_directory,
session,
) )
from flask_oauthlib.client import OAuth
from functools import wraps
from gevent import sleep, spawn from gevent import sleep, spawn
from gevent.pywsgi import WSGIServer from gevent.pywsgi import WSGIServer
from jq import jq from jq import jq
@ -34,11 +30,9 @@ from os import getenv
from re import X, compile from re import X, compile
from requests.exceptions import RequestException from requests.exceptions import RequestException
from signal import SIGTERM, signal from signal import SIGTERM, signal
from urllib.parse import urljoin
from . import __version__ from . import __version__
from .cluster_discovery import DEFAULT_CLUSTERS, StaticClusterDiscoverer from .cluster_discovery import DEFAULT_CLUSTERS, StaticClusterDiscoverer
from .oauth import OAuthRemoteAppWithRefresh
from .spiloutils import ( from .spiloutils import (
apply_postgresql, apply_postgresql,
@ -71,11 +65,8 @@ logger = getLogger(__name__)
SERVER_STATUS = {'shutdown': False} SERVER_STATUS = {'shutdown': False}
APP_URL = getenv('APP_URL') APP_URL = getenv('APP_URL')
AUTHORIZE_URL = getenv('AUTHORIZE_URL')
SPILO_S3_BACKUP_BUCKET = getenv('SPILO_S3_BACKUP_BUCKET') SPILO_S3_BACKUP_BUCKET = getenv('SPILO_S3_BACKUP_BUCKET')
TEAM_SERVICE_URL = getenv('TEAM_SERVICE_URL') TEAM_SERVICE_URL = getenv('TEAM_SERVICE_URL')
ACCESS_TOKEN_URL = getenv('ACCESS_TOKEN_URL')
TOKENINFO_URL = getenv('OAUTH2_TOKEN_INFO_URL')
OPERATOR_API_URL = getenv('OPERATOR_API_URL', 'http://postgres-operator') OPERATOR_API_URL = getenv('OPERATOR_API_URL', 'http://postgres-operator')
OPERATOR_CLUSTER_NAME_LABEL = getenv('OPERATOR_CLUSTER_NAME_LABEL', 'cluster-name') OPERATOR_CLUSTER_NAME_LABEL = getenv('OPERATOR_CLUSTER_NAME_LABEL', 'cluster-name')
@ -184,38 +175,6 @@ class WSGITransferEncodingChunked:
return self.app(environ, start_response) return self.app(environ, start_response)
oauth = OAuth(app)
auth = OAuthRemoteAppWithRefresh(
oauth,
'auth',
request_token_url=None,
access_token_method='POST',
access_token_url=ACCESS_TOKEN_URL,
authorize_url=AUTHORIZE_URL,
)
oauth.remote_apps['auth'] = auth
def verify_token(token):
if not token:
return False
r = requests.get(TOKENINFO_URL, headers={'Authorization': token})
return r.status_code == 200
def authorize(f):
@wraps(f)
def wrapper(*args, **kwargs):
if AUTHORIZE_URL and 'auth_token' not in session:
return redirect(urljoin(APP_URL, '/login'))
return f(*args, **kwargs)
return wrapper
def ok(body={}, status=200): def ok(body={}, status=200):
return ( return (
Response( Response(
@ -297,19 +256,16 @@ STATIC_HEADERS = {
@app.route('/css/<path:path>') @app.route('/css/<path:path>')
@authorize
def send_css(path): def send_css(path):
return send_from_directory('static/', path), 200, STATIC_HEADERS return send_from_directory('static/', path), 200, STATIC_HEADERS
@app.route('/js/<path:path>') @app.route('/js/<path:path>')
@authorize
def send_js(path): def send_js(path):
return send_from_directory('static/', path), 200, STATIC_HEADERS return send_from_directory('static/', path), 200, STATIC_HEADERS
@app.route('/') @app.route('/')
@authorize
def index(): def index():
return render_template('index.html', google_analytics=GOOGLE_ANALYTICS) return render_template('index.html', google_analytics=GOOGLE_ANALYTICS)
@ -345,7 +301,6 @@ DEFAULT_UI_CONFIG = {
@app.route('/config') @app.route('/config')
@authorize
def get_config(): def get_config():
config = DEFAULT_UI_CONFIG.copy() config = DEFAULT_UI_CONFIG.copy()
config.update(OPERATOR_UI_CONFIG) config.update(OPERATOR_UI_CONFIG)
@ -407,17 +362,15 @@ def get_teams_for_user(user_name):
@app.route('/teams') @app.route('/teams')
@authorize
def get_teams(): def get_teams():
return ok( return ok(
get_teams_for_user( get_teams_for_user(
session.get('user_name', ''), request.headers.get('X-Uid', ''),
) )
) )
@app.route('/services/<namespace>/<cluster>') @app.route('/services/<namespace>/<cluster>')
@authorize
def get_service(namespace: str, cluster: str): def get_service(namespace: str, cluster: str):
if TARGET_NAMESPACE not in ['', '*', namespace]: if TARGET_NAMESPACE not in ['', '*', namespace]:
@ -433,7 +386,6 @@ def get_service(namespace: str, cluster: str):
@app.route('/pooler/<namespace>/<cluster>') @app.route('/pooler/<namespace>/<cluster>')
@authorize
def get_list_poolers(namespace: str, cluster: str): def get_list_poolers(namespace: str, cluster: str):
if TARGET_NAMESPACE not in ['', '*', namespace]: if TARGET_NAMESPACE not in ['', '*', namespace]:
@ -449,7 +401,6 @@ def get_list_poolers(namespace: str, cluster: str):
@app.route('/statefulsets/<namespace>/<cluster>') @app.route('/statefulsets/<namespace>/<cluster>')
@authorize
def get_list_clusters(namespace: str, cluster: str): def get_list_clusters(namespace: str, cluster: str):
if TARGET_NAMESPACE not in ['', '*', namespace]: if TARGET_NAMESPACE not in ['', '*', namespace]:
@ -465,7 +416,6 @@ def get_list_clusters(namespace: str, cluster: str):
@app.route('/statefulsets/<namespace>/<cluster>/pods') @app.route('/statefulsets/<namespace>/<cluster>/pods')
@authorize
def get_list_members(namespace: str, cluster: str): def get_list_members(namespace: str, cluster: str):
if TARGET_NAMESPACE not in ['', '*', namespace]: if TARGET_NAMESPACE not in ['', '*', namespace]:
@ -485,7 +435,6 @@ def get_list_members(namespace: str, cluster: str):
@app.route('/namespaces') @app.route('/namespaces')
@authorize
def get_namespaces(): def get_namespaces():
if TARGET_NAMESPACE not in ['', '*']: if TARGET_NAMESPACE not in ['', '*']:
@ -503,7 +452,6 @@ def get_namespaces():
@app.route('/postgresqls') @app.route('/postgresqls')
@authorize
def get_postgresqls(): def get_postgresqls():
postgresqls = [ postgresqls = [
{ {
@ -602,7 +550,6 @@ def read_only(handler):
@app.route('/postgresqls/<namespace>/<cluster>', methods=['POST']) @app.route('/postgresqls/<namespace>/<cluster>', methods=['POST'])
@authorize
@namespaced @namespaced
def update_postgresql(namespace: str, cluster: str): def update_postgresql(namespace: str, cluster: str):
if READ_ONLY_MODE: if READ_ONLY_MODE:
@ -614,8 +561,8 @@ def update_postgresql(namespace: str, cluster: str):
postgresql = request.get_json(force=True) postgresql = request.get_json(force=True)
teams = get_teams_for_user(session.get('user_name', '')) teams = get_teams_for_user(request.headers.get('X-Uid', ''))
logger.info(f'Changes to: {cluster} by {session.get("user_name", "local-user")}/{teams} {postgresql}') # noqa logger.info(f'Changes to: {cluster} by {request.headers.get("X-Uid", "local-user")}/{teams} {postgresql}') # noqa
if SUPERUSER_TEAM and SUPERUSER_TEAM in teams: if SUPERUSER_TEAM and SUPERUSER_TEAM in teams:
logger.info(f'Allowing edit due to membership in superuser team {SUPERUSER_TEAM}') # noqa logger.info(f'Allowing edit due to membership in superuser team {SUPERUSER_TEAM}') # noqa
@ -810,7 +757,6 @@ def update_postgresql(namespace: str, cluster: str):
@app.route('/postgresqls/<namespace>/<cluster>', methods=['GET']) @app.route('/postgresqls/<namespace>/<cluster>', methods=['GET'])
@authorize
def get_postgresql(namespace: str, cluster: str): def get_postgresql(namespace: str, cluster: str):
if TARGET_NAMESPACE not in ['', '*', namespace]: if TARGET_NAMESPACE not in ['', '*', namespace]:
@ -826,7 +772,6 @@ def get_postgresql(namespace: str, cluster: str):
@app.route('/stored_clusters') @app.route('/stored_clusters')
@authorize
def get_stored_clusters(): def get_stored_clusters():
return respond( return respond(
read_stored_clusters( read_stored_clusters(
@ -837,7 +782,6 @@ def get_stored_clusters():
@app.route('/stored_clusters/<pg_cluster>', methods=['GET']) @app.route('/stored_clusters/<pg_cluster>', methods=['GET'])
@authorize
def get_versions(pg_cluster: str): def get_versions(pg_cluster: str):
return respond( return respond(
read_versions( read_versions(
@ -850,9 +794,7 @@ def get_versions(pg_cluster: str):
) )
@app.route('/stored_clusters/<pg_cluster>/<uid>', methods=['GET']) @app.route('/stored_clusters/<pg_cluster>/<uid>', methods=['GET'])
@authorize
def get_basebackups(pg_cluster: str, uid: str): def get_basebackups(pg_cluster: str, uid: str):
return respond( return respond(
read_basebackups( read_basebackups(
@ -867,7 +809,6 @@ def get_basebackups(pg_cluster: str, uid: str):
@app.route('/create-cluster', methods=['POST']) @app.route('/create-cluster', methods=['POST'])
@authorize
def create_new_cluster(): def create_new_cluster():
if READ_ONLY_MODE: if READ_ONLY_MODE:
@ -885,8 +826,8 @@ def create_new_cluster():
if TARGET_NAMESPACE not in ['', '*', namespace]: if TARGET_NAMESPACE not in ['', '*', namespace]:
return wrong_namespace() return wrong_namespace()
teams = get_teams_for_user(session.get('user_name', '')) teams = get_teams_for_user(request.headers.get('X-Uid', ''))
logger.info(f'Create cluster by {session.get("user_name", "local-user")}/{teams} {postgresql}') # noqa logger.info(f'Create cluster by {request.headers.get("X-Uid", "local-user")}/{teams} {postgresql}') # noqa
if SUPERUSER_TEAM and SUPERUSER_TEAM in teams: if SUPERUSER_TEAM and SUPERUSER_TEAM in teams:
logger.info(f'Allowing create due to membership in superuser team {SUPERUSER_TEAM}') # noqa logger.info(f'Allowing create due to membership in superuser team {SUPERUSER_TEAM}') # noqa
@ -898,7 +839,6 @@ def create_new_cluster():
@app.route('/postgresqls/<namespace>/<cluster>', methods=['DELETE']) @app.route('/postgresqls/<namespace>/<cluster>', methods=['DELETE'])
@authorize
def delete_postgresql(namespace: str, cluster: str): def delete_postgresql(namespace: str, cluster: str):
if TARGET_NAMESPACE not in ['', '*', namespace]: if TARGET_NAMESPACE not in ['', '*', namespace]:
return wrong_namespace() return wrong_namespace()
@ -910,9 +850,9 @@ def delete_postgresql(namespace: str, cluster: str):
if postgresql is None: if postgresql is None:
return not_found() return not_found()
teams = get_teams_for_user(session.get('user_name', '')) teams = get_teams_for_user(request.headers.get('X-Uid', ''))
logger.info(f'Delete cluster: {cluster} by {session.get("user_name", "local-user")}/{teams}') # noqa logger.info(f'Delete cluster: {cluster} by {request.headers.get("X-Uid", "local-user")}/{teams}') # noqa
if SUPERUSER_TEAM and SUPERUSER_TEAM in teams: if SUPERUSER_TEAM and SUPERUSER_TEAM in teams:
logger.info(f'Allowing delete due to membership in superuser team {SUPERUSER_TEAM}') # noqa logger.info(f'Allowing delete due to membership in superuser team {SUPERUSER_TEAM}') # noqa
@ -936,78 +876,30 @@ def proxy_operator(url: str):
@app.route('/operator/status') @app.route('/operator/status')
@authorize
def get_operator_status(): def get_operator_status():
return proxy_operator('/status/') return proxy_operator('/status/')
@app.route('/operator/workers/<worker>/queue') @app.route('/operator/workers/<worker>/queue')
@authorize
def get_operator_get_queue(worker: int): def get_operator_get_queue(worker: int):
return proxy_operator(f'/workers/{worker}/queue') return proxy_operator(f'/workers/{worker}/queue')
@app.route('/operator/workers/<worker>/logs') @app.route('/operator/workers/<worker>/logs')
@authorize
def get_operator_get_logs(worker: int): def get_operator_get_logs(worker: int):
return proxy_operator(f'/workers/{worker}/logs') return proxy_operator(f'/workers/{worker}/logs')
@app.route('/operator/clusters/<namespace>/<cluster>/logs') @app.route('/operator/clusters/<namespace>/<cluster>/logs')
@authorize
def get_operator_get_logs_per_cluster(namespace: str, cluster: str): def get_operator_get_logs_per_cluster(namespace: str, cluster: str):
return proxy_operator(f'/clusters/{namespace}/{cluster}/logs/') return proxy_operator(f'/clusters/{namespace}/{cluster}/logs/')
@app.route('/login')
def login():
redirect = request.args.get('redirect', False)
if not redirect:
return render_template('login-deeplink.html')
redirect_uri = urljoin(APP_URL, '/login/authorized')
return auth.authorize(callback=redirect_uri)
@app.route('/logout')
def logout():
session.pop('auth_token', None)
return redirect(urljoin(APP_URL, '/'))
@app.route('/favicon.png') @app.route('/favicon.png')
def favicon(): def favicon():
return send_from_directory('static/', 'favicon-96x96.png'), 200 return send_from_directory('static/', 'favicon-96x96.png'), 200
@app.route('/login/authorized')
def authorized():
resp = auth.authorized_response()
if resp is None:
return 'Access denied: reason=%s error=%s' % (
request.args['error'],
request.args['error_description']
)
if not isinstance(resp, dict):
return 'Invalid auth response'
session['auth_token'] = (resp['access_token'], '')
r = requests.get(
TOKENINFO_URL,
headers={
'Authorization': f'Bearer {session["auth_token"][0]}',
},
)
session['user_name'] = r.json().get('uid')
logger.info(f'Login from: {session["user_name"]}')
# return redirect(urljoin(APP_URL, '/'))
return render_template('login-resolve-deeplink.html')
def shutdown(): def shutdown():
# just wait some time to give Kubernetes time to update endpoints # just wait some time to give Kubernetes time to update endpoints
# this requires changing the readinessProbe's # this requires changing the readinessProbe's
@ -1083,28 +975,20 @@ def init_cluster():
help='Verbose logging', help='Verbose logging',
is_flag=True, is_flag=True,
) )
@option(
'--secret-key',
default='development',
envvar='SECRET_KEY',
help='Secret key for session cookies',
)
@option( @option(
'--clusters', '--clusters',
envvar='CLUSTERS', envvar='CLUSTERS',
help=f'Comma separated list of Kubernetes API server URLs (default: {DEFAULT_CLUSTERS})', # noqa help=f'Comma separated list of Kubernetes API server URLs (default: {DEFAULT_CLUSTERS})', # noqa
type=CommaSeparatedValues(), type=CommaSeparatedValues(),
) )
def main(port, secret_key, debug, clusters: list): def main(port, debug, clusters: list):
global TARGET_NAMESPACE global TARGET_NAMESPACE
basicConfig(stream=sys.stdout, level=(DEBUG if debug else INFO), format='%(asctime)s %(levelname)s: %(message)s',) basicConfig(stream=sys.stdout, level=(DEBUG if debug else INFO), format='%(asctime)s %(levelname)s: %(message)s',)
init_cluster() init_cluster()
logger.info(f'Access token URL: {ACCESS_TOKEN_URL}')
logger.info(f'App URL: {APP_URL}') logger.info(f'App URL: {APP_URL}')
logger.info(f'Authorize URL: {AUTHORIZE_URL}')
logger.info(f'Operator API URL: {OPERATOR_API_URL}') logger.info(f'Operator API URL: {OPERATOR_API_URL}')
logger.info(f'Operator cluster name label: {OPERATOR_CLUSTER_NAME_LABEL}') logger.info(f'Operator cluster name label: {OPERATOR_CLUSTER_NAME_LABEL}')
logger.info(f'Readonly mode: {"enabled" if READ_ONLY_MODE else "disabled"}') # noqa logger.info(f'Readonly mode: {"enabled" if READ_ONLY_MODE else "disabled"}') # noqa
@ -1113,7 +997,6 @@ def main(port, secret_key, debug, clusters: list):
logger.info(f'Superuser team: {SUPERUSER_TEAM}') logger.info(f'Superuser team: {SUPERUSER_TEAM}')
logger.info(f'Target namespace: {TARGET_NAMESPACE}') logger.info(f'Target namespace: {TARGET_NAMESPACE}')
logger.info(f'Teamservice URL: {TEAM_SERVICE_URL}') logger.info(f'Teamservice URL: {TEAM_SERVICE_URL}')
logger.info(f'Tokeninfo URL: {TOKENINFO_URL}')
logger.info(f'Use AWS instance_profile: {USE_AWS_INSTANCE_PROFILE}') logger.info(f'Use AWS instance_profile: {USE_AWS_INSTANCE_PROFILE}')
logger.info(f'WAL-E S3 endpoint: {WALE_S3_ENDPOINT}') logger.info(f'WAL-E S3 endpoint: {WALE_S3_ENDPOINT}')
logger.info(f'AWS S3 endpoint: {AWS_ENDPOINT}') logger.info(f'AWS S3 endpoint: {AWS_ENDPOINT}')
@ -1136,7 +1019,6 @@ def main(port, secret_key, debug, clusters: list):
logger.info(f'Target namespace set to: {TARGET_NAMESPACE or "*"}') logger.info(f'Target namespace set to: {TARGET_NAMESPACE or "*"}')
app.debug = debug app.debug = debug
app.secret_key = secret_key
signal(SIGTERM, exit_gracefully) signal(SIGTERM, exit_gracefully)

View File

@ -1,32 +0,0 @@
import os
from flask_oauthlib.client import OAuthRemoteApp
CREDENTIALS_DIR = os.getenv('CREDENTIALS_DIR', '')
class OAuthRemoteAppWithRefresh(OAuthRemoteApp):
'''Same as flask_oauthlib.client.OAuthRemoteApp, but always loads client credentials from file.'''
def __init__(self, oauth, name, **kwargs):
# constructor expects some values, so make it happy..
kwargs['consumer_key'] = 'not-needed-here'
kwargs['consumer_secret'] = 'not-needed-here'
OAuthRemoteApp.__init__(self, oauth, name, **kwargs)
def refresh_credentials(self):
with open(os.path.join(CREDENTIALS_DIR, 'authcode-client-id')) as fd:
self._consumer_key = fd.read().strip()
with open(os.path.join(CREDENTIALS_DIR, 'authcode-client-secret')) as fd:
self._consumer_secret = fd.read().strip()
@property
def consumer_key(self):
self.refresh_credentials()
return self._consumer_key
@property
def consumer_secrect(self):
self.refresh_credentials()
return self._consumer_secret

View File

@ -3,7 +3,13 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>PostgreSQL Operator UI</title> <title>PostgreSQL Operator UI</title>
<script>
const hash = localStorage.getItem("original-location-hash");
if (null !== hash) {
localStorage.removeItem("original-location-hash");
location.replace("/" + hash)
}
</script>
<!-- fonts --> <!-- fonts -->

View File

@ -1,13 +0,0 @@
<html>
<head>
<title>Storing client location ...</title>
</head>
<body>
<script language="javascript">
if (document.location.hash != null && document.location.hash != "") {
localStorage.setItem("login-url-hash", document.location.hash)
}
window.location.href = document.location.pathname + "?redirect=1"
</script>
</body>
</html>

View File

@ -1,18 +0,0 @@
<html>
<head>
<title>Restoring client location ...</title>
</head>
<body>
<script language="javascript">
// /login/authorized
hash = localStorage.getItem("login-url-hash")
if (null != hash) {
localStorage.removeItem("login-url-hash")
location.href = "/" + hash
}
else {
location.href = "/"
}
</script>
</body>
</html>

View File

@ -1,15 +1,14 @@
backoff==2.2.1 backoff==2.2.1
boto3==1.26.51 boto3==1.34.110
boto==2.49.0 boto==2.49.0
click==8.1.3 click==8.1.7
Flask-OAuthlib==0.9.6 Flask==3.0.3
Flask==2.3.2
furl==2.1.3 furl==2.1.3
gevent==23.9.1 gevent==24.2.1
jq==1.4.0 jq==1.7.0
json_delta>=2.0.2 json_delta>=2.0.2
kubernetes==11.0.0 kubernetes==11.0.0
requests==2.31.0 requests==2.32.2
stups-tokens>=1.1.19 stups-tokens>=1.1.19
wal_e==1.1.1 wal_e==1.1.1
werkzeug==2.3.3 werkzeug==3.0.3