From da1cddfa8d11225f503a77456fed1ebbcb64e7cc Mon Sep 17 00:00:00 2001 From: Alexander Skoblikov Date: Wed, 22 Jun 2022 21:06:31 +0300 Subject: [PATCH] CB-2127 security manager asynchronous authorization --- .../io/cloudbeaver/auth/CBAuthConstants.java | 25 ++ .../auth/NoAuthCredentialsProvider.java | 30 ++ .../model/app/WebAppConfiguration.java | 1 + .../cloudbeaver/model/app/WebApplication.java | 1 + .../model/app/WebAuthConfiguration.java | 28 ++ .../model/session/WebAuthInfo.java | 22 +- .../model/session/WebSessionAuthJob.java | 227 +++++++++++++++ .../model/session/WebUserContext.java | 4 +- .../io/cloudbeaver/server/CBAppConfig.java | 20 +- .../schema/service.auth.graphqls | 14 +- .../service/auth/DBWServiceAuth.java | 1 + .../service/auth/RPSessionHandler.java | 4 +- .../service/auth/WebServiceBindingAuth.java | 1 + .../service/auth/impl/WebServiceAuthImpl.java | 225 ++------------- .../db/cb_schema_create.sql | 29 ++ .../db/cb_schema_update_7.sql | 29 ++ .../internal/AuthAttemptSessionInfo.java | 47 ++++ .../CBEmbeddedSecurityController.java | 263 +++++++++++++++++- 18 files changed, 747 insertions(+), 224 deletions(-) create mode 100644 server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/CBAuthConstants.java create mode 100644 server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/NoAuthCredentialsProvider.java create mode 100644 server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebAuthConfiguration.java create mode 100644 server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSessionAuthJob.java create mode 100644 server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_7.sql create mode 100644 server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/internal/AuthAttemptSessionInfo.java diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/CBAuthConstants.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/CBAuthConstants.java new file mode 100644 index 0000000000..3d4814a658 --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/CBAuthConstants.java @@ -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"; + +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/NoAuthCredentialsProvider.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/NoAuthCredentialsProvider.java new file mode 100644 index 0000000000..a360ab34f5 --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/NoAuthCredentialsProvider.java @@ -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; + } +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebAppConfiguration.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebAppConfiguration.java index 11bf846170..465a8b9a18 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebAppConfiguration.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebAppConfiguration.java @@ -38,4 +38,5 @@ public interface WebAppConfiguration { boolean isResourceManagerEnabled(); + boolean isFeaturesEnabled(String[] requiredFeatures); } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebApplication.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebApplication.java index dbaaaf9db8..e10005b801 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebApplication.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebApplication.java @@ -45,4 +45,5 @@ public interface WebApplication extends DBPApplication { RMController getResourceController(@NotNull SMCredentialsProvider credentialsProvider); + String getServerURL(); } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebAuthConfiguration.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebAuthConfiguration.java new file mode 100644 index 0000000000..1ff8c7c2de --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebAuthConfiguration.java @@ -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); +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebAuthInfo.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebAuthInfo.java index 308310aa9b..67eb0e9196 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebAuthInfo.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebAuthInfo.java @@ -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 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 diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSessionAuthJob.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSessionAuthJob.java new file mode 100644 index 0000000000..5459fe2d60 --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSessionAuthJob.java @@ -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 { + 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 providerConfig = Collections.emptyMap(); + var newAuthInfos = new ArrayList(); + for (Map.Entry entry : authInfo.getAuthData().entrySet()) { + String providerId = entry.getKey(); + Map userCredentials = (Map) 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); + } +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebUserContext.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebUserContext.java index 7fce004226..75f2c083b9 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebUserContext.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebUserContext.java @@ -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); diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBAppConfig.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBAppConfig.java index 10f9e129d4..1ff22f7ba9 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBAppConfig.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBAppConfig.java @@ -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() { diff --git a/server/bundles/io.cloudbeaver.service.auth/schema/service.auth.graphqls b/server/bundles/io.cloudbeaver.service.auth/schema/service.auth.graphqls index 54ca6a3dae..35a018f087 100644 --- a/server/bundles/io.cloudbeaver.service.auth/schema/service.auth.graphqls +++ b/server/bundles/io.cloudbeaver.service.auth/schema/service.auth.graphqls @@ -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 { diff --git a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/DBWServiceAuth.java b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/DBWServiceAuth.java index 749f1587ab..4738728f54 100644 --- a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/DBWServiceAuth.java +++ b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/DBWServiceAuth.java @@ -37,6 +37,7 @@ public interface DBWServiceAuth extends DBWService { WebAuthInfo authLogin( @NotNull WebSession webSession, @NotNull String providerId, + @Nullable String providerConfigurationId, @NotNull Map credentials, boolean linkWithActiveUser) throws DBWebException; diff --git a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/RPSessionHandler.java b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/RPSessionHandler.java index 4a8ddec114..27c3688c3d 100644 --- a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/RPSessionHandler.java +++ b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/RPSessionHandler.java @@ -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); diff --git a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebServiceBindingAuth.java b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebServiceBindingAuth.java index 510d4e46b4..66ff2dffea 100644 --- a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebServiceBindingAuth.java +++ b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebServiceBindingAuth.java @@ -39,6 +39,7 @@ public class WebServiceBindingAuth extends WebServiceBindingBase .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 -> { diff --git a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/impl/WebServiceAuthImpl.java b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/impl/WebServiceAuthImpl.java index 66d3cd47ff..d919aa38a1 100644 --- a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/impl/WebServiceAuthImpl.java +++ b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/impl/WebServiceAuthImpl.java @@ -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 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 providerConfig = Collections.emptyMap(); - SMAuthProvider authProviderInstance = authProvider.getInstance(); - SMAuthProviderExternal authProviderExternal = authProviderInstance instanceof SMAuthProviderExternal ? - (SMAuthProviderExternal) authProviderInstance : null; - Map 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) 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 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 diff --git a/server/bundles/io.cloudbeaver.service.security/db/cb_schema_create.sql b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_create.sql index 84aebe3ed9..b97b437c49 100644 --- a/server/bundles/io.cloudbeaver.service.security/db/cb_schema_create.sql +++ b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_create.sql @@ -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); diff --git a/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_7.sql b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_7.sql new file mode 100644 index 0000000000..e13c1d740e --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_7.sql @@ -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 +); \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/internal/AuthAttemptSessionInfo.java b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/internal/AuthAttemptSessionInfo.java new file mode 100644 index 0000000000..0f4401a0b1 --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/internal/AuthAttemptSessionInfo.java @@ -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 sessionParams; + + public AuthAttemptSessionInfo(String appSessionId, SMSessionType sessionType, Map sessionParams) { + this.appSessionId = appSessionId; + this.sessionType = sessionType; + this.sessionParams = sessionParams; + } + + public String getAppSessionId() { + return appSessionId; + } + + public SMSessionType getSessionType() { + return sessionType; + } + + public Map getSessionParams() { + return sessionParams; + } +} diff --git a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/internal/CBEmbeddedSecurityController.java b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/internal/CBEmbeddedSecurityController.java index a88a7850bf..a167c6bd8a 100644 --- a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/internal/CBEmbeddedSecurityController.java +++ b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/internal/CBEmbeddedSecurityController.java @@ -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>() { + }.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 sessionParameters, @NotNull SMSessionType sessionType, @NotNull String authProviderId, @NotNull Map userCredentials) throws DBException { + public SMAuthInfo authenticate( + @NotNull String appSessionId, + @NotNull Map sessionParameters, + @NotNull SMSessionType sessionType, + @NotNull String authProviderId, + @Nullable String authProviderConfigurationId, + @NotNull Map 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 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 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 authData, + String appSessionId, + SMSessionType sessionType, + Map 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 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 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 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 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) 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 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 sessionParameters, @NotNull Map userCredentials,