mirror of
https://github.com/zalando/postgres-operator.git
synced 2026-09-30 09:17:15 +02:00
[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:
+8
-126
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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 -->
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
+7
-8
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user