我终于成功让 Apache Superset 将用户验证路由到 Keycloak(弹出 keycloak 登录屏幕,正在使用、验证和使用 KC 用户)。在 Keycloak 中设置与 Superset 角色完全匹配的角色,并且在 Keycloak 客户端配置中正确定义 Superset URL。但是,用户登录后,会收到错误 500 页面。
从显示的字体/图形来看,错误 500 似乎是 Supeset 错误 500(与 nginx/kc 错误相反)。
从客户端检查网络流量,我使用 oidc_callback 引用调用超集 URL:
并且 docker 日志显示对“unknonwn 主机”的引用:
superset_app | 10.30.61.186 - - [10/Aug/2023:14:09:06 +0000] "GET /superset/welcome/ HTTP/1.0" 302 201 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36"
superset_app | 10.30.61.186 - - [10/Aug/2023:14:09:06 +0000] "GET /login/ HTTP/1.0" 302 1449 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36"
superset_app | 127.0.0.1 - - [10/Aug/2023:14:09:10 +0000] "GET /health HTTP/1.1" 200 2 "-" "curl/7.74.0"
superset_app | (504, b'Unknown Host')
superset_app | Traceback (most recent call last):
superset_app | File "/usr/local/lib/python3.8/site-packages/flask/app.py", line 1517, in full_dispatch_request
superset_app | rv = self.dispatch_request()
superset_app | File "/usr/local/lib/python3.8/site-packages/flask/app.py", line 1503, in dispatch_request
superset_app | return self.ensure_sync(self.view_functions[rule.endpoint])(**req.view_args)
superset_app | File "/usr/local/lib/python3.8/site-packages/flask_oidc/__init__.py", line 657, in _oidc_callback
superset_app | plainreturn, data = self._process_callback('destination')
superset_app | File "/usr/local/lib/python3.8/site-packages/flask_oidc/__init__.py", line 689, in _process_callback
superset_app | credentials = flow.step2_exchange(code)
superset_app | File "/usr/local/lib/python3.8/site-packages/oauth2client/_helpers.py", line 133, in positional_wrapper
superset_app | return wrapped(*args, **kwargs)
superset_app | File "/usr/local/lib/python3.8/site-packages/oauth2client/client.py", line 2053, in step2_exchange
superset_app | resp, content = transport.request(
superset_app | File "/usr/local/lib/python3.8/site-packages/oauth2client/transport.py", line 280, in request
superset_app | return http_callable(uri, method=method, body=body, headers=headers,
superset_app | File "/usr/local/lib/python3.8/site-packages/httplib2/__init__.py", line 1724, in request
superset_app | (response, content) = self._request(
superset_app | File "/usr/local/lib/python3.8/site-packages/httplib2/__init__.py", line 1444, in _request
superset_app | (response, content) = self._conn_request(conn, request_uri, method, body, headers)
superset_app | File "/usr/local/lib/python3.8/site-packages/httplib2/__init__.py", line 1366, in _conn_request
superset_app | conn.connect()
superset_app | File "/usr/local/lib/python3.8/site-packages/httplib2/__init__.py", line 1156, in connect
superset_app | sock.connect((self.host, self.port))
superset_app | File "/usr/local/lib/python3.8/site-packages/httplib2/socks.py", line 504, in connect
superset_app | self.__negotiatehttp(destpair[0], destpair[1])
superset_app | File "/usr/local/lib/python3.8/site-packages/httplib2/socks.py", line 465, in __negotiatehttp
superset_app | raise HTTPError((statuscode, statusline[2]))
superset_app | httplib2.socks.HTTPError: (504, b'Unknown Host')
superset_app | 2023-08-10 14:09:32,111:ERROR:superset.views.base:(504, b'Unknown Host')
我的 superset_config.py 的授权处理部分是:
from security import OIDCSecurityManager
from flask_appbuilder.security.manager import AUTH_OID
logger.info('@keycloak config ..')
AUTH_TYPE = AUTH_OID
curr = os.path.abspath(os.getcwd())
OIDC_CLIENT_SECRETS = curr + '/docker/pythonpath_dev/client_secret.json'
OIDC_ID_TOKEN_COOKIE_SECURE = False
OIDC_REQUIRE_VERIFIED_EMAIL = False
OIDC_ID_TOKEN_COOKIE_NAME = 'pergamino'
AUTH_USER_REGISTRATION = True
#AUTH_USER_REGISTRATION_ROLE = 'Gamma'
AUTH_USER_REGISTRATION_ROLE = 'Public'
CUSTOM_SECURITY_MANAGER = OIDCSecurityManager
我的 security.py 是:
from flask import redirect, request, abort
from flask_appbuilder.security.manager import AUTH_OID
from superset.security import SupersetSecurityManager
from flask_oidc import OpenIDConnect
from flask_appbuilder.security.views import AuthOIDView
from flask_login import login_user
from urllib.parse import quote
from flask_appbuilder.views import ModelView, SimpleFormView, expose
import logging
import json
from base64 import b64decode
import os
logger = logging.getLogger()
AFIP_SERVICE_ID = os.environ.get("AFIP_SERVICE_ID", "pergamino")
class AuthOIDCView(AuthOIDView):
@expose('/login/', methods=['GET', 'POST'])
def login(self, flag=True):
sm = self.appbuilder.sm
oidc = sm.oid
@self.appbuilder.sm.oid.require_login
def handle_login():
if not oidc.get_access_token():
logger.warn('Not found access_token')
return self.logout()
roles = findAccessRoles(sm)
logger.debug('findAccessRoles %s' % roles)
if not roles or len(roles) == 0:
logger.warn('Not found roles')
return self.logout()
info = oidc.user_getinfo(
['preferred_username', 'given_name', 'family_name', 'email', 'sub'])
email = oidc.user_getfield('email')
if email is None:
logger.debug('email no informado, se intenta crear un valor')
username = oidc.user_getfield('username') or info.get('preferred_username')
email = username + "@afip.gob.ar"
user = sm.auth_user_oid(email)
if user is None:
logger.debug('crear usuario %s' % email)
user = sm.add_user(info.get('preferred_username'), info.get(
'given_name'), info.get('family_name'), email, roles[0])
if len(roles) > 1:
user.roles = roles
sm.update_user(user)
elif len(roles) != len(user.roles) or not all(e in roles for e in user.roles):
logger.debug('actualizar usuario %s' % email)
user.roles = roles
sm.update_user(user)
logger.debug('user.roles %s' % user.roles)
login_user(user, remember=False)
return redirect(self.appbuilder.get_url_for_index)
return handle_login()
@expose('/logout/', methods=['GET', 'POST'])
def logout(self):
oidc = self.appbuilder.sm.oid
oidc.logout()
super(AuthOIDCView, self).logout()
redirect_url = request.url_root.strip(
'/') + self.appbuilder.get_url_for_login
return redirect(oidc.client_secrets.get('issuer') + '/protocol/openid-connect/logout?redirect_uri=' + quote(redirect_url))
class OIDCSecurityManager(SupersetSecurityManager):
authoidview = AuthOIDCView
def __init__(self, appbuilder):
super(OIDCSecurityManager, self).__init__(appbuilder)
if self.auth_type == AUTH_OID:
self.oid = OpenIDConnect(self.appbuilder.get_app)
def findAccessRoles(sm):
try:
pre, tkn, post = sm.oid.get_access_token().split('.')
# logger.debug("get_access_token split . {},{},{}".format(pre, tkn, post))
if not tkn:
return None
idp_token_decode = decode_base64(tkn)
logger.debug('idp_token_decode %s' % idp_token_decode)
access_token = json.loads(idp_token_decode)
logger.debug('access_token in key resource_access.%s.roles' % AFIP_SERVICE_ID)
rolesAccess = access_token['resource_access'][AFIP_SERVICE_ID]['roles']
except Exception as e:
logger.error('Failed to parser access_token: '+ str(e))
return None
logger.debug('rolesAccess %s' % rolesAccess)
roles = None
if rolesAccess or len(rolesAccess) > 0:
roles = []
for r in rolesAccess:
find = sm.find_role(r.capitalize())
if find:
roles.append(find)
return roles
def decode_base64(data):
'''
FIX -> OIDC + Python3 + Incorrect padding during base64decode
'''
missing_padding = 4-len(data) % 4
if missing_padding:
data = f"{data}{'=' * missing_padding:}"
return b64decode(data)
我也面临同样的500内部服务器错误,你解决了吗?