Cb 4389 convert reverse proxy (#2300)

* CB-4389. Converted reverse proxy to identity provider

* CB-4039 build logout redirect link on backend side

* CB-4039 fix return value

* CB-4389. Added auto configuration for Reverse proxy if its enabled on ce, fixed typo, rename plugin parameters

* CB-4389. Added check for already exist configuration

* CB-4389. Refactor after review

* CB-4039 adds logout with context for Okta Identity Provider

* CB-4039 backward compatibility fix

* CB-4039 add since annotation

* CB-4039 api return type fix

* CB-4039 logout extended

* CB-4389. Refactor after review

* CB-4389. Refactor after review

* CB-4039 okta logout redirect moved to AuthenticationService

* CB-4039 remove windowService dep

* CB-4039 removes old logic of identity provider logout links

* CB-4039 okta review changes

* CB-4389. Merge logout logic into branch

* CB-4389. Delete param after review

* CB-4389. Converted reverse proxy to identity provider

* CB-4389. Added auto configuration for Reverse proxy if its enabled on ce, fixed typo, rename plugin parameters

* CB-4389. Added check for already exist configuration

* CB-4389. Refactor after review

* CB-4389. Refactor after review

* CB-4389. Refactor after review

* CB-4039 adds logout with context for Okta Identity Provider

* CB-4039 okta logout redirect moved to AuthenticationService

* CB-4389. Merge logout logic into branch

* CB-4389. Delete param after review

* CB-4546 shows hex/base64 only on blob column selection (#2314)

Co-authored-by: s.teleshev <s.teleshev@mu.se>
Co-authored-by: Evgenia Bezborodova <139753579+EvgeniaBzzz@users.noreply.github.com>
Co-authored-by: Daria Marutkina <125263541+dariamarutkina@users.noreply.github.com>

* CB-4389. Fix logout redirect and npe for logout parameter

* CB-4389. Fix logout redirect and npe for logout parameter

* CB-4389. Change version

---------

Co-authored-by: Aleksandr Skoblikov <aleksandr.skoblikov@dbeaver.com>
Co-authored-by: s.teleshev <s.teleshev@mu.se>
Co-authored-by: Daria Marutkina <125263541+dariamarutkina@users.noreply.github.com>
Co-authored-by: Evgenia Bezborodova <139753579+EvgeniaBzzz@users.noreply.github.com>
Co-authored-by: sergeyteleshev <iamsergeyteleshev@gmail.com>
This commit is contained in:
DenisSinelnikov
2024-01-23 19:15:34 +03:00
committed by GitHub
co-authored by s.teleshev Evgenia Bezborodova Daria Marutkina Aleksandr Skoblikov sergeyteleshev
parent f4fd603297
commit 4cdb3ac30b
11 changed files with 262 additions and 27 deletions
@@ -27,17 +27,12 @@ import java.util.Map;
* Federated auth provider.
* Provides links to external auth resource
*/
public interface SMAuthProviderFederated {
public interface SMAuthProviderFederated extends SMSignOutLinkProvider {
@NotNull
String getSignInLink(String id, @NotNull Map<String, Object> providerConfig) throws DBException;
/**
* @return a common link for logout, not related with the user context
*/
@NotNull
String getCommonSignOutLink(String id, @NotNull Map<String, Object> providerConfig) throws DBException;
@Override
default String getUserSignOutLink(
@NotNull SMAuthProviderCustomConfiguration providerConfig,
@NotNull Map<String, Object> userCredentials
@@ -0,0 +1,37 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2024 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.cloudbeaver.auth;
import org.jkiss.code.NotNull;
import org.jkiss.dbeaver.DBException;
import org.jkiss.dbeaver.model.security.SMAuthProviderCustomConfiguration;
import java.util.Map;
public interface SMSignOutLinkProvider {
/**
* @return a common link for logout, not related with the user context
*/
@NotNull
String getCommonSignOutLink(String id, @NotNull Map<String, Object> providerConfig) throws DBException;
String getUserSignOutLink(
@NotNull SMAuthProviderCustomConfiguration providerConfig,
@NotNull Map<String, Object> userCredentials
) throws DBException;
}
@@ -18,6 +18,7 @@ package io.cloudbeaver.registry;
import io.cloudbeaver.auth.CBAuthConstants;
import io.cloudbeaver.auth.SMAuthProviderFederated;
import io.cloudbeaver.auth.SMSignOutLinkProvider;
import io.cloudbeaver.utils.WebAppUtils;
import org.jkiss.dbeaver.DBException;
import org.jkiss.dbeaver.Log;
@@ -85,8 +86,8 @@ public class WebAuthProviderConfiguration {
@Property
public String getSignOutLink() throws DBException {
SMAuthProvider<?> instance = providerDescriptor.getInstance();
return instance instanceof SMAuthProviderFederated
? ((SMAuthProviderFederated) instance).getCommonSignOutLink(getId(), config.getParameters())
return instance instanceof SMSignOutLinkProvider
? ((SMSignOutLinkProvider) instance).getCommonSignOutLink(getId(), config.getParameters())
: null;
}
@@ -7,6 +7,9 @@
<service id="auth" label="User authentication" description="User authentication services" class="io.cloudbeaver.service.auth.WebServiceBindingAuth">
</service>
<service id="reverse.configurator" label="Reverse Proxy Configurator" description="Reverse proxy configurator"
class="io.cloudbeaver.service.auth.ReverseProxyConfigurator">
</service>
</extension>
<extension point="io.cloudbeaver.handler">
<sessionHandler id="RPSessionHandler" class="io.cloudbeaver.service.auth.RPSessionHandler"/>
@@ -0,0 +1,26 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2024 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.cloudbeaver.service.auth;
interface RPConstants {
String PARAM_LOGOUT_URL = "logout-url";
String PARAM_USER = "user-header";
String PARAM_TEAM = "team-header";
String PARAM_FIRST_NAME = "first-name-header";
String PARAM_LAST_NAME = "last-name-header";
String PARAM_ROLE_NAME = "role-header";
}
@@ -33,6 +33,7 @@ import org.jkiss.code.NotNull;
import org.jkiss.dbeaver.DBException;
import org.jkiss.dbeaver.Log;
import org.jkiss.dbeaver.model.auth.SMAuthInfo;
import org.jkiss.dbeaver.model.security.SMAuthProviderCustomConfiguration;
import org.jkiss.dbeaver.model.security.SMConstants;
import org.jkiss.dbeaver.model.security.SMController;
import org.jkiss.dbeaver.model.security.SMStandardMeta;
@@ -41,10 +42,10 @@ import org.jkiss.utils.CommonUtils;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
public class RPSessionHandler implements DBWSessionHandler {
@@ -62,22 +63,38 @@ public class RPSessionHandler implements DBWSessionHandler {
return false;
}
public void reverseProxyAuthentication(@NotNull HttpServletRequest request, @NotNull WebSession webSession) throws DBWebException {
public void reverseProxyAuthentication(@NotNull HttpServletRequest request, @NotNull WebSession webSession) throws DBException {
SMController securityController = webSession.getSecurityController();
WebAuthProviderDescriptor authProvider = WebAuthProviderRegistry.getInstance().getAuthProvider(RPAuthProvider.AUTH_PROVIDER);
if (authProvider == null) {
throw new DBWebException("Auth provider " + RPAuthProvider.AUTH_PROVIDER + " not found");
}
SMAuthProviderExternal<?> authProviderExternal = (SMAuthProviderExternal<?>) authProvider.getInstance();
String userName = request.getHeader(RPAuthProvider.X_USER);
String teams = request.getHeader(RPAuthProvider.X_TEAM);
SMAuthProviderCustomConfiguration configuration = WebAppUtils.getWebAuthApplication()
.getAuthConfiguration()
.getAuthCustomConfigurations()
.stream()
.filter(p -> p.getProvider().equals(authProvider.toString()))
.findFirst()
.orElse(null);
Map<String, Object> paramConfigMap = new HashMap<>();
if (configuration != null) {
authProvider.getConfigurationParameters().forEach(p ->
paramConfigMap.put(p.getId(), configuration.getParameters().get(p.getId())
));
}
String userName = request.getHeader(
resolveParam(paramConfigMap.get(RPConstants.PARAM_USER), RPAuthProvider.X_USER)
);
String teams = request.getHeader(resolveParam(paramConfigMap.get(RPConstants.PARAM_TEAM), RPAuthProvider.X_TEAM));
if (CommonUtils.isEmpty(teams)) {
// backward compatibility
teams = request.getHeader(RPAuthProvider.X_ROLE);
}
String role = request.getHeader(RPAuthProvider.X_ROLE_TE);
String firstName = request.getHeader(RPAuthProvider.X_FIRST_NAME);
String lastName = request.getHeader(RPAuthProvider.X_LAST_NAME);
String role = request.getHeader(resolveParam(paramConfigMap.get(RPConstants.PARAM_ROLE_NAME), RPAuthProvider.X_ROLE_TE));
String firstName = request.getHeader(resolveParam(paramConfigMap.get(RPConstants.PARAM_FIRST_NAME), RPAuthProvider.X_FIRST_NAME));
String lastName = request.getHeader(resolveParam(paramConfigMap.get(RPConstants.PARAM_LAST_NAME), RPAuthProvider.X_LAST_NAME));
String logoutUrl = Objects.requireNonNull(configuration).getParameter(RPConstants.PARAM_LOGOUT_URL);
List<String> userTeams = teams == null ? Collections.emptyList() : List.of(teams.split("\\|"));
if (userName != null) {
try {
@@ -89,6 +106,9 @@ public class RPSessionHandler implements DBWSessionHandler {
if (!CommonUtils.isEmpty(lastName)) {
credentials.put(SMStandardMeta.META_LAST_NAME, lastName);
}
if (CommonUtils.isNotEmpty(logoutUrl)) {
credentials.put("logoutUrl", logoutUrl);
}
Map<String, Object> sessionParameters = webSession.getSessionParameters();
sessionParameters.put(SMConstants.SESSION_PARAM_TRUSTED_USER_TEAMS, userTeams);
sessionParameters.put(SMConstants.SESSION_PARAM_TRUSTED_USER_ROLE, role);
@@ -102,7 +122,7 @@ public class RPSessionHandler implements DBWSessionHandler {
webSession.getSessionId(),
currentSmSessionId,
sessionParameters,
WebSession.CB_SESSION_TYPE, authProvider.getId(), null, userCredentials);
WebSession.CB_SESSION_TYPE, authProvider.getId(), configuration.getId(), userCredentials);
new WebSessionAuthProcessor(webSession, smAuthInfo, false).authenticateSession();
log.debug(MessageFormat.format(
"Successful reverse proxy authentication: user ''{0}'' with teams {1}", userName, userTeams));
@@ -120,4 +140,11 @@ public class RPSessionHandler implements DBWSessionHandler {
public boolean handleSessionClose(WebSession webSession) throws DBException, IOException {
return false;
}
private String resolveParam(Object value, String defaultValue) {
if (value != null && !value.toString().isEmpty()) {
return value.toString();
}
return defaultValue;
}
}
@@ -0,0 +1,110 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2024 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.cloudbeaver.service.auth;
import io.cloudbeaver.auth.provider.rp.RPAuthProvider;
import io.cloudbeaver.model.app.WebAppConfiguration;
import io.cloudbeaver.model.app.WebApplication;
import io.cloudbeaver.model.app.WebAuthApplication;
import io.cloudbeaver.model.session.WebSession;
import io.cloudbeaver.service.DBWServiceServerConfigurator;
import org.jkiss.code.NotNull;
import org.jkiss.code.Nullable;
import org.jkiss.dbeaver.DBException;
import org.jkiss.dbeaver.Log;
import org.jkiss.dbeaver.model.security.SMAuthProviderCustomConfiguration;
import java.util.HashMap;
import java.util.Map;
public class ReverseProxyConfigurator implements DBWServiceServerConfigurator {
private static final Log log = Log.getLog(ReverseProxyConfigurator.class);
@Override
public void configureServer(
@NotNull WebApplication application,
@Nullable WebSession session,
@NotNull WebAppConfiguration appConfig
) throws DBException {
}
@Override
public void migrateConfigurationIfNeeded(@NotNull WebApplication application) throws DBException {
if (migrationNotNeeded(application)) {
return;
}
migrateConfiguration(application);
}
@Override
public void reloadConfiguration(@NotNull WebAppConfiguration appConfig) throws DBException {
}
private void migrateConfiguration(
@NotNull WebApplication application
) {
if (!(application instanceof WebAuthApplication authApplication)) {
return;
}
SMAuthProviderCustomConfiguration smReverseProxyProviderConfiguration =
authApplication.getAuthConfiguration().getAuthProviderConfiguration(RPAuthProvider.AUTH_PROVIDER);
if (smReverseProxyProviderConfiguration == null) {
smReverseProxyProviderConfiguration = new SMAuthProviderCustomConfiguration(RPAuthProvider.AUTH_PROVIDER);
smReverseProxyProviderConfiguration.setProvider(RPAuthProvider.AUTH_PROVIDER);
smReverseProxyProviderConfiguration.setDisplayName("Reverse Proxy");
smReverseProxyProviderConfiguration.setDescription(
"Automatically created provider after changing Reverse Proxy configuration way in 23.3.4 version"
);
smReverseProxyProviderConfiguration .setIconURL("");
Map<String, Object> parameters = new HashMap<>();
parameters.put(RPConstants.PARAM_USER, RPAuthProvider.X_USER);
parameters.put(RPConstants.PARAM_TEAM, RPAuthProvider.X_TEAM);
parameters.put(RPConstants.PARAM_FIRST_NAME, RPAuthProvider.X_FIRST_NAME);
parameters.put(RPConstants.PARAM_LAST_NAME, RPAuthProvider.X_LAST_NAME);
smReverseProxyProviderConfiguration.setParameters(parameters);
authApplication.getAuthConfiguration().addAuthProviderConfiguration(smReverseProxyProviderConfiguration );
try {
authApplication.flushConfiguration();
} catch (Exception e) {
log.error("Failed to save server configuration", e);
}
}
}
private boolean migrationNotNeeded(@NotNull WebApplication application) {
if (!(application instanceof WebAuthApplication authApplication)) {
return true;
}
if (!authApplication.getAuthConfiguration().isAuthProviderEnabled(RPAuthProvider.AUTH_PROVIDER)) {
log.debug("Reverse proxy provider disabled, migration not needed");
return true;
}
boolean isReverseProxyConfigured = authApplication.getAuthConfiguration()
.getAuthCustomConfigurations().stream()
.anyMatch(p -> p.getProvider().equals(RPAuthProvider.AUTH_PROVIDER));
if (isReverseProxyConfigured) {
log.debug("Reverse proxy provider already exist, migration not needed");
return true;
}
return false;
}
}
@@ -19,6 +19,7 @@ package io.cloudbeaver.service.auth.impl;
import io.cloudbeaver.DBWebException;
import io.cloudbeaver.WebServiceUtils;
import io.cloudbeaver.auth.SMAuthProviderFederated;
import io.cloudbeaver.auth.SMSignOutLinkProvider;
import io.cloudbeaver.auth.provider.local.LocalAuthProvider;
import io.cloudbeaver.model.WebPropertyInfo;
import io.cloudbeaver.model.session.WebAuthInfo;
@@ -148,8 +149,8 @@ public class WebServiceAuthImpl implements DBWServiceAuth {
var cbApp = CBApplication.getInstance();
for (WebAuthInfo removedInfo : removedInfos) {
if (removedInfo.getAuthProviderDescriptor()
.getInstance() instanceof SMAuthProviderFederated federatedProvider
&& removedInfo.getAuthSession() instanceof SMSessionExternal externalSession
.getInstance() instanceof SMSignOutLinkProvider provider
&& removedInfo.getAuthSession() != null
) {
var providerConfig =
cbApp.getAuthConfiguration().getAuthProviderConfiguration(removedInfo.getAuthConfiguration());
@@ -157,9 +158,17 @@ public class WebServiceAuthImpl implements DBWServiceAuth {
log.warn(removedInfo.getAuthConfiguration() + " provider configuration wasn't found");
continue;
}
String logoutUrl = federatedProvider.getUserSignOutLink(providerConfig,
externalSession.getAuthParameters());
logoutUrls.add(logoutUrl);
String logoutUrl;
if (removedInfo.getAuthSession() instanceof SMSessionExternal externalSession) {
logoutUrl = provider.getUserSignOutLink(providerConfig,
externalSession.getAuthParameters());
} else {
logoutUrl = provider.getUserSignOutLink(providerConfig,
Map.of());
}
if (CommonUtils.isNotEmpty(logoutUrl)) {
logoutUrls.add(logoutUrl);
}
}
}
return new WebLogoutInfo(logoutUrls);
@@ -18,6 +18,7 @@
<authProvider id="reverseProxy" label="Reverse proxy"
description="Reverse proxy header based authentication"
trusted="true"
configurable="true"
class="io.cloudbeaver.auth.provider.rp.RPAuthProvider"
icon="platform:/plugin/org.jkiss.dbeaver.model/icons/tree/key.png">
<credentials>
@@ -25,6 +26,16 @@
<property id="user" label="User name" type="string" description="User name" admin="true" user="true" identifying="true"/>
</propertyGroup>
</credentials>
<configuration>
<propertyGroup label="Configuration">
<property id="logout-url" label="Logout url" type="string" description="Logout url"/>
<property id="user-header" label="User header name" type="string" description="User header name"/>
<property id="team-header" label="Team name" type="string" description="Team header name"/>
<property id="first-name-header" label="First name header" type="string" description="First name header name"/>
<property id="last-name-header" label="Last name header" type="string" description="Last name header name"/>
<property id="role-header" label="Role header" type="string" description="Role header name" features="distributed"/>
</propertyGroup>
</configuration>
</authProvider>
</extension>
</plugin>
@@ -18,6 +18,7 @@ package io.cloudbeaver.auth.provider.rp;
import io.cloudbeaver.DBWUserIdentity;
import io.cloudbeaver.auth.SMAuthProviderExternal;
import io.cloudbeaver.auth.SMSignOutLinkProvider;
import io.cloudbeaver.auth.provider.local.LocalAuthSession;
import io.cloudbeaver.model.session.WebSession;
import io.cloudbeaver.model.user.WebUser;
@@ -37,7 +38,7 @@ import org.jkiss.utils.CommonUtils;
import java.util.HashMap;
import java.util.Map;
public class RPAuthProvider implements SMAuthProviderExternal<SMSession> {
public class RPAuthProvider implements SMAuthProviderExternal<SMSession>, SMSignOutLinkProvider {
private static final Log log = Log.getLog(RPAuthProvider.class);
@@ -49,6 +50,7 @@ public class RPAuthProvider implements SMAuthProviderExternal<SMSession> {
public static final String X_FIRST_NAME = "X-First-name";
public static final String X_LAST_NAME = "X-Last-name";
public static final String AUTH_PROVIDER = "reverseProxy";
public static final String LOGOUT_URL = "logout-url";
@NotNull
@Override
@@ -122,4 +124,18 @@ public class RPAuthProvider implements SMAuthProviderExternal<SMSession> {
public void refreshSession(@NotNull DBRProgressMonitor monitor, @NotNull SMSession mainSession, SMSession session) throws DBException {
}
@NotNull
@Override
public String getCommonSignOutLink(String id, @NotNull Map<String, Object> providerConfig) throws DBException {
return providerConfig.get(LOGOUT_URL) != null ? providerConfig.get(LOGOUT_URL).toString() : "";
}
@Override
public String getUserSignOutLink(@NotNull SMAuthProviderCustomConfiguration providerConfig, @NotNull Map<String, Object> userCredentials) throws DBException {
return providerConfig.getParameters().get(LOGOUT_URL) != null ?
providerConfig.getParameters().get(LOGOUT_URL).toString() :
null;
}
}
@@ -1346,7 +1346,7 @@ public class CBEmbeddedSecurityController<T extends WebAuthApplication>
String signInLink = buildRedirectLink(authProviderFederated.getSignInLink(authProviderConfigurationId, Map.of()),
authAttemptId);
String signOutLink = authProviderFederated.getCommonSignOutLink(authProviderConfigurationId,
Map.of());
providerConfig.getParameters());
Map<SMAuthConfigurationReference, Object> authData = Map.of(new SMAuthConfigurationReference(authProviderId,
authProviderConfigurationId), filteredUserCreds);
return SMAuthInfo.inProgress(authAttemptId, signInLink, signOutLink, authData, isMainSession);