August 26, 2022

Как создать пользователя при авторизации через 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:

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

Total Answers 3

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:

  1. The superset session
  2. The OIDC token
  3. The cookies set by Keycloak

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'

0

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

0

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.