diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index 7a807dd5d..fff8f8ff2 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -14,6 +14,7 @@ import types import errno import json import yaml +import copy from importlib import import_module from django.urls import reverse_lazy from urllib.parse import urljoin, urlparse @@ -348,7 +349,8 @@ class Config(dict): 'HEALTH_CHECK_TOKEN': '', } - def compatible_auth_openid_of_key(self): + @staticmethod + def convert_keycloak_to_openid(keycloak_config): """ 兼容OpenID旧配置 (即 version <= 1.5.8) 因为旧配置只支持OpenID协议的Keycloak实现, @@ -356,65 +358,79 @@ class Config(dict): 构造出新配置中标准OpenID协议中所需的Endpoint即可 (Keycloak说明文档参考: https://www.keycloak.org/docs/latest/securing_apps/) """ - if self.AUTH_OPENID and not self.AUTH_OPENID_REALM_NAME: - self['AUTH_OPENID_KEYCLOAK'] = False - if not self.AUTH_OPENID: - return + openid_config = copy.deepcopy(keycloak_config) + + auth_openid = openid_config.get('AUTH_OPENID') + auth_openid_realm_name = openid_config.get('AUTH_OPENID_REALM_NAME') + auth_openid_server_url = openid_config.get('AUTH_OPENID_SERVER_URL') - realm_name = self.AUTH_OPENID_REALM_NAME - if realm_name is None: + if not auth_openid: return - compatible_keycloak_config = [ - ( - 'AUTH_OPENID_PROVIDER_ENDPOINT', - self.AUTH_OPENID_SERVER_URL - ), - ( - 'AUTH_OPENID_PROVIDER_AUTHORIZATION_ENDPOINT', - '/realms/{}/protocol/openid-connect/auth'.format(realm_name) - ), - ( - 'AUTH_OPENID_PROVIDER_TOKEN_ENDPOINT', - '/realms/{}/protocol/openid-connect/token'.format(realm_name) - ), - ( - 'AUTH_OPENID_PROVIDER_JWKS_ENDPOINT', - '/realms/{}/protocol/openid-connect/certs'.format(realm_name) - ), - ( - 'AUTH_OPENID_PROVIDER_USERINFO_ENDPOINT', - '/realms/{}/protocol/openid-connect/userinfo'.format(realm_name) - ), - ( - 'AUTH_OPENID_PROVIDER_END_SESSION_ENDPOINT', - '/realms/{}/protocol/openid-connect/logout'.format(realm_name) - ) - ] - for key, value in compatible_keycloak_config: - self[key] = value + if auth_openid and not auth_openid_realm_name: + # 开启的是标准 OpenID 配置,关掉 Keycloak 配置 + openid_config.update({ + 'AUTH_OPENID_KEYCLOAK': False + }) - def compatible_auth_openid_of_value(self): - """ - 兼容值的绝对路径、相对路径 - (key 为 AUTH_OPENID_PROVIDER_*_ENDPOINT 的配置) - """ - if not self.AUTH_OPENID: + if auth_openid_realm_name is None: return - base = self.AUTH_OPENID_PROVIDER_ENDPOINT - config = list(self.items()) - for key, value in config: + # # convert key # # + compatible_config = { + 'AUTH_OPENID_PROVIDER_ENDPOINT': auth_openid_server_url, + + 'AUTH_OPENID_PROVIDER_AUTHORIZATION_ENDPOINT': '/realms/{}/protocol/openid-connect/auth' + ''.format(auth_openid_realm_name), + 'AUTH_OPENID_PROVIDER_TOKEN_ENDPOINT': '/realms/{}/protocol/openid-connect/token' + ''.format(auth_openid_realm_name), + 'AUTH_OPENID_PROVIDER_JWKS_ENDPOINT': '/realms/{}/protocol/openid-connect/certs' + ''.format(auth_openid_realm_name), + 'AUTH_OPENID_PROVIDER_USERINFO_ENDPOINT': '/realms/{}/protocol/openid-connect/userinfo' + ''.format(auth_openid_realm_name), + 'AUTH_OPENID_PROVIDER_END_SESSION_ENDPOINT': '/realms/{}/protocol/openid-connect/logout' + ''.format(auth_openid_realm_name) + } + for key, value in compatible_config.items(): + openid_config[key] = value + + # # convert value # # + """ 兼容值的绝对路径、相对路径 (key 为 AUTH_OPENID_PROVIDER_*_ENDPOINT 的配置) """ + base = openid_config.get('AUTH_OPENID_PROVIDER_ENDPOINT') + for key, value in openid_config.items(): result = re.match(r'^AUTH_OPENID_PROVIDER_.*_ENDPOINT$', key) if result is None: continue if value is None: # None 在 url 中有特殊含义 (比如对于: end_session_endpoint) continue + value = build_absolute_uri(base, value) + openid_config[key] = value + + return openid_config + + def get_keycloak_config(self): + keycloak_config = { + 'AUTH_OPENID': self.AUTH_OPENID, + 'AUTH_OPENID_REALM_NAME': self.AUTH_OPENID_REALM_NAME, + 'AUTH_OPENID_SERVER_URL': self.AUTH_OPENID_SERVER_URL, + 'AUTH_OPENID_PROVIDER_ENDPOINT': self.AUTH_OPENID_PROVIDER_ENDPOINT + } + return keycloak_config + + def set_openid_config(self, openid_config): + for key, value in openid_config.items(): self[key] = value + def compatible_auth_openid(self, keycloak_config=None): + if keycloak_config is None: + keycloak_config = self.get_keycloak_config() + openid_config = self.convert_keycloak_to_openid(keycloak_config) + if openid_config: + self.set_openid_config(openid_config) + def compatible(self): """ 对配置做兼容处理 @@ -424,14 +440,8 @@ class Config(dict): 处理顺序要保持先对key做处理, 再对value做处理, 因为处理value的时候,只根据最新版本支持的key进行 """ - parts = ['key', 'value'] - targets = ['auth_openid'] - for part in parts: - for target in targets: - method_name = 'compatible_{}_of_{}'.format(target, part) - method = getattr(self, method_name, None) - if method is not None: - method() + # 兼容 OpenID 配置 + self.compatible_auth_openid() def convert_type(self, k, v): default_value = self.defaults.get(k) diff --git a/apps/settings/models.py b/apps/settings/models.py index d6dc601ee..128693bb9 100644 --- a/apps/settings/models.py +++ b/apps/settings/models.py @@ -84,6 +84,7 @@ class Setting(models.Model): getattr(self.__class__, f'refresh_{self.name}')() else: setattr(settings, self.name, self.cleaned_value) + self.refresh_keycloak_to_openid_if_need() @classmethod def refresh_authentications(cls, name): @@ -129,6 +130,41 @@ class Setting(models.Model): def refresh_AUTH_OPENID(cls): cls.refresh_authentications('AUTH_OPENID') + def refresh_keycloak_to_openid_if_need(self): + watch_config_names = [ + 'AUTH_OPENID', 'AUTH_OPENID_REALM_NAME', 'AUTH_OPENID_SERVER_URL', + 'AUTH_OPENID_PROVIDER_ENDPOINT', 'AUTH_OPENID_KEYCLOAK' + ] + if self.name not in watch_config_names: + # 不在监听的配置中, 不需要刷新 + return + auth_keycloak = self.__class__.objects.filter(name='AUTH_OPENID_KEYCLOAK').first() + if not auth_keycloak or not auth_keycloak.cleaned_value: + # 关闭 Keycloak 方式的配置, 不需要刷新 + return + + from jumpserver.conf import Config + config_names = [ + 'AUTH_OPENID', 'AUTH_OPENID_REALM_NAME', + 'AUTH_OPENID_SERVER_URL', 'AUTH_OPENID_PROVIDER_ENDPOINT' + ] + # 获取当前 keycloak 配置 + keycloak_config = {} + for name in config_names: + setting = self.__class__.objects.filter(name=name).first() + if not setting: + continue + value = setting.cleaned_value + keycloak_config[name] = value + + # 转化 keycloak 配置为 openid 配置 + openid_config = Config.convert_keycloak_to_openid(keycloak_config) + if not openid_config: + return + # 刷新 settings + for key, value in openid_config.items(): + setattr(settings, key, value) + @classmethod def refresh_AUTH_RADIUS(cls): cls.refresh_authentications('AUTH_RADIUS')