CB-2127 security manager asynchronous authorization

This commit is contained in:
Alexander Skoblikov
2022-06-22 21:13:20 +03:00
parent 455daafce9
commit da1cddfa8d
18 changed files with 747 additions and 224 deletions
@@ -0,0 +1,25 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2022 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;
public interface CBAuthConstants {
String CB_AUTH_ID_COOKIE_NAME = "cb-auth-id";
String CB_AUTH_ID_REQUEST_PARAM = "authId";
}
@@ -0,0 +1,30 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2022 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.Nullable;
import org.jkiss.dbeaver.model.auth.SMCredentials;
import org.jkiss.dbeaver.model.auth.SMCredentialsProvider;
public class NoAuthCredentialsProvider implements SMCredentialsProvider {
@Nullable
@Override
public SMCredentials getActiveUserCredentials() {
return null;
}
}
@@ -38,4 +38,5 @@ public interface WebAppConfiguration {
boolean isResourceManagerEnabled();
boolean isFeaturesEnabled(String[] requiredFeatures);
}
@@ -45,4 +45,5 @@ public interface WebApplication extends DBPApplication {
RMController getResourceController(@NotNull SMCredentialsProvider credentialsProvider);
String getServerURL();
}
@@ -0,0 +1,28 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2022 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.model.app;
import io.cloudbeaver.auth.provider.AuthProviderConfig;
/**
* Application configuration
*/
public interface WebAuthConfiguration {
boolean isAuthProviderEnabled(String authProviderId);
AuthProviderConfig getAuthProviderConfigurations(String configId);
}
@@ -17,6 +17,7 @@
package io.cloudbeaver.model.session;
import io.cloudbeaver.DBWUserIdentity;
import io.cloudbeaver.model.WebAsyncTaskInfo;
import io.cloudbeaver.model.user.WebAuthProviderConfiguration;
import io.cloudbeaver.model.user.WebUser;
import io.cloudbeaver.model.user.WebUserOriginInfo;
@@ -46,6 +47,8 @@ public class WebAuthInfo implements SMSessionPrincipal {
private final OffsetDateTime loginTime;
private final DBWUserIdentity userIdentity;
private String message;
private String redirectLink;
private final WebAsyncTaskInfo taskInfo;
private transient Map<String, Object> userCredentials;
@@ -55,14 +58,29 @@ public class WebAuthInfo implements SMSessionPrincipal {
@NotNull AuthProviderDescriptor authProvider,
@NotNull DBWUserIdentity userIdentity,
@NotNull SMSession authSession,
@NotNull OffsetDateTime loginTime)
{
@NotNull OffsetDateTime loginTime) {
this.session = session;
this.user = user;
this.authProvider = authProvider;
this.userIdentity = userIdentity;
this.authSession = authSession;
this.loginTime = loginTime;
this.taskInfo = null;
}
public WebAuthInfo(@NotNull WebSession session,
@NotNull WebAsyncTaskInfo taskInfo,
@NotNull String redirectLink
) {
this.session = session;
this.taskInfo = taskInfo;
this.redirectLink = redirectLink;
this.user = null;
this.authProvider = null;
this.userIdentity = null;
this.authSession = null;
this.loginTime = null;
}
@Property
@@ -0,0 +1,227 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2022 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.model.session;
import io.cloudbeaver.DBWConstants;
import io.cloudbeaver.DBWUserIdentity;
import io.cloudbeaver.DBWebException;
import io.cloudbeaver.auth.SMAuthProviderExternal;
import io.cloudbeaver.model.app.WebAuthConfiguration;
import io.cloudbeaver.model.user.WebUser;
import io.cloudbeaver.utils.WebAppUtils;
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.auth.SMAuthProvider;
import org.jkiss.dbeaver.model.auth.SMAuthStatus;
import org.jkiss.dbeaver.model.auth.SMSession;
import org.jkiss.dbeaver.model.exec.DBCException;
import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor;
import org.jkiss.dbeaver.model.security.SMController;
import org.jkiss.dbeaver.registry.auth.AuthProviderDescriptor;
import org.jkiss.dbeaver.registry.auth.AuthProviderRegistry;
import org.jkiss.utils.CommonUtils;
import java.lang.reflect.InvocationTargetException;
import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Map;
public class WebSessionAuthJob extends WebAsyncTaskProcessor<SMAuthInfo> {
private static final Log log = Log.getLog(WebSessionAuthJob.class);
private static final int MAX_ATTEMPT = 10;
@NotNull
private final WebSession webSession;
@NotNull
private SMAuthInfo smAuthInfo;
@NotNull
private final SMController smController;
private boolean linkWithActiveUser;
private int attemptCounter = 0;
public WebSessionAuthJob(@NotNull WebSession webSession, @NotNull SMAuthInfo smAuthInfo, boolean linkWithActiveUser) {
this.webSession = webSession;
this.smController = webSession.getSecurityController();
this.smAuthInfo = smAuthInfo;
this.linkWithActiveUser = linkWithActiveUser;
}
@Override
public void run(DBRProgressMonitor monitor) throws InvocationTargetException, InterruptedException {
boolean resetUserStateOnError = webSession.getUser() == null;
try {
while (attemptCounter < MAX_ATTEMPT && smAuthInfo.getAuthStatus() == SMAuthStatus.IN_PROGRESS) {
attemptCounter++;
smAuthInfo = smController.getAuthStatus(smAuthInfo.getAuthAttemptId());
if (smAuthInfo.getAuthStatus() == SMAuthStatus.IN_PROGRESS) {
Thread.sleep(1000);
} else {
break;
}
}
var authStatus = smAuthInfo.getAuthStatus();
switch (authStatus) {
case SUCCESS:
finishWebSessionAuthorization(smAuthInfo);
break;
case ERROR:
throw new DBException("Authentication failed: " + smAuthInfo.getError());
case IN_PROGRESS:
throw new DBException("Authorization didn't complete within the expected period");
default:
throw new DBException("Unexpected authorization status: " + authStatus);
}
} catch (Exception e) {
if (resetUserStateOnError) {
webSession.resetUserState();
}
throw new InvocationTargetException(e);
}
}
@SuppressWarnings("unchecked")
private void finishWebSessionAuthorization(SMAuthInfo authInfo) throws DBException {
boolean configMode = WebAppUtils.getWebApplication().isConfigurationMode();
boolean resetUserStateOnError = webSession.getUser() == null;
try {
webSession.updateSMAuthInfo(authInfo);
WebUser curUser = webSession.getUser();
if (curUser == null) {
//should never happen in theory since we should get the error much earlier
throw new DBWebException("Missing user after authorization in security controller");
}
String userId = curUser.getUserId();
var securityController = webSession.getSecurityController();
Map<String, Object> providerConfig = Collections.emptyMap();
var newAuthInfos = new ArrayList<WebAuthInfo>();
for (Map.Entry<String, Object> entry : authInfo.getAuthData().entrySet()) {
String providerId = entry.getKey();
Map<String, Object> userCredentials = (Map<String, Object>) entry.getValue();
var authProviderDescriptor = getAuthProvider(providerId);
SMAuthProvider<?> authProviderInstance = authProviderDescriptor.getInstance();
SMAuthProviderExternal<?> authProviderExternal = authProviderInstance instanceof SMAuthProviderExternal<?> ?
(SMAuthProviderExternal<?>) authProviderInstance : null;
boolean providerEnabled = isProviderEnabled(providerId);
if (configMode || webSession.hasPermission(DBWConstants.PERMISSION_ADMIN)) {
// 1. Admin can authorize in any providers
// 2. When it authorizes in non-local provider for the first time we force linkUser flag
if (!providerEnabled && webSession.getUser() != null) {
linkWithActiveUser = true;
}
} else if (!providerEnabled || !webSession.hasPermission(DBWConstants.PERMISSION_ADMIN)) {
throw new DBWebException("Authentication provider '" + providerId + "' is disabled");
}
SMSession authSession;
if (configMode) {
if (webSession.getUser() != null) {
// Already logged in - remove auth token
webSession.removeAuthInfo(providerId);
webSession.resetAuthToken();
}
} else {
if (authProviderExternal != null) {
// We may need to associate new credentials with active user
if (linkWithActiveUser) {
securityController.setUserCredentials(userId, authProviderDescriptor.getId(), userCredentials);
}
}
}
if (!configMode && !webSession.isAuthorizedInSecurityManager()) {
throw new DBCException("No authorization in the security manager");
}
DBWUserIdentity userIdentity = null;
if (authProviderExternal != null) {
try {
userIdentity =
authProviderExternal.getUserIdentity(
webSession.getProgressMonitor(),
providerConfig,
userCredentials);
} catch (DBException e) {
log.debug("Error reading auth display name from provider " + providerId, e);
}
}
if (userIdentity == null) {
userIdentity = new DBWUserIdentity(userId, userId);
}
if (CommonUtils.isEmpty(curUser.getDisplayName())) {
curUser.setDisplayName(userIdentity.getDisplayName());
}
authSession = authProviderInstance.openSession(
webSession.getProgressMonitor(),
webSession,
providerConfig,
userCredentials);
if (!configMode && securityController.getUserPermissions(userId).isEmpty()) {
throw new DBWebException("Access denied (no permissions)");
}
if (!configMode && !securityController.getUserById(userId).isEnabled()) {
throw new DBWebException("User account is locked");
}
WebAuthInfo webAuthInfo = new WebAuthInfo(
webSession,
curUser,
authProviderDescriptor,
userIdentity,
authSession,
OffsetDateTime.now());
webAuthInfo.setMessage("Authenticated with " + authProviderDescriptor.getLabel() + " provider");
if (configMode) {
webAuthInfo.setUserCredentials(userCredentials);
}
webSession.addAuthInfo(webAuthInfo);
newAuthInfos.add(webAuthInfo);
}
this.extendedResults = newAuthInfos;
} catch (DBException e) {
if (resetUserStateOnError) {
webSession.resetUserState();
}
throw new DBWebException("User authentication failed", e);
}
}
private AuthProviderDescriptor getAuthProvider(String providerId) throws DBWebException {
AuthProviderDescriptor authProvider = AuthProviderRegistry.getInstance().getAuthProvider(providerId);
if (authProvider == null) {
throw new DBWebException("Invalid auth provider '" + providerId + "'");
}
return authProvider;
}
private boolean isProviderEnabled(@NotNull String providerId) {
WebAuthConfiguration appConfiguration = (WebAuthConfiguration) WebAppUtils.getWebApplication().getAppConfiguration();
return appConfiguration.isAuthProviderEnabled(providerId);
}
}
@@ -59,7 +59,7 @@ public class WebUserContext implements SMCredentialsProvider {
* refresh context state based on new token from security manager
*
* @param smAuthInfo - auth info from security manager
* @throws DBException - if user already authorized and new token
* @throws DBException - if user already authorized and new token come from another user
*/
public void refresh(SMAuthInfo smAuthInfo) throws DBException {
var isNonAnonymousUserAuthorized = isAuthorizedInSecurityManager() && getUser() != null;
@@ -67,7 +67,7 @@ public class WebUserContext implements SMCredentialsProvider {
if (isNonAnonymousUserAuthorized && !Objects.equals(getUserId(), tokenInfo.getUserId())) {
throw new DBCException("Another user is already logged in");
}
this.smCredentials = new SMCredentials(smAuthInfo.getAuthToken(), tokenInfo.getUserId());
this.smCredentials = new SMCredentials(smAuthInfo.getSmAuthToken(), tokenInfo.getUserId());
this.userPermissions = tokenInfo.getPermissions();
this.securityController = application.getSecurityController(this);
this.adminSecurityController = application.getAdminSecurityController(this);
@@ -22,6 +22,7 @@ import io.cloudbeaver.DBWFeatureSet;
import io.cloudbeaver.auth.provider.AuthProviderConfig;
import io.cloudbeaver.auth.provider.local.LocalAuthProvider;
import io.cloudbeaver.model.app.BaseWebAppConfiguration;
import io.cloudbeaver.model.app.WebAuthConfiguration;
import io.cloudbeaver.registry.WebFeatureRegistry;
import org.jkiss.code.NotNull;
import org.jkiss.code.Nullable;
@@ -39,7 +40,7 @@ import java.util.Map;
/**
* Application configuration
*/
public class CBAppConfig extends BaseWebAppConfiguration {
public class CBAppConfig extends BaseWebAppConfiguration implements WebAuthConfiguration {
public static final DataSourceNavigatorSettings DEFAULT_VIEW_SETTINGS = DataSourceNavigatorSettings.PRESET_FULL.getSettings();
private boolean supportsCustomConnections;
@@ -186,7 +187,22 @@ public class CBAppConfig extends BaseWebAppConfiguration {
}
public boolean isAuthProviderEnabled(String id) {
return ArrayUtils.contains(getEnabledAuthProviders(), id);
var authProviderDescriptor = AuthProviderRegistry.getInstance().getAuthProvider(id);
if (authProviderDescriptor == null) {
return false;
}
if (!ArrayUtils.contains(getEnabledAuthProviders(), id)) {
return false;
}
if (!ArrayUtils.isEmpty(authProviderDescriptor.getRequiredFeatures())) {
for (String rf : authProviderDescriptor.getRequiredFeatures()) {
if (!isFeatureEnabled(rf)) {
return false;
}
}
}
return true;
}
public String getDefaultAuthProvider() {
@@ -63,26 +63,30 @@ type AuthProviderInfo {
}
type UserAuthToken {
# Auth provider used for autgorization
# Auth provider used for authorization
authProvider: ID!
# Auth provider configuration ID
authConfiguration: ID
# Authorization time
loginTime: DateTime!
loginTime: DateTime
# User identity (aka user name) specific to auth provider
userId: String!
userId: String
# User display name specific to auth provider
displayName: String!
displayName: String
# Optional login message
message: String
# Auth origin
origin: ObjectOrigin!
origin: ObjectOrigin
redirectLink: String
taskInfo: AsyncTaskInfo
}
type UserInfo {
@@ -37,6 +37,7 @@ public interface DBWServiceAuth extends DBWService {
WebAuthInfo authLogin(
@NotNull WebSession webSession,
@NotNull String providerId,
@Nullable String providerConfigurationId,
@NotNull Map<String, Object> credentials,
boolean linkWithActiveUser) throws DBWebException;
@@ -81,8 +81,8 @@ public class RPSessionHandler implements DBWSessionHandler {
webSession.getProgressMonitor(), sessionParameters, credentials);
try {
SMAuthInfo smAuthInfo = securityController.authenticate(
webSession.getSessionId(), sessionParameters,
WebSession.CB_SESSION_TYPE, authProvider.getId(), userCredentials);
webSession.getSessionId(), sessionParameters,
WebSession.CB_SESSION_TYPE, authProvider.getId(), null, userCredentials);
webSession.updateSMAuthInfo(smAuthInfo);
} catch (SMException e) {
log.debug("Error during user authentication", e);
@@ -39,6 +39,7 @@ public class WebServiceBindingAuth extends WebServiceBindingBase<DBWServiceAuth>
.dataFetcher("authLogin", env -> getService(env).authLogin(
getWebSession(env, false),
env.getArgument("provider"),
env.getArgument("configuration"),
env.getArgument("credentials"),
CommonUtils.toBoolean(env.getArgument("linkUser"))))
.dataFetcher("authLogout", env -> {
@@ -16,39 +16,29 @@
*/
package io.cloudbeaver.service.auth.impl;
import io.cloudbeaver.DBWConstants;
import io.cloudbeaver.DBWUserIdentity;
import io.cloudbeaver.DBWebException;
import io.cloudbeaver.auth.SMAuthProviderExternal;
import io.cloudbeaver.auth.provider.local.LocalAuthProvider;
import io.cloudbeaver.model.WebAsyncTaskInfo;
import io.cloudbeaver.model.WebPropertyInfo;
import io.cloudbeaver.model.session.WebAuthInfo;
import io.cloudbeaver.model.session.WebSession;
import io.cloudbeaver.model.session.WebSessionAuthJob;
import io.cloudbeaver.model.user.WebAuthProviderInfo;
import io.cloudbeaver.model.user.WebUser;
import io.cloudbeaver.registry.WebUserProfileRegistry;
import io.cloudbeaver.server.CBAppConfig;
import io.cloudbeaver.server.CBApplication;
import io.cloudbeaver.service.auth.DBWServiceAuth;
import io.cloudbeaver.service.auth.WebUserInfo;
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.auth.SMAuthInfo;
import org.jkiss.dbeaver.model.auth.SMAuthProvider;
import org.jkiss.dbeaver.model.auth.SMSession;
import org.jkiss.dbeaver.model.exec.DBCException;
import org.jkiss.dbeaver.model.auth.SMAuthStatus;
import org.jkiss.dbeaver.model.security.SMController;
import org.jkiss.dbeaver.model.security.exception.SMException;
import org.jkiss.dbeaver.model.security.user.SMUser;
import org.jkiss.dbeaver.registry.auth.AuthProviderDescriptor;
import org.jkiss.dbeaver.registry.auth.AuthProviderRegistry;
import org.jkiss.utils.ArrayUtils;
import org.jkiss.utils.CommonUtils;
import java.time.OffsetDateTime;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
@@ -63,202 +53,39 @@ public class WebServiceAuthImpl implements DBWServiceAuth {
public WebAuthInfo authLogin(
@NotNull WebSession webSession,
@NotNull String providerId,
@Nullable String providerConfigurationId,
@NotNull Map<String, Object> authParameters,
boolean linkWithActiveUser) throws DBWebException {
SMController securityController = webSession.getSecurityController();
boolean linkWithActiveUser
) throws DBWebException {
if (CommonUtils.isEmpty(providerId)) {
throw new DBWebException("Missing auth provider parameter");
}
AuthProviderDescriptor authProvider = AuthProviderRegistry.getInstance().getAuthProvider(providerId);
if (authProvider == null) {
throw new DBWebException("Invalid auth provider '" + providerId + "'");
}
boolean configMode = CBApplication.getInstance().isConfigurationMode();
// Check enabled auth providers
boolean providerEnabled = isProviderEnabled(providerId, authProvider);
boolean resetUserStateOnError = webSession.getUser() == null;
SMController securityController = webSession.getSecurityController();
try {
Map<String, Object> providerConfig = Collections.emptyMap();
SMAuthProvider<?> authProviderInstance = authProvider.getInstance();
SMAuthProviderExternal<?> authProviderExternal = authProviderInstance instanceof SMAuthProviderExternal<?> ?
(SMAuthProviderExternal<?>) authProviderInstance : null;
Map<String, Object> userCredentials;
var smAuthInfo = securityController.authenticate(
webSession.getSessionId(),
webSession.getSessionParameters(),
WebSession.CB_SESSION_TYPE,
providerId,
providerConfigurationId,
authParameters
);
if (authProviderExternal != null) {
userCredentials = authProviderExternal.authExternalUser(webSession.getProgressMonitor(), providerConfig, authParameters);
if (smAuthInfo.getAuthStatus() == SMAuthStatus.IN_PROGRESS) {
//run async auth process
WebAsyncTaskInfo taskInfo = webSession.createAndRunAsyncTask("Authentication", new WebSessionAuthJob(webSession, smAuthInfo, linkWithActiveUser));
return new WebAuthInfo(webSession, taskInfo, smAuthInfo.getRedirectUrl());
} else {
// User credentials are the same as auth parameters
userCredentials = authParameters;
}
if (configMode || webSession.hasPermission(DBWConstants.PERMISSION_ADMIN)) {
// 1. Admin can authorize in any providers
// 2. When it authorizes in non-local provider for the first time we force linkUser flag
if (!providerEnabled && webSession.getUser() != null) {
linkWithActiveUser = true;
}
} else if (!providerEnabled) {
if (!isAdminAuthTry(webSession, authProvider, userCredentials)) {
throw new DBWebException("Authentication provider '" + providerId + "' is disabled");
}
}
WebUser user = null;
String userId;
SMSession authSession;
if (configMode) {
if (webSession.getUser() != null) {
// Already logged in - remove auth token
webSession.removeAuthInfo(providerId);
webSession.resetAuthToken();
}
if (authProviderExternal != null) {
userId = authProviderExternal.validateLocalAuth(
webSession.getProgressMonitor(),
securityController,
providerConfig,
userCredentials,
webSession.getUserId());
} else {
userId = CONFIG_TEMP_ADMIN_USER_ID;
}
} else {
WebUser curUser = webSession.getUser();
if (curUser == null) {
try {
SMAuthInfo smAuthInfo = securityController.authenticate(webSession.getSessionId(), webSession.getSessionParameters(), WebSession.CB_SESSION_TYPE, authProvider.getId(), userCredentials);
userId = smAuthInfo.getAuthPermissions().getUserId();
if (userId == null) {
throw new SMException("Anonymous authentication restricted");
}
webSession.updateSMAuthInfo(smAuthInfo);
curUser = webSession.getUser();
securityController = webSession.getSecurityController();
} catch (SMException e) {
log.debug("Error during user authentication", e);
throw e;
}
} else { // user already logged in
userId = curUser.getUserId();
if (authProviderExternal != null) {
// We may need to associate new credentials with active user
if (linkWithActiveUser) {
securityController.setUserCredentials(userId, authProvider.getId(), userCredentials);
}
}
}
if (linkWithActiveUser && curUser != null && !curUser.getUserId().equals(userId)) {
log.debug("Attempt to authorize user '" + userId + "' while user '" + curUser.getUserId() + "' already authorized");
throw new DBCException("You cannot authorize with different users credentials");
}
user = curUser;
}
if (user == null) {
user = new WebUser(new SMUser(userId));
}
if (!configMode && !webSession.isAuthorizedInSecurityManager()) {
throw new DBCException("No authorization in the security manager");
}
DBWUserIdentity userIdentity = null;
if (authProviderExternal != null) {
try {
userIdentity =
authProviderExternal.getUserIdentity(
webSession.getProgressMonitor(),
providerConfig,
userCredentials);
} catch (DBException e) {
log.debug("Error reading auth display name from provider " + providerId, e);
}
}
if (userIdentity == null) {
userIdentity = new DBWUserIdentity(userId, userId);
}
if (CommonUtils.isEmpty(user.getDisplayName())) {
user.setDisplayName(userIdentity.getDisplayName());
}
authSession = authProviderInstance.openSession(
webSession.getProgressMonitor(),
webSession,
providerConfig,
userCredentials);
if (!configMode && securityController.getUserPermissions(userId).isEmpty()) {
throw new DBWebException("Access denied (no permissions)");
}
if (!configMode && !securityController.getUserById(userId).isEnabled()) {
throw new DBWebException("User account is locked");
}
WebAuthInfo authInfo = new WebAuthInfo(
webSession,
user,
authProvider,
userIdentity,
authSession,
OffsetDateTime.now());
authInfo.setMessage("Authenticated with " + authProvider.getLabel() + " provider");
if (configMode) {
authInfo.setUserCredentials(userCredentials);
}
webSession.addAuthInfo(authInfo);
return authInfo;
} catch (DBException e) {
if (resetUserStateOnError) {
webSession.resetUserState();
//run it sync
var job = new WebSessionAuthJob(webSession, smAuthInfo, linkWithActiveUser);
job.run(webSession.getProgressMonitor());
//TODO return list
return ((List<WebAuthInfo>) job.getExtendedResults()).stream().findFirst().orElseThrow();
}
} catch (Exception e) {
throw new DBWebException("User authentication failed", e);
}
}
private boolean isProviderEnabled(@NotNull String providerId, AuthProviderDescriptor authProvider) {
boolean providerEnabled = true;
CBAppConfig appConfiguration = CBApplication.getInstance().getAppConfiguration();
String[] enabledAuthProviders = appConfiguration.getEnabledAuthProviders();
if (enabledAuthProviders != null && !ArrayUtils.contains(enabledAuthProviders, providerId)) {
providerEnabled = false;
} else {
if (!ArrayUtils.isEmpty(authProvider.getRequiredFeatures())) {
for (String rf : authProvider.getRequiredFeatures()) {
if (!appConfiguration.isFeatureEnabled(rf)) {
providerEnabled = false;
break;
}
}
}
}
return providerEnabled;
}
private boolean isAdminAuthTry(@NotNull WebSession session, @NotNull AuthProviderDescriptor authProvider, @NotNull Map<String, Object> userCredentials) {
SMController securityController = session.getSecurityController();
boolean isAdmin = false;
try {
SMAuthInfo authInfo = securityController.authenticate(
session.getSessionId(),
session.getSessionParameters(),
WebSession.CB_SESSION_TYPE,
authProvider.getId(),
userCredentials);
isAdmin = authInfo.getAuthPermissions().getPermissions().contains(DBWConstants.PERMISSION_ADMIN);
} catch (DBException e) {
log.error(e);
}
return isAdmin;
}
@Override
@@ -227,5 +227,34 @@ CREATE TABLE CB_AUTH_TOKEN
FOREIGN KEY (USER_ID) REFERENCES CB_USER (USER_ID) ON DELETE CASCADE
);
CREATE TABLE CB_AUTH_ATTEMPT
(
AUTH_ID VARCHAR(128) NOT NULL,
AUTH_STATUS VARCHAR(32) NOT NULL,
AUTH_ERROR TEXT NOT NULL,
APP_SESSION_ID VARCHAR(64) NOT NULL,
SESSION_ID VARCHAR(64),
SESSION_TYPE VARCHAR(64),
APP_SESSION_STATE TEXT NOT NULL,
CREATE_TIME TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (AUTH_ID),
FOREIGN KEY (SESSION_ID) REFERENCES CB_SESSION (SESSION_ID) ON DELETE CASCADE
);
CREATE TABLE CB_AUTH_ATTEMPT_INFO
(
AUTH_ID VARCHAR(128) NOT NULL,
AUTH_PROVIDER_ID VARCHAR(128) NOT NULL,
AUTH_PROVIDER_CONFIGURATION_ID VARCHAR(128) NULL,
AUTH_STATE TEXT NOT NULL,
CREATE_TIME TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (AUTH_ID, AUTH_PROVIDER_ID),
FOREIGN KEY (AUTH_ID) REFERENCES CB_AUTH_ATTEMPT (AUTH_ID) ON DELETE CASCADE
);
CREATE INDEX CB_SESSION_LOG_INDEX ON CB_SESSION_LOG(SESSION_ID,LOG_TIME);
@@ -0,0 +1,29 @@
CREATE TABLE CB_AUTH_ATTEMPT
(
AUTH_ID VARCHAR(128) NOT NULL,
AUTH_STATUS VARCHAR(32) NOT NULL,
AUTH_ERROR TEXT NOT NULL,
APP_SESSION_ID VARCHAR(64) NOT NULL,
SESSION_ID VARCHAR(64),
SESSION_TYPE VARCHAR(64),
APP_SESSION_STATE TEXT NOT NULL,
CREATE_TIME TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (AUTH_ID),
FOREIGN KEY (SESSION_ID) REFERENCES CB_SESSION (SESSION_ID) ON DELETE CASCADE
);
CREATE TABLE CB_AUTH_ATTEMPT_INFO
(
AUTH_ID VARCHAR(128) NOT NULL,
AUTH_PROVIDER_ID VARCHAR(128) NOT NULL,
AUTH_PROVIDER_CONFIGURATION_ID VARCHAR(128) NULL,
AUTH_STATE TEXT NOT NULL,
CREATE_TIME TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (AUTH_ID, AUTH_PROVIDER_ID),
FOREIGN KEY (AUTH_ID) REFERENCES CB_AUTH_ATTEMPT (AUTH_ID) ON DELETE CASCADE
);
@@ -0,0 +1,47 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2022 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.security.internal;
import org.jkiss.dbeaver.model.security.SMSessionType;
import java.util.Map;
public class AuthAttemptSessionInfo {
private final String appSessionId;
private final SMSessionType sessionType;
private final Map<String, Object> sessionParams;
public AuthAttemptSessionInfo(String appSessionId, SMSessionType sessionType, Map<String, Object> sessionParams) {
this.appSessionId = appSessionId;
this.sessionType = sessionType;
this.sessionParams = sessionParams;
}
public String getAppSessionId() {
return appSessionId;
}
public SMSessionType getSessionType() {
return sessionType;
}
public Map<String, Object> getSessionParams() {
return sessionParams;
}
}
@@ -16,8 +16,12 @@
*/
package io.cloudbeaver.service.security.internal;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import io.cloudbeaver.DBWConstants;
import io.cloudbeaver.auth.SMAuthProviderExternal;
import io.cloudbeaver.auth.SMWAuthProviderFederated;
import io.cloudbeaver.model.app.WebApplication;
import io.cloudbeaver.service.security.internal.db.CBDatabase;
import io.cloudbeaver.utils.WebAppUtils;
@@ -43,6 +47,7 @@ import org.jkiss.utils.ArrayUtils;
import org.jkiss.utils.CommonUtils;
import org.jkiss.utils.SecurityUtils;
import java.lang.reflect.Type;
import java.sql.*;
import java.time.Instant;
import java.time.LocalDateTime;
@@ -63,6 +68,9 @@ public class CBEmbeddedSecurityController implements SMAdminController {
private static final String SUBJECT_USER = "U";
private static final String SUBJECT_ROLE = "R";
private static final Type MAP_STRING_OBJECT_TYPE = new TypeToken<Map<String, Object>>() {
}.getType();
private static final Gson gson = new GsonBuilder().create();
private final CBDatabase database;
@@ -831,7 +839,7 @@ public class CBEmbeddedSecurityController implements SMAdminController {
var token = generateAuthToken(smSessionId, null, dbCon);
var permissions = getAnonymousUserPermissions();
txn.commit();
return new SMAuthInfo(token, new SMAuthPermissions(null, smSessionId, permissions));
return SMAuthInfo.success(UUID.randomUUID().toString(), token, new SMAuthPermissions(null, smSessionId, permissions), Map.of());
}
} catch (SQLException e) {
throw new DBException(e.getMessage(), e);
@@ -844,27 +852,258 @@ public class CBEmbeddedSecurityController implements SMAdminController {
}
@Override
public SMAuthInfo authenticate(@NotNull String appSessionId, @NotNull Map<String, Object> sessionParameters, @NotNull SMSessionType sessionType, @NotNull String authProviderId, @NotNull Map<String, Object> userCredentials) throws DBException {
public SMAuthInfo authenticate(
@NotNull String appSessionId,
@NotNull Map<String, Object> sessionParameters,
@NotNull SMSessionType sessionType,
@NotNull String authProviderId,
@Nullable String authProviderConfigurationId,
@NotNull Map<String, Object> userCredentials
) throws DBException {
var authProgressMonitor = new VoidProgressMonitor();
try (Connection dbCon = database.openConnection()) {
try (JDBCTransaction txn = new JDBCTransaction(dbCon)) {
var userId = findOrCreateExternalUserByCredentials(authProviderId,
sessionParameters,
userCredentials,
new VoidProgressMonitor());
if (userId == null) {
throw new SMException("Invalid user credentials");
Map<String, Object> userIdentifyingCredentials = userCredentials;
AuthProviderDescriptor authProviderDescriptor = getAuthProvider(authProviderId);
var authProviderInstance = authProviderDescriptor.getInstance();
if (SMAuthProviderExternal.class.isAssignableFrom(authProviderInstance.getClass())) {
var authProviderExternal = (SMAuthProviderExternal<?>) authProviderInstance;
userIdentifyingCredentials = authProviderExternal.authExternalUser(authProgressMonitor, Map.of(), userCredentials);
}
Map<String, Object> authData = Map.of(authProviderId, userIdentifyingCredentials);
var authAttemptId = createNewAuthAttempt(
SMAuthStatus.IN_PROGRESS,
authProviderId,
authProviderConfigurationId,
authData,
appSessionId,
sessionType,
sessionParameters
);
if (SMWAuthProviderFederated.class.isAssignableFrom(authProviderInstance.getClass())) {
//async auth
var authProviderFederated = (SMWAuthProviderFederated) authProviderInstance;
var redirectUrl = authProviderFederated.getRedirectLink(authProviderConfigurationId, Map.of());
return SMAuthInfo.inProgress(authAttemptId, redirectUrl, authData);
}
var smSessionId = createSessionIfNotExist(appSessionId, userId, sessionParameters, sessionType, dbCon);
var token = generateAuthToken(smSessionId, userId, dbCon);
var permissions = getUserPermissions(userId);
txn.commit();
return new SMAuthInfo(token, new SMAuthPermissions(userId, smSessionId, permissions));
return finishAuthentication(authAttemptId);
}
} catch (SQLException e) {
throw new DBException(e.getMessage(), e);
}
}
private String createNewAuthAttempt(
SMAuthStatus status,
String authProviderId,
String authProviderConfigurationId,
Map<String, Object> authData,
String appSessionId,
SMSessionType sessionType,
Map<String, Object> sessionParameters
) throws DBException {
String authAttemptId = UUID.randomUUID().toString();
try (Connection dbCon = database.openConnection()) {
try (JDBCTransaction txn = new JDBCTransaction(dbCon)) {
try (PreparedStatement dbStat = dbCon.prepareStatement(
"INSERT INTO CB_AUTH_ATTEMPT(AUTH_ID,AUTH_STATUS,APP_SESSION_ID,SESSION_TYPE,APP_SESSION_STATE) " +
"VALUES(?,?,?,?,?)")) {
dbStat.setString(1, authAttemptId);
dbStat.setString(2, status.toString());
dbStat.setString(3, appSessionId);
dbStat.setString(4, sessionType.getSessionType());
dbStat.setString(5, gson.toJson(sessionParameters));
dbStat.execute();
}
try (PreparedStatement dbStat = dbCon.prepareStatement(
"INSERT INTO CB_AUTH_ATTEMPT_INFO(AUTH_ID,AUTH_PROVIDER_ID,AUTH_PROVIDER_CONFIGURATION_ID,AUTH_STATE) " +
"VALUES(?,?,?,?,?)")) {
dbStat.setString(1, authAttemptId);
dbStat.setString(2, authProviderId);
dbStat.setString(3, authProviderConfigurationId);
dbStat.setString(4, gson.toJson(authData));
dbStat.execute();
}
txn.commit();
}
return authAttemptId;
} catch (SQLException e) {
throw new DBException(e.getMessage(), e);
}
}
@Override
public void updateAuthStatus(@NotNull String authId,
@NotNull SMAuthStatus authStatus,
@NotNull Map<String, Object> authInfo,
@Nullable String error) throws DBException {
try (Connection dbCon = database.openConnection()) {
try (PreparedStatement dbStat = dbCon.prepareStatement(
"UPDATE CB_AUTH_INFO SET AUTH_STATUS=?,AUTH_STATE=? WHERE AUTH_ID=?")) {
dbStat.setString(1, authStatus.toString());
dbStat.setString(2, gson.toJson(authId));
dbStat.setString(3, authId);
if (dbStat.executeUpdate() <= 0) {
throw new DBCException("Auth attempt '" + authId + "' doesn't exist");
}
}
} catch (SQLException e) {
throw new DBCException("Error reading session state", e);
}
}
@Override
public SMAuthInfo getAuthStatus(@NotNull String authId) throws DBException {
try (Connection dbCon = database.openConnection()) {
SMAuthStatus smAuthStatus;
String authError;
String smSessionId;
try (PreparedStatement dbStat = dbCon.prepareStatement(
"SELECT AUTH_STATUS,AUTH_ERROR,SESSION_ID FROM CB_AUTH_ATTEMPT WHERE AUTH_ID=?"
)) {
try (ResultSet dbResult = dbStat.executeQuery()) {
if (!dbResult.next()) {
throw new SMException("Auth attempt not found");
}
smAuthStatus = SMAuthStatus.valueOf(dbResult.getString(1));
authError = dbResult.getString(2);
smSessionId = dbResult.getString(3);
}
}
Map<String, Object> authData = new LinkedHashMap<>();
String redirectUrl = null;
try (PreparedStatement dbStat = dbCon.prepareStatement(
"SELECT AUTH_PROVIDER_ID,AUTH_PROVIDER_CONFIGURATION_ID,AUTH_STATE FROM CB_AUTH_ATTEMPT_INFO "
+ "WHERE AUTH_ID=? ORDER BY CREATE_TIME"
)) {
try (ResultSet dbResult = dbStat.executeQuery()) {
while (dbResult.next()) {
String authProviderId = dbResult.getString(1);
String authProviderConfiguration = dbResult.getString(2);
Map<String, Object> authProviderData = gson.fromJson(dbResult.getString(3), MAP_STRING_OBJECT_TYPE);
if (authProviderConfiguration != null) {
var authProviderInstance = getAuthProvider(authProviderId).getInstance();
if (SMWAuthProviderFederated.class.isAssignableFrom(authProviderInstance.getClass())) {
redirectUrl = ((SMWAuthProviderFederated) authProviderInstance).getRedirectLink(authProviderConfiguration, Map.of());
}
}
authData.put(authProviderId, authProviderData);
}
}
}
if (smAuthStatus != SMAuthStatus.SUCCESS) {
switch (smAuthStatus) {
case IN_PROGRESS:
return SMAuthInfo.inProgress(authId, redirectUrl, authData);
case ERROR:
return SMAuthInfo.error(authId, authError);
default:
throw new SMException("Unknown auth status:" + smAuthStatus);
}
}
String smToken = findTokenBySmSession(smSessionId);
return SMAuthInfo.success(authId, smToken, getTokenPermissions(smToken), authData);
} catch (SQLException e) {
throw new DBException("Error while read auth info", e);
}
}
private String findTokenBySmSession(String smSessionId) throws DBException {
try (Connection dbCon = database.openConnection();
PreparedStatement dbStat = dbCon.prepareStatement("SELECT TOKEN_ID FROM CB_AUTH_TOKEN WHERE SESSION_ID=?");
) {
dbStat.setString(1, smSessionId);
try (var dbResult = dbStat.executeQuery()) {
if (!dbResult.next()) {
throw new SMException("Token not found");
}
return dbResult.getString(1);
}
} catch (SQLException e) {
throw new DBCException("Error reading token info in database", e);
}
}
@Override
public SMAuthInfo finishAuthentication(@NotNull String authId) throws DBException {
SMAuthInfo authInfo = getAuthStatus(authId);
if (authInfo.getAuthStatus() != SMAuthStatus.IN_PROGRESS) {
throw new SMException("Authorization has already been completed with status: " + authInfo.getAuthStatus());
}
Set<String> authProviderIds = authInfo.getAuthData().keySet();
if (authProviderIds.isEmpty()) {
throw new SMException("Authorization providers are not defined");
}
String userId = null;
var finishAuthMonitor = new VoidProgressMonitor();
AuthAttemptSessionInfo authAttemptSessionInfo = readAuthAttemptSessionInfo(authId);
for (String authProviderId : authProviderIds) {
var userCredentials = (Map<String, Object>) authInfo.getAuthData().get(authProviderId);
var userIdFromCreds = findOrCreateExternalUserByCredentials(
authProviderId,
authAttemptSessionInfo.getSessionParams(),
userCredentials,
finishAuthMonitor
);
if (userId == null) {
userId = userIdFromCreds;
} else if (!userId.equals(userIdFromCreds)) {
throw new SMException("Authorization attempt contains different users");
}
}
if (userId == null) {
return SMAuthInfo.error(authId, "Invalid user credentials");
}
try (Connection dbCon = database.openConnection()) {
try (JDBCTransaction txn = new JDBCTransaction(dbCon)) {
var smSessionId = createSessionIfNotExist(
authAttemptSessionInfo.getAppSessionId(),
userId,
authAttemptSessionInfo.getSessionParams(),
authAttemptSessionInfo.getSessionType(),
dbCon
);
var token = generateAuthToken(smSessionId, userId, dbCon);
var permissions = getUserPermissions(userId);
return SMAuthInfo.success(authId, token, new SMAuthPermissions(userId, smSessionId, permissions), authInfo.getAuthData());
}
} catch (SQLException e) {
throw new SMException("Error during token generation", e);
}
}
private AuthAttemptSessionInfo readAuthAttemptSessionInfo(@NotNull String authId) throws DBException {
try (Connection dbCon = database.openConnection()) {
try (PreparedStatement dbStat = dbCon.prepareStatement(
"SELECT APP_SESSION_ID,SESSION_TYPE,APP_SESSION_STATE FROM CB_AUTH_ATTEMPT WHERE AUTH_ID=?"
)) {
dbStat.setString(1, authId);
try (ResultSet dbResult = dbStat.executeQuery()) {
if (!dbResult.next()) {
throw new SMException("Auth attempt not found");
}
String appSessionId = dbResult.getString(1);
SMSessionType sessionType = new SMSessionType(dbResult.getString(2));
Map<String, Object> sessionParams = gson.fromJson(
dbResult.getString(3), MAP_STRING_OBJECT_TYPE
);
return new AuthAttemptSessionInfo(appSessionId, sessionType, sessionParams);
}
}
} catch (SQLException e) {
throw new DBException("Error while read auth info", e);
}
}
private String findOrCreateExternalUserByCredentials(@NotNull String authProviderId,
@NotNull Map<String, Object> sessionParameters,
@NotNull Map<String, Object> userCredentials,