[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,
Response,
abort,
redirect,
render_template,
request,
send_from_directory,
session,
)
from flask_oauthlib.client import OAuth
from functools import wraps
from gevent import sleep, spawn
from gevent.pywsgi import WSGIServer
from jq import jq
@ -34,11 +30,9 @@ from os import getenv
from re import X, compile
from requests.exceptions import RequestException
from signal import SIGTERM, signal
from urllib.parse import urljoin
from . import __version__
from .cluster_discovery import DEFAULT_CLUSTERS, StaticClusterDiscoverer
from .oauth import OAuthRemoteAppWithRefresh
from .spiloutils import (
apply_postgresql,
@ -71,11 +65,8 @@ logger = getLogger(__name__)
SERVER_STATUS = {'shutdown': False}
APP_URL = getenv('APP_URL')
AUTHORIZE_URL = getenv('AUTHORIZE_URL')
SPILO_S3_BACKUP_BUCKET = getenv('SPILO_S3_BACKUP_BUCKET')
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_CLUSTER_NAME_LABEL = getenv('OPERATOR_CLUSTER_NAME_LABEL', 'cluster-name')
@ -184,38 +175,6 @@ class WSGITransferEncodingChunked:
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):
return (
Response(
@ -297,19 +256,16 @@ STATIC_HEADERS = {
@app.route('/css/<path:path>')
@authorize
def send_css(path):
return send_from_directory('static/', path), 200, STATIC_HEADERS
@app.route('/js/<path:path>')
@authorize
def send_js(path):
return send_from_directory('static/', path), 200, STATIC_HEADERS
@app.route('/')
@authorize
def index():
return render_template('index.html', google_analytics=GOOGLE_ANALYTICS)
@ -345,7 +301,6 @@ DEFAULT_UI_CONFIG = {
@app.route('/config')
@authorize
def get_config():
config = DEFAULT_UI_CONFIG.copy()
config.update(OPERATOR_UI_CONFIG)
@ -407,17 +362,15 @@ def get_teams_for_user(user_name):
@app.route('/teams')
@authorize
def get_teams():
return ok(
get_teams_for_user(
session.get('user_name', ''),
request.headers.get('X-Uid', ''),
)
)
@app.route('/services/<namespace>/<cluster>')
@authorize
def get_service(namespace: str, cluster: str):
if TARGET_NAMESPACE not in ['', '*', namespace]:
@ -433,7 +386,6 @@ def get_service(namespace: str, cluster: str):
@app.route('/pooler/<namespace>/<cluster>')
@authorize
def get_list_poolers(namespace: str, cluster: str):
if TARGET_NAMESPACE not in ['', '*', namespace]:
@ -449,7 +401,6 @@ def get_list_poolers(namespace: str, cluster: str):
@app.route('/statefulsets/<namespace>/<cluster>')
@authorize
def get_list_clusters(namespace: str, cluster: str):
if TARGET_NAMESPACE not in ['', '*', namespace]:
@ -465,7 +416,6 @@ def get_list_clusters(namespace: str, cluster: str):
@app.route('/statefulsets/<namespace>/<cluster>/pods')
@authorize
def get_list_members(namespace: str, cluster: str):
if TARGET_NAMESPACE not in ['', '*', namespace]:
@ -485,7 +435,6 @@ def get_list_members(namespace: str, cluster: str):
@app.route('/namespaces')
@authorize
def get_namespaces():
if TARGET_NAMESPACE not in ['', '*']:
@ -503,7 +452,6 @@ def get_namespaces():
@app.route('/postgresqls')
@authorize
def get_postgresqls():
postgresqls = [
{
@ -602,7 +550,6 @@ def read_only(handler):
@app.route('/postgresqls/<namespace>/<cluster>', methods=['POST'])
@authorize
@namespaced
def update_postgresql(namespace: str, cluster: str):
if READ_ONLY_MODE:
@ -614,8 +561,8 @@ def update_postgresql(namespace: str, cluster: str):
postgresql = request.get_json(force=True)
teams = get_teams_for_user(session.get('user_name', ''))
logger.info(f'Changes to: {cluster} by {session.get("user_name", "local-user")}/{teams} {postgresql}') # noqa
teams = get_teams_for_user(request.headers.get('X-Uid', ''))
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:
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'])
@authorize
def get_postgresql(namespace: str, cluster: str):
if TARGET_NAMESPACE not in ['', '*', namespace]:
@ -826,7 +772,6 @@ def get_postgresql(namespace: str, cluster: str):
@app.route('/stored_clusters')
@authorize
def get_stored_clusters():
return respond(
read_stored_clusters(
@ -837,7 +782,6 @@ def get_stored_clusters():
@app.route('/stored_clusters/<pg_cluster>', methods=['GET'])
@authorize
def get_versions(pg_cluster: str):
return respond(
read_versions(
@ -850,9 +794,7 @@ def get_versions(pg_cluster: str):
)
@app.route('/stored_clusters/<pg_cluster>/<uid>', methods=['GET'])
@authorize
def get_basebackups(pg_cluster: str, uid: str):
return respond(
read_basebackups(
@ -867,7 +809,6 @@ def get_basebackups(pg_cluster: str, uid: str):
@app.route('/create-cluster', methods=['POST'])
@authorize
def create_new_cluster():
if READ_ONLY_MODE:
@ -885,8 +826,8 @@ def create_new_cluster():
if TARGET_NAMESPACE not in ['', '*', namespace]:
return wrong_namespace()
teams = get_teams_for_user(session.get('user_name', ''))
logger.info(f'Create cluster by {session.get("user_name", "local-user")}/{teams} {postgresql}') # noqa
teams = get_teams_for_user(request.headers.get('X-Uid', ''))
logger.info(f'Create cluster by {request.headers.get("X-Uid", "local-user")}/{teams} {postgresql}') # noqa
if SUPERUSER_TEAM and SUPERUSER_TEAM in teams:
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'])
@authorize
def delete_postgresql(namespace: str, cluster: str):
if TARGET_NAMESPACE not in ['', '*', namespace]:
return wrong_namespace()
@ -910,9 +850,9 @@ def delete_postgresql(namespace: str, cluster: str):
if postgresql is None:
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:
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')
@authorize
def get_operator_status():
return proxy_operator('/status/')
@app.route('/operator/workers/<worker>/queue')
@authorize
def get_operator_get_queue(worker: int):
return proxy_operator(f'/workers/{worker}/queue')
@app.route('/operator/workers/<worker>/logs')
@authorize
def get_operator_get_logs(worker: int):
return proxy_operator(f'/workers/{worker}/logs')
@app.route('/operator/clusters/<namespace>/<cluster>/logs')
@authorize
def get_operator_get_logs_per_cluster(namespace: str, cluster: str):
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')
def favicon():
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():
# just wait some time to give Kubernetes time to update endpoints
# this requires changing the readinessProbe's
@ -1083,28 +975,20 @@ def init_cluster():
help='Verbose logging',
is_flag=True,
)
@option(
'--secret-key',
default='development',
envvar='SECRET_KEY',
help='Secret key for session cookies',
)
@option(
'--clusters',
envvar='CLUSTERS',
help=f'Comma separated list of Kubernetes API server URLs (default: {DEFAULT_CLUSTERS})', # noqa
type=CommaSeparatedValues(),
)
def main(port, secret_key, debug, clusters: list):
def main(port, debug, clusters: list):
global TARGET_NAMESPACE
basicConfig(stream=sys.stdout, level=(DEBUG if debug else INFO), format='%(asctime)s %(levelname)s: %(message)s',)
init_cluster()
logger.info(f'Access token URL: {ACCESS_TOKEN_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 cluster name label: {OPERATOR_CLUSTER_NAME_LABEL}')
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'Target namespace: {TARGET_NAMESPACE}')
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'WAL-E S3 endpoint: {WALE_S3_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 "*"}')
app.debug = debug
app.secret_key = secret_key
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>
<meta charset="utf-8">
<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 -->

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
boto3==1.26.51
boto3==1.34.110
boto==2.49.0
click==8.1.3
Flask-OAuthlib==0.9.6
Flask==2.3.2
click==8.1.7
Flask==3.0.3
furl==2.1.3
gevent==23.9.1
jq==1.4.0
gevent==24.2.1
jq==1.7.0
json_delta>=2.0.2
kubernetes==11.0.0
requests==2.31.0
requests==2.32.2
stups-tokens>=1.1.19
wal_e==1.1.1
werkzeug==2.3.3
werkzeug==3.0.3