Использование OpenID/Keycloak с Superset

Я хочу использовать keycloak для аутентификации моих пользователей в нашей среде Superset.

Superset использует flask-openid, как это реализовано в флеш-безопасности:

Чтобы включить аутентификацию пользователя, отличную от обычной (база данных), вам необходимо переопределить параметр AUTH_TYPE в файле superset_config.py. Вам также нужно будет предоставить ссылку на вашу область openid-connect и включить регистрацию пользователей. Как я понимаю, это должно выглядеть примерно так:

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'

При такой конфигурации страница входа меняется на приглашение, где пользователь может выбрать нужного поставщика OpenID (в нашем случае keycloak). У нас также есть две кнопки, одна для входа (для существующих пользователей) и одна для регистрации в качестве нового пользователя.

Я ожидаю, что любая из этих кнопок приведет меня на мою страницу входа в Keycloak. Однако этого не происходит. Вместо этого я перенаправлен обратно на страницу входа.

В случае, когда я нажимаю кнопку регистрации, я получаю сообщение "Невозможно зарегистрировать вас в данный момент, повторите попытку позже". Когда я нажимаю кнопку входа, сообщение не отображается. Журналы Superset показывают запрос, который загружает страницу входа, но не запросы к keycloak. Я попробовал то же самое с помощью провайдера Google OpenID, который работает просто отлично.

Так как я не вижу запросов к keycloak, это заставляет меня думать, что я либо пропускаю настройки конфигурации где-то, либо использую неправильные настройки. Не могли бы вы помочь мне выяснить, какие настройки я должен использовать?

3 ответа

Решение

Мне удалось решить свой вопрос. Основная проблема была вызвана неправильным предположением, которое я сделал относительно плагина колба-openid, который использует суперсет. Этот плагин на самом деле поддерживает OpenID 2.x, но не OpenID-Connect (это версия, реализованная Keycloak).

В качестве обходного пути я решил переключиться на плагин flask-oidc. Переключение на нового провайдера аутентификации на самом деле требует определенных усилий по копанию. Чтобы интегрировать плагин, мне пришлось выполнить следующие шаги:

Настроить колбу-oidc для брелка

К сожалению, flask-oidc не поддерживает формат конфигурации, сгенерированный Keycloak. Вместо этого ваша конфигурация должна выглядеть примерно так:

{
    "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 ожидает, что конфигурация будет в файле. Я сохранил мой в client_secret.json, Вы можете настроить путь к файлу конфигурации в вашем superset_config.py,

Расширить диспетчер безопасности

Во-первых, вы должны убедиться, что фляга перестает использовать флеш-openid, и вместо нее начинает работать фляга-oidc. Для этого вам нужно будет создать собственный менеджер безопасности, который настраивает flask-oidc в качестве поставщика аутентификации. Я реализовал свой менеджер безопасности следующим образом:

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

Чтобы включить OpenID в Superset, ранее вам нужно было установить тип аутентификации AUTH_OID. Мой менеджер безопасности по-прежнему выполняет все поведение суперкласса, но переопределяет атрибут oid с объектом OpenIDConnect. Кроме того, он заменяет стандартное представление проверки подлинности OpenID пользовательским. Я реализовал мой так:

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))

Мое представление переопределяет поведение в конечных точках /login и /logout. При входе в систему запускается метод handle_login. Требуется, чтобы пользователь прошел проверку подлинности поставщиком OIDC. В нашем случае это означает, что пользователь сначала будет перенаправлен на Keycloak для входа.

При аутентификации пользователь перенаправляется обратно в Superset. Далее мы смотрим, узнаем ли мы пользователя. Если нет, мы создаем пользователя на основе его информации о пользователе OIDC. Наконец, мы регистрируем пользователя в Superset и перенаправляем его на целевую страницу.

При выходе из системы нам потребуется аннулировать эти файлы cookie:

  1. Суперсет сессия
  2. Токен OIDC
  3. Печенье от Keycloak

По умолчанию Superset позаботится только о первом. Расширенный метод выхода из системы учитывает все три момента.

Настроить суперсет

Наконец, нам нужно добавить некоторые параметры в наш superset_config.py, Вот как я настроил мой:

'''
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'

У меня были проблемы с библиотекой OIDC, поэтому я настроил ее немного иначе -

В Keycloak я создал новый client с участием standard flow а также confidentialдоступ.
Я также добавил roles утверждение токена в картографе, чтобы я мог сопоставить «Роли клиента» с ролями надмножества.

Для Superset я монтирую пользовательские файлы конфигурации в свой контейнер [k8s в моем случае].

/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

И в /app/pythonpath/superset_config.py Я добавил несколько конфигов -

      
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

Эти конфигурации ожидают несколько параметров env:

      KEYCLOAK_BASE_URL
OAUTH_CLIENT_ID
OAUTH_CLIENT_SECRET

Я пытался следовать советам, основанным на комментариях в этом посте, но даже в этом случае в процессе все еще были другие сомнения, и мне удалось решить проблему, и она отлично работает, я хотел бы поделиться кодом для решения проблемы. суперсет-keycloak. Этот подход использует докер для развертывания расширенного приложения.

Другие вопросы по тегам