Как создать пользователя при авторизации через keycloak в Apache Superset
Автоматическое создание пользователя
В данном примере приведен скрипт из config файла, который автоматически создает пользователя при авторизации через keycloak. Кейс не полный, это лишь отрывок кода, но возможно кому-то поможет.
configOverrides: enable_remote_user: | from flask_appbuilder.security.manager import AUTH_REMOTE_USER from superset.security import SupersetSecurityManager from flask import redirect, g, flash, request, session from flask_appbuilder._compat import as_unicode from flask_appbuilder.security.views import expose, AuthRemoteUserView from flask_appbuilder.security.forms import LoginForm_db from flask_login import login_user class CustomRemoteUserView(AuthRemoteUserView): login_template = "appbuilder/general/security/login_db.html" @expose('/login/', methods=["GET", "POST"]) def login(self): if g.user is not None and g.user.is_authenticated: return redirect(self.appbuilder.get_url_for_index) username = request.headers.get('X-Auth-Username', None) if username: user = self.appbuilder.sm.auth_user_remote_user(username) if user is None: first_name = request.headers.get('X-Auth-Given-Name', '') last_name = request.headers.get('X-Auth-Family-Name', '') email = username user_role = self.appbuilder.sm.find_role(AUTH_NEW_USER_ROLE) user = self.appbuilder.sm.add_user(username, first_name, last_name, email, user_role) login_user(user) return redirect(self.appbuilder.get_url_for_index) else: form = LoginForm_db() if form.validate_on_submit(): user = self.appbuilder.sm.auth_user_db( form.username.data, form.password.data ) if not user: flash(as_unicode(self.invalid_login_message), 'warning') return redirect(self.appbuilder.get_url_for_login) login_user(user, remember=False) return redirect(self.appbuilder.get_url_for_index) return self.render_template( self.login_template, title=self.title, form=form, appbuilder=self.appbuilder ) class CustomSecurityManager(SupersetSecurityManager): authremoteuserview = CustomRemoteUserView ENABLE_PROXY_FIX = True AUTH_TYPE = AUTH_REMOTE_USER CUSTOM_SECURITY_MANAGER = CustomSecurityManager AUTH_USER_REGISTRATION = False AUTH_NEW_USER_ROLE = 'Public' # Workaround as defult config from chart not set redis password for celery brokers class CustomCeleryConfig(object): BROKER_URL = f"redis://:{env('REDIS_PASSWORD')}@{env('REDIS_HOST')}:{env('REDIS_PORT')}/0" CELERY_IMPORTS = ('superset.sql_lab', ) CELERY_RESULT_BACKEND = f"redis://:{env('REDIS_PASSWORD')}@{env('REDIS_HOST')}:{env('REDIS_PORT')}/0" CELERY_ANNOTATIONS = {'tasks.add': {'rate_limit': '10/s'}} CELERY_CONFIG = CustomCeleryConfig RESULTS_BACKEND = RedisCache( host=env('REDIS_HOST'), port=env('REDIS_PORT'), password=env('REDIS_PASSWORD'), key_prefix='superset_results' )
Using OpenIDKeycloak with Superset
https://www.anycodings.com/1questions/1028410/using-openidkeycloak-with-superset
Если вдруг статья не откроется - ниже копия.
Questions : Using OpenIDKeycloak with Superset
I want to use keycloak to authenticate my anycodings_apache-superset users in our Superset environment.
Superset is using flask-openid, as anycodings_apache-superset implemented in flask-security:
- http://flask-appbuilder.readthedocs.io/en/latest/_modules/flask_appbuilder/security/manager.html
- https://pythonhosted.org/Flask-OpenID/
To enable a different user authentication anycodings_apache-superset than the regular one (database), you need to anycodings_apache-superset override the AUTH_TYPE parameter in your anycodings_apache-superset superset_config.py file. You will also need anycodings_apache-superset to provide a reference to your anycodings_apache-superset openid-connect realm and enable user anycodings_apache-superset registration. As I understand, it should anycodings_apache-superset look something like this:
from flask_appbuilder.security.manager import AUTH_OID AUTH_TYPE = AUTH_OID OPENID_PROVIDERS = [ { 'name':'keycloak', 'url':'http://localhost:8080/auth/realms/superset' } ] AUTH_USER_REGISTRATION = True AUTH_USER_REGISTRATION_ROLE = 'Gamma'
With this configuration, the login page anycodings_apache-superset changes to a prompt where the user can anycodings_apache-superset select the desired OpenID provider (in our anycodings_apache-superset case keycloak). We also have two buttons, anycodings_apache-superset one to sign in (for existing users) and one anycodings_apache-superset to register as a new user.
I would expect that either of these buttons anycodings_apache-superset would take me to my keycloak login page. anycodings_apache-superset However, this does not happen. Instead, I am anycodings_apache-superset redirected right back to the login page.
In the case where I press the registration anycodings_apache-superset button, I get a message that says 'Not anycodings_apache-superset possible to register you at the moment, try anycodings_apache-superset again later'. When I press the sign in anycodings_apache-superset button, no message is displayed. The anycodings_apache-superset Superset logs show the request that loads anycodings_apache-superset the login page, but no requests to keycloak. anycodings_apache-superset I have tried the same using the Google anycodings_apache-superset OpenID provider, which works just fine.
Since I am seeing no requests to keycloak, anycodings_apache-superset this makes me think that I am either missing anycodings_apache-superset a configuration setting somewhere, or that I anycodings_apache-superset am using the wrong settings. Could you anycodings_apache-superset please help me figure out which settings I anycodings_apache-superset should be using?
PYTHONOPENID-CONNECTKEYCLOAKFLASK-SECURITYAPACHE-SUPERSET
Answers 1 : of Using OpenIDKeycloak with Superset
Update 03-02-2020
@s.j.meyer has written an updated guide anycodings_python which works with Superset 0.28.1 and up. anycodings_python I haven't tried it myself, but thanks anycodings_python @nawazxy for confirming this solution anycodings_python works.
I managed to solve my own question. The anycodings_python main problem was caused by a wrong anycodings_python assumption I made regarding the anycodings_python flask-openid plugin that superset is anycodings_python using. This plugin actually supports anycodings_python OpenID 2.x, but not OpenID-Connect anycodings_python (which is the version implemented by anycodings_python Keycloak).
As a workaround, I decided to switch to anycodings_python the flask-oidc plugin. Switching to a anycodings_python new authentication provider actually anycodings_python requires some digging work. To integrate anycodings_python the plugin, I had to follow these steps:
Configue flask-oidc for keycloak
Unfortunately, flask-oidc does not anycodings_python support the configuration format anycodings_python generated by Keycloak. Instead, your anycodings_python configuration should look something like anycodings_python this:
{ "web": { "realm_public_key": "<YOUR_REALM_PUBLIC_KEY>", "issuer": "http://<YOUR_DOMAIN>/auth/realms/<YOUR_REALM_ID>", "auth_uri": "http://<YOUR_DOMAIN>/auth/realms/<YOUR_REALM_ID>/protocol/openid-connect/auth", "client_id": "<YOUR_CLIENT_ID>", "client_secret": "<YOUR_SECRET_KEY>", "redirect_urls": [ "http://<YOUR_DOMAIN>/*" ], "userinfo_uri": "http://<YOUR_DOMAIN>/auth/realms/<YOUR_REALM_ID>/protocol/openid-connect/userinfo", "token_uri": "http://<YOUR_DOMAIN>/auth/realms/<YOUR_REALM_ID>/protocol/openid-connect/token", "token_introspection_uri": "http://<YOUR_DOMAIN>/auth/realms/<YOUR_REALM_ID>/protocol/openid-connect/token/introspect" } }
Flask-oidc expects the configuration to anycodings_python be in a file. I have stored mine in anycodings_python client_secret.json. You can configure anycodings_python the path to the configuration file in anycodings_python your superset_config.py.
Extend the Security Manager
Firstly, you will want to make sure that anycodings_python flask stops using flask-openid ad starts anycodings_python using flask-oidc instead. To do so, you anycodings_python will need to create your own security anycodings_python manager that configures flask-oidc as anycodings_python its authentication provider. I have anycodings_python implemented my security manager like anycodings_python this:
from flask_appbuilder.security.manager import AUTH_OID from flask_appbuilder.security.sqla.manager import SecurityManager from flask_oidc import OpenIDConnect class OIDCSecurityManager(SecurityManager): def __init__(self,appbuilder): super(OIDCSecurityManager, self).__init__(appbuilder) if self.auth_type == AUTH_OID: self.oid = OpenIDConnect(self.appbuilder.get_app) self.authoidview = AuthOIDCView
To enable OpenID in Superset, you would anycodings_python previously have had to set the anycodings_python authentication type to AUTH_OID. My anycodings_python security manager still executes all the anycodings_python behaviour of the super class, but anycodings_python overrides the oid attribute with the anycodings_python OpenIDConnect object. Further, it anycodings_python replaces the default OpenID anycodings_python authentication view with a custom one. I anycodings_python have implemented mine like this:
from flask_appbuilder.security.views import AuthOIDView from flask_login import login_user from urllib import quote 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(): user = sm.auth_user_oid(oidc.user_getfield('email')) if user is None: info = oidc.user_getinfo(['preferred_username', 'given_name', 'family_name', 'email']) user = sm.add_user(info.get('preferred_username'), info.get('given_name'), info.get('family_name'), info.get('email'), sm.find_role('Gamma')) 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))
My view overrides the behaviours at the anycodings_python /login and /logout endpoints. On login, anycodings_python the handle_login method is run. It anycodings_python requires the user to be authenticated by anycodings_python the OIDC provider. In our case, this anycodings_python means the user will first be redirected anycodings_python to Keycloak to log in.
On authentication, the user is anycodings_python redirected back to Superset. Next, we anycodings_python look up whether we recognize the user. anycodings_python If not, we create the user based on anycodings_python their OIDC user info. Finally, we log anycodings_python the user into Superset and redirect them anycodings_python to the landing page.
On logout, we will need to invalidate anycodings_python these cookies:
By default, Superset will only take care anycodings_python of the first. The extended logout method anycodings_python takes care of all three points.
Configure Superset
Finally, we need to add some parameters anycodings_python to our superset_config.py. This is how anycodings_python I've configured mine:
''' AUTHENTICATION ''' AUTH_TYPE = AUTH_OID OIDC_CLIENT_SECRETS = 'client_secret.json' OIDC_ID_TOKEN_COOKIE_SECURE = False OIDC_REQUIRE_VERIFIED_EMAIL = False CUSTOM_SECURITY_MANAGER = OIDCSecurityManager AUTH_USER_REGISTRATION = True AUTH_USER_REGISTRATION_ROLE = 'Gamma'
Answers 2 : of Using OpenIDKeycloak with Superset
I had some trouble with the OIDC anycodings_python library, so I configured it a bit anycodings_python differently -
In Keycloak, I created a new client with anycodings_python standard flow and confidential access. I anycodings_python also added a roles token claim in the anycodings_python mapper, so I could map "Client Roles" to anycodings_python Superset Roles.
For Superset, I mount the custom anycodings_python configuration files to my container [k8s anycodings_python in my case].
/app/pythonpath/custom_sso_security_manager.py
import logging import os import json from superset.security import SupersetSecurityManager logger = logging.getLogger('oauth_login') class CustomSsoSecurityManager(SupersetSecurityManager): def oauth_user_info(self, provider, response=None): logging.debug("Oauth2 provider: {0}.".format(provider)) logging.debug("Oauth2 oauth_remotes provider: {0}.".format(self.appbuilder.sm.oauth_remotes[provider])) if provider == 'keycloak': # Get the user info using the access token res = self.appbuilder.sm.oauth_remotes[provider].get(os.getenv('KEYCLOAK_BASE_URL') + '/userinfo') logger.info(f"userinfo response:") for attr, value in vars(res).items(): print(attr, '=', value) if res.status_code != 200: logger.error('Failed to obtain user info: %s', res._content) return #dict_str = res._content.decode("UTF-8") me = json.loads(res._content) logger.debug(" user_data: %s", me) return { 'username' : me['preferred_username'], 'name' : me['name'], 'email' : me['email'], 'first_name': me['given_name'], 'last_name': me['family_name'], 'roles': me['roles'], 'is_active': True, } def auth_user_oauth(self, userinfo): user = super(CustomSsoSecurityManager, self).auth_user_oauth(userinfo) roles = [self.find_role(x) for x in userinfo['roles']] roles = [x for x in roles if x is not None] user.roles = roles logger.debug(' Update <User: %s> role to %s', user.username, roles) self.update_user(user) # update user roles return user
and in anycodings_python /app/pythonpath/superset_config.py I anycodings_python added some configs -
from flask_appbuilder.security.manager import AUTH_OAUTH, AUTH_REMOTE_USER from custom_sso_security_manager import CustomSsoSecurityManager CUSTOM_SECURITY_MANAGER = CustomSsoSecurityManager oauthSecretPair = env('OAUTH_CLIENT_ID') + ':' + env('OAUTH_CLIENT_SECRET') AUTH_TYPE = AUTH_OAUTH OAUTH_PROVIDERS = [ { 'name':'keycloak', 'token_key':'access_token', # Name of the token in the response of access_token_url 'icon':'fa-address-card', # Icon for the provider 'remote_app': { 'api_base_url': env('KEYCLOAK_BASE_URL', 'http://CHANGEME'), 'client_id':env('OAUTH_CLIENT_ID'), # Client Id (Identify Superset application) 'client_secret':env('OAUTH_CLIENT_SECRET'), # Secret for this Client Id (Identify Superset application) 'client_kwargs':{ 'scope': 'profile' # Scope for the Authorization }, 'request_token_url':None, 'access_token_url': env('KEYCLOAK_BASE_URL', 'http://CHANGEME') + '/token', 'authorize_url': env('KEYCLOAK_BASE_URL', 'http://CHANGEME') + '/auth', } } ] # Will allow user self registration, allowing to create Flask users from Authorized User AUTH_USER_REGISTRATION = True # The default user self registration role AUTH_USER_REGISTRATION_ROLE = "Gamma" # This will make sure the redirect_uri is properly computed, even with SSL offloading ENABLE_PROXY_FIX = True
There are a few env parameters that anycodings_python these configs expect -
KEYCLOAK_BASE_URL OAUTH_CLIENT_ID OAUTH_CLIENT_SECRET
Answers 3 : of Using OpenIDKeycloak with Superset
I tried to follow the tips based on the anycodings_python comments in this post, but even so, anycodings_python there were still other doubts along the anycodings_python process, and I managed to solve the anycodings_python problem and it works perfectly, I would anycodings_python like to share the code to solve the anycodings_python problem superset-keycloak. This aprouch anycodings_python use docker to deploy the superset anycodings_python application.