dbeaver/pro#5249 Migrate authUpdateStatus to websocket (#3369)

This commit is contained in:
Ruslan Musaev
2025-04-29 16:36:55 +00:00
committed by GitHub
parent 90eb0520a7
commit d06575af9c
24 changed files with 514 additions and 300 deletions
@@ -1,6 +1,6 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2024 DBeaver Corp and others
* Copyright (C) 2010-2025 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.
@@ -41,7 +41,7 @@ public interface DBWConstants {
GLOBAL,
EMBEDDED
}
String TASK_STATUS_FINISHED = "Finished";
//public static final String PERMISSION_USER = "user";
}
@@ -0,0 +1,24 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2025 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;
import io.cloudbeaver.model.session.WebSession;
import org.jkiss.code.NotNull;
public interface CustomCancelableJob {
void cancelJob(@NotNull WebSession webSession, @NotNull WebAsyncTaskInfo taskInfo);
}
@@ -1,6 +1,6 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2024 DBeaver Corp and others
* Copyright (C) 2010-2025 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.
@@ -35,6 +35,7 @@ public class WebAsyncTaskInfo {
private Throwable jobError;
private AbstractJob job;
private boolean cancelled = false;
public WebAsyncTaskInfo(@NotNull String id, @NotNull String name) {
this.id = id;
@@ -107,5 +108,4 @@ public class WebAsyncTaskInfo {
public void setJob(AbstractJob job) {
this.job = job;
}
}
@@ -34,6 +34,7 @@ import java.util.Map;
/**
* WebAuthInfo
*/
//TODO: create serializable model?
public class WebAuthInfo implements SMSessionPrincipal, WebUserAuthToken {
private static final Log log = Log.getLog(WebAuthInfo.class);
@@ -1,6 +1,6 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2024 DBeaver Corp and others
* Copyright (C) 2010-2025 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.
@@ -17,6 +17,7 @@
package io.cloudbeaver.model.session;
import io.cloudbeaver.*;
import io.cloudbeaver.model.CustomCancelableJob;
import io.cloudbeaver.model.WebAsyncTaskInfo;
import io.cloudbeaver.model.WebConnectionInfo;
import io.cloudbeaver.model.WebServerMessage;
@@ -544,15 +545,36 @@ public class WebSession extends BaseWebSession
}
}
AbstractJob job = taskInfo.getJob();
if (job instanceof CustomCancelableJob cancelableJob) {
cancelableJob.cancelJob(this, taskInfo);
}
if (job != null) {
job.cancel();
}
return true;
}
public WebAsyncTaskInfo createAndRunAsyncTask(@NotNull String taskName, @NotNull WebAsyncTaskProcessor<?> runnable) {
public WebAsyncTaskInfo createAsyncTask(@NotNull String taskName) {
int taskId = TASK_ID.incrementAndGet();
WebAsyncTaskInfo asyncTask = getAsyncTask(String.valueOf(taskId), taskName, true);
return asyncTask;
}
public List<WebAsyncTaskInfo> findTasksByJob(@NotNull Class<? extends AbstractJob> jobClass) {
synchronized (asyncTasks) {
List<WebAsyncTaskInfo> result = new ArrayList<>();
for (WebAsyncTaskInfo task : asyncTasks.values()) {
if (task.getJob() != null && jobClass.isAssignableFrom(task.getJob().getClass())) {
result.add(task);
}
}
return result;
}
}
public WebAsyncTaskInfo createAndRunAsyncTask(@NotNull String taskName, @NotNull WebAsyncTaskProcessor<?> runnable) {
WebAsyncTaskInfo asyncTask = createAsyncTask(taskName);
AbstractJob job = new AbstractJob(taskName) {
@Override
@@ -571,7 +593,7 @@ public class WebSession extends BaseWebSession
runnable.run(taskMonitor);
asyncTask.setResult(runnable.getResult());
asyncTask.setExtendedResult(runnable.getExtendedResults());
asyncTask.setStatus("Finished");
asyncTask.setStatus(DBWConstants.TASK_STATUS_FINISHED);
} catch (InvocationTargetException e) {
addSessionError(e.getTargetException());
asyncTask.setJobError(e.getTargetException());
@@ -16,6 +16,7 @@
*/
package io.cloudbeaver.registry;
import io.cloudbeaver.auth.SMAuthProviderFederated;
import org.eclipse.core.runtime.IConfigurationElement;
import org.jkiss.code.NotNull;
import org.jkiss.code.Nullable;
@@ -229,4 +230,13 @@ public class WebAuthProviderDescriptor extends AbstractDescriptor {
public boolean isServiceProvider() {
return serviceProvider;
}
public boolean isFederated() {
try {
implType.checkObjectClass(SMAuthProviderFederated.class);
return true;
} catch (DBException e) {
return false;
}
}
}
@@ -37,8 +37,7 @@ enum CBServerEventId {
cb_transaction_count @since(version: "24.3.3")
cb_session_task_info_updated @since(version: "24.3.1"),
cb_web_session_auth @since(version: "25.0.1")
cb_session_task_info_updated @since(version: "24.3.1")
}
# Events sent by client
@@ -227,12 +226,6 @@ type WSTransactionalCountEvent implements CBServerEvent {
transactionalCount: Int!
}
type WSWebSessionAuthEvent implements CBServerEvent {
id: CBServerEventId!
topicId: CBEventTopic!
userTokens: [UserAuthToken!]
}
extend type Query {
emptyEvent: Boolean
}
@@ -1,6 +1,6 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2024 DBeaver Corp and others
* Copyright (C) 2010-2025 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.
@@ -201,10 +201,10 @@ public interface DBWServiceCore extends DBWService {
///////////////////////////////////////////
// Async tasks
@WebAction
@WebAction(authRequired = false)
WebAsyncTaskInfo getAsyncTaskInfo(WebSession webSession, String taskId, Boolean removeOnFinish) throws DBWebException;
@WebAction
@WebAction(authRequired = false)
boolean cancelAsyncTask(WebSession webSession, String taskId) throws DBWebException;
}
@@ -19,15 +19,10 @@
<sessionHandler id="LocalSessionHandler" class="io.cloudbeaver.service.auth.local.LocalSessionHandler"/>
</extension>
<!--TODO: uncomment after frontend migration-->
<!-- <extension point="org.jkiss.dbeaver.ws.event.handler">-->
<!-- <eventHandler class="io.cloudbeaver.service.auth.handler.WSAuthSessionEventHandler">-->
<!-- <topic id="cb_session_auth"/>-->
<!-- </eventHandler>-->
<!-- </extension>-->
<extension point="org.jkiss.dbeaver.ws.event">
<event topicId="cb_session" id="cb_web_session_auth"
class="io.cloudbeaver.service.auth.handler.WebSessionAuthEvent"/>
<extension point="org.jkiss.dbeaver.ws.event.handler">
<eventHandler class="io.cloudbeaver.service.auth.handler.WSAuthSessionEventHandler">
<topic id="cb_session_auth"/>
</eventHandler>
</extension>
</plugin>
@@ -81,17 +81,25 @@ type AuthProviderInfo {
required: Boolean!
}
type AuthInfo {
redirectLink: String
type AuthInfo {
redirectLink: String @deprecated
authId: String @deprecated
authStatus: AuthStatus!
authStatus: AuthStatus! @deprecated
userTokens: [UserAuthToken!]
}
type FederatedAuthInfo @since(version: "25.0.3") {
redirectLink: String!
taskInfo: AsyncTaskInfo!
}
type FederatedAuthResult @since(version: "25.0.3") {
userTokens: [UserAuthToken!]! @since(version: "25.0.3")
}
type LogoutInfo @since(version: "23.3.3") {
redirectLinks: [String!]!
@@ -158,6 +166,10 @@ extend type Query {
# If forceSessionsLogout=true then kill another sessions
authLogin(provider: ID!, configuration: ID, credentials: Object, linkUser: Boolean, forceSessionsLogout: Boolean): AuthInfo!
@since(version: "25.0.3")
federatedAuthTaskResult(taskId: String!): FederatedAuthResult!
@deprecated
authUpdateStatus(authId: ID!, linkUser: Boolean): AuthInfo!
# Logouts user. If provider not specified then all authorizations are revoked from session.
@@ -187,4 +199,6 @@ extend type Mutation {
# Updates user preferences
setUserPreferences(preferences: Object!): UserInfo! @since(version: "24.0.1")
@since(version: "25.0.3")
federatedLogin(provider: ID!, configuration: ID, linkUser: Boolean, forceSessionsLogout: Boolean): FederatedAuthInfo!
}
@@ -1,6 +1,6 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2024 DBeaver Corp and others
* Copyright (C) 2010-2025 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.
@@ -39,8 +39,23 @@ public interface DBWServiceAuth extends DBWService {
@Nullable String providerConfigurationId,
@Nullable Map<String, Object> credentials,
boolean linkWithActiveUser,
boolean forceSessionsLogout) throws DBWebException;
boolean forceSessionsLogout
) throws DBWebException;
@WebAction(authRequired = false)
WebAsyncAuthStatus federatedLogin(
@NotNull WebSession webSession,
@NotNull String providerId,
@Nullable String providerConfigurationId,
boolean linkWithActiveUser,
boolean forceSessionsLogout
) throws DBWebException;
@WebAction(authRequired = false)
WebAsyncAuthTaskResult federatedAuthTaskResult(
@NotNull WebSession webSession,
@NotNull String taskId
) throws DBWebException;
@WebAction(authRequired = false)
WebAuthStatus authUpdateStatus(@NotNull WebSession webSession, @NotNull String authId, boolean linkWithActiveUser) throws DBWebException;
@@ -0,0 +1,78 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2025 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.model.CustomCancelableJob;
import io.cloudbeaver.model.WebAsyncTaskInfo;
import io.cloudbeaver.model.session.WebAuthInfo;
import io.cloudbeaver.model.session.WebSession;
import io.cloudbeaver.utils.WebEventUtils;
import org.eclipse.core.runtime.IStatus;
import org.jkiss.code.NotNull;
import org.jkiss.code.Nullable;
import org.jkiss.dbeaver.DBException;
import org.jkiss.dbeaver.model.runtime.AbstractJob;
import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor;
import java.util.List;
public class WebAsyncAuthJob extends AbstractJob implements CustomCancelableJob {
@NotNull
private final String authId;
private final boolean linkWithUser;
//result from task do used, because it cannot be serialized into 'object' gql type and separate request is used
//to get auth result
@Nullable
private List<WebAuthInfo> authResult;
public WebAsyncAuthJob(@NotNull String name, @NotNull String authId, boolean linkWithUser) {
super(name);
this.authId = authId;
this.linkWithUser = linkWithUser;
}
//do nothing, this job is workaround to use exist async process
@Override
protected IStatus run(DBRProgressMonitor monitor) {
return null;
}
@NotNull
public String getAuthId() {
return authId;
}
public boolean isLinkWithUser() {
return linkWithUser;
}
@Nullable
public List<WebAuthInfo> getAuthResult() {
return authResult;
}
public void setAuthResult(@Nullable List<WebAuthInfo> authResult) {
this.authResult = authResult;
}
@Override
public void cancelJob(@NotNull WebSession webSession, @NotNull WebAsyncTaskInfo taskInfo) {
taskInfo.setRunning(false);
taskInfo.setJobError(new DBException("Canceled by the user"));
WebEventUtils.sendAsyncTaskEvent(webSession, taskInfo);
}
}
@@ -0,0 +1,46 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2025 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.model.WebAsyncTaskInfo;
import org.jkiss.code.NotNull;
import org.jkiss.dbeaver.model.meta.Property;
public class WebAsyncAuthStatus {
@NotNull
private final String redirectLink;
@NotNull
private final WebAsyncTaskInfo taskInfo;
public WebAsyncAuthStatus(@NotNull String redirectLink, @NotNull WebAsyncTaskInfo taskInfo) {
this.redirectLink = redirectLink;
this.taskInfo = taskInfo;
}
@Property
@NotNull
public String getRedirectLink() {
return redirectLink;
}
@NotNull
@Property
public WebAsyncTaskInfo getTaskInfo() {
return taskInfo;
}
}
@@ -0,0 +1,38 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2025 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.model.session.WebAuthInfo;
import org.jkiss.code.NotNull;
import org.jkiss.dbeaver.model.meta.Property;
import java.util.List;
public class WebAsyncAuthTaskResult {
@NotNull
private final List<WebAuthInfo> userTokens;
public WebAsyncAuthTaskResult(@NotNull List<WebAuthInfo> userTokens) {
this.userTokens = userTokens;
}
@NotNull
@Property
public List<WebAuthInfo> getUserTokens() {
return userTokens;
}
}
@@ -1,6 +1,6 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2024 DBeaver Corp and others
* Copyright (C) 2010-2025 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.
@@ -44,6 +44,10 @@ public class WebServiceBindingAuth extends WebServiceBindingBase<DBWServiceAuth>
CommonUtils.toBoolean(env.getArgument("linkUser")),
CommonUtils.toBoolean(env.getArgument("forceSessionsLogout"))
))
.dataFetcher("federatedAuthTaskResult", env -> getService(env).federatedAuthTaskResult(
getWebSession(env, false),
env.getArgument("taskId")
))
.dataFetcher("authLogoutExtended", env -> getService(env).authLogout(
getWebSession(env, false),
env.getArgument("provider"),
@@ -78,6 +82,13 @@ public class WebServiceBindingAuth extends WebServiceBindingBase<DBWServiceAuth>
.dataFetcher("setUserPreferences",
env -> getService(env).setUserConfigurationParameters(getWebSession(env),
env.getArgument("preferences")))
.dataFetcher("federatedLogin", env -> getService(env).federatedLogin(
getWebSession(env, false),
env.getArgument("provider"),
env.getArgument("configuration"),
CommonUtils.toBoolean(env.getArgument("linkUser")),
CommonUtils.toBoolean(env.getArgument("forceSessionsLogout"))
))
;
}
}
@@ -16,11 +16,18 @@
*/
package io.cloudbeaver.service.auth.handler;
import io.cloudbeaver.DBWConstants;
import io.cloudbeaver.DBWebException;
import io.cloudbeaver.model.session.*;
import io.cloudbeaver.model.WebAsyncTaskInfo;
import io.cloudbeaver.model.session.BaseWebSession;
import io.cloudbeaver.model.session.WebAuthInfo;
import io.cloudbeaver.model.session.WebSession;
import io.cloudbeaver.model.session.WebSessionAuthProcessor;
import io.cloudbeaver.server.WebAppSessionManager;
import io.cloudbeaver.server.WebAppUtils;
import io.cloudbeaver.server.WebApplication;
import io.cloudbeaver.service.auth.WebAsyncAuthJob;
import io.cloudbeaver.utils.WebEventUtils;
import org.jkiss.code.NotNull;
import org.jkiss.dbeaver.DBException;
import org.jkiss.dbeaver.Log;
@@ -48,6 +55,27 @@ public class WSAuthSessionEventHandler implements WSEventHandler<WSAuthEvent> {
log.trace("No web session found in current node with id '" + sessionId + "'");
return;
}
List<WebAsyncTaskInfo> allAuthJobs = webSession.findTasksByJob(WebAsyncAuthJob.class);
WebAsyncTaskInfo relatedTask = allAuthJobs.stream().filter(
task -> {
WebAsyncAuthJob job = (WebAsyncAuthJob) task.getJob();
return job.getAuthId().equals(authInfo.getAuthAttemptId());
})
.findFirst().orElse(null);
if (relatedTask == null) {
String message = "No related authentication task was found in'" + sessionId + "',"
+ " probably authentication was canceled";
log.warn(message);
webSession.addWarningMessage(message);
return;
}
if (!relatedTask.isRunning()) {
String message = "Related authentication task was canceled";
log.warn(message);
webSession.addWarningMessage(message);
return;
}
WebAsyncAuthJob relatedJob = (WebAsyncAuthJob) relatedTask.getJob();
switch (authInfo.getAuthStatus()) {
case SUCCESS:
boolean linkCredentialsWithActiveUser = !webApplication.isConfigurationMode()
@@ -58,23 +86,25 @@ public class WSAuthSessionEventHandler implements WSEventHandler<WSAuthEvent> {
authInfo,
linkCredentialsWithActiveUser
).authenticateSession();
List<WebUserAuthTokenInfo> tokenInfos = newInfos
.stream()
.map(WebUserAuthTokenInfo::new)
.toList();
webSession.addSessionEvent(new WebSessionAuthEvent(tokenInfos));
relatedJob.setAuthResult(newInfos);
} catch (DBException e) {
webSession.addSessionError(e);
relatedTask.setJobError(e);
}
break;
case ERROR:
webSession.addSessionEvent(new WebSessionAuthEvent(new DBWebException(authInfo.getError(), authInfo.getErrorCode())));
var error = new DBWebException(authInfo.getError(), authInfo.getErrorCode());
relatedTask.setJobError(error);
break;
case IN_PROGRESS, EXPIRED:
log.error("Invalid auth status: " + authInfo.getAuthStatus());
default:
log.error("Unknown auth status: " + authInfo.getAuthStatus());
String message = "Invalid auth status: " + authInfo.getAuthStatus();
log.error(message);
var exception = new DBWebException(message);
webSession.addSessionError(exception);
relatedTask.setJobError(new DBWebException(message));
}
relatedTask.setRunning(false);
relatedTask.setStatus(DBWConstants.TASK_STATUS_FINISHED);
WebEventUtils.sendAsyncTaskEvent(webSession, relatedTask);
}
}
@@ -1,52 +0,0 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2025 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.handler;
import io.cloudbeaver.DBWebException;
import org.jkiss.code.NotNull;
import org.jkiss.code.Nullable;
import org.jkiss.dbeaver.model.websocket.WSConstants;
import org.jkiss.dbeaver.model.websocket.event.WSAbstractEvent;
import java.util.List;
public class WebSessionAuthEvent extends WSAbstractEvent {
@Nullable
private final List<WebUserAuthTokenInfo> userTokens;
private final DBWebException error;
protected WebSessionAuthEvent(@NotNull List<WebUserAuthTokenInfo> userTokens) {
super("cb_web_session_auth", WSConstants.TOPIC_SESSION);
this.userTokens = userTokens;
this.error = null;
}
protected WebSessionAuthEvent(@NotNull DBWebException error) {
super("cb_web_session_auth", WSConstants.TOPIC_SESSION);
this.userTokens = null;
this.error = error;
}
@Nullable
public List<WebUserAuthTokenInfo> getUserTokens() {
return userTokens;
}
public DBWebException getError() {
return error;
}
}
@@ -1,104 +0,0 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2025 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.handler;
import io.cloudbeaver.model.session.WebAuthInfo;
import io.cloudbeaver.model.session.WebUserAuthToken;
import org.jkiss.code.NotNull;
import org.jkiss.code.Nullable;
import java.time.OffsetDateTime;
public class WebUserAuthTokenInfo implements WebUserAuthToken {
@NotNull
private final String authProvider;
@Nullable
private final String authConfiguration;
@NotNull
private final OffsetDateTime loginTime;
@NotNull
private final String userId;
@NotNull
private final String displayName;
@Nullable
private final String message;
public WebUserAuthTokenInfo(
@NotNull WebAuthInfo authInfo
) {
this(
authInfo.getAuthProvider(),
authInfo.getAuthConfiguration(),
authInfo.getDisplayName(),
authInfo.getLoginTime(),
authInfo.getMessage(),
authInfo.getUserId()
);
}
public WebUserAuthTokenInfo(
@NotNull String authProvider,
@Nullable String authConfiguration,
@NotNull String displayName,
@NotNull OffsetDateTime loginTime,
@Nullable String message,
@NotNull String userId
) {
this.authConfiguration = authConfiguration;
this.authProvider = authProvider;
this.displayName = displayName;
this.loginTime = loginTime;
this.message = message;
this.userId = userId;
}
@Override
@Nullable
public String getAuthConfiguration() {
return authConfiguration;
}
@Override
@NotNull
public String getAuthProvider() {
return authProvider;
}
@Override
@NotNull
public String getDisplayName() {
return displayName;
}
@Override
@NotNull
public OffsetDateTime getLoginTime() {
return loginTime;
}
@Override
@Nullable
public String getMessage() {
return message;
}
@Override
@NotNull
public String getUserId() {
return userId;
}
}
@@ -20,6 +20,7 @@ import io.cloudbeaver.DBWebException;
import io.cloudbeaver.WebServiceUtils;
import io.cloudbeaver.auth.SMSignOutLinkProvider;
import io.cloudbeaver.auth.provider.local.LocalAuthProvider;
import io.cloudbeaver.model.WebAsyncTaskInfo;
import io.cloudbeaver.model.WebPropertyInfo;
import io.cloudbeaver.model.app.ServletAppConfiguration;
import io.cloudbeaver.model.session.WebAuthInfo;
@@ -30,10 +31,7 @@ import io.cloudbeaver.registry.WebAuthProviderDescriptor;
import io.cloudbeaver.registry.WebAuthProviderRegistry;
import io.cloudbeaver.registry.WebMetaParametersRegistry;
import io.cloudbeaver.server.CBApplication;
import io.cloudbeaver.service.auth.DBWServiceAuth;
import io.cloudbeaver.service.auth.WebAuthStatus;
import io.cloudbeaver.service.auth.WebLogoutInfo;
import io.cloudbeaver.service.auth.WebUserInfo;
import io.cloudbeaver.service.auth.*;
import io.cloudbeaver.service.auth.model.user.WebAuthProviderInfo;
import io.cloudbeaver.service.security.SMUtils;
import org.jkiss.code.NotNull;
@@ -72,6 +70,89 @@ public class WebServiceAuthImpl implements DBWServiceAuth {
boolean linkWithActiveUser,
boolean forceSessionsLogout
) throws DBWebException {
try {
var smAuthInfo = initiateAuthentication(webSession, providerId, providerConfigurationId, authParameters, forceSessionsLogout);
//TODO deprecated, use asyncAuthLogin for federated auth, exits for backward compatibility
linkWithActiveUser = linkWithActiveUser && CBApplication.getInstance().getAppConfiguration()
.isLinkExternalCredentialsWithUser();
if (smAuthInfo.getAuthStatus() == SMAuthStatus.IN_PROGRESS) {
//run async auth process
return new WebAuthStatus(smAuthInfo.getAuthAttemptId(), smAuthInfo.getRedirectUrl(), smAuthInfo.getAuthStatus());
} else {
//run it sync
var authProcessor = new WebSessionAuthProcessor(webSession, smAuthInfo, linkWithActiveUser);
return new WebAuthStatus(smAuthInfo.getAuthStatus(), authProcessor.authenticateSession());
}
} catch (SMTooManySessionsException e) {
throw new DBWebException("User authentication failed", e.getErrorType(), e);
} catch (Exception e) {
throw new DBWebException("User authentication failed", e);
}
}
@Override
public WebAsyncAuthStatus federatedLogin(
@NotNull WebSession webSession,
@NotNull String providerId,
@Nullable String providerConfigurationId,
boolean linkWithActiveUser,
boolean forceSessionsLogout
) throws DBWebException {
WebAuthProviderDescriptor providerDescriptor = WebAuthProviderRegistry.getInstance().getAuthProvider(providerId);
if (providerDescriptor == null) {
throw new DBWebException("Provider '" + providerId + "' not found");
}
if (!providerDescriptor.isFederated()) {
throw new DBWebException("Provider '" + providerId + "' is not federated");
}
try {
var smAuthInfo = initiateAuthentication(webSession, providerId, providerConfigurationId, Map.of(), forceSessionsLogout);
if (smAuthInfo.getAuthStatus() != SMAuthStatus.IN_PROGRESS) {
throw new DBWebException("Unexpected auth status: " + smAuthInfo.getAuthStatus());
}
if (CommonUtils.isEmpty(smAuthInfo.getRedirectUrl())) {
throw new DBWebException("Missing redirect URL");
}
WebAsyncTaskInfo authTask = webSession.createAsyncTask(providerId + " authentication");
authTask.setRunning(true);
authTask.setJob(
new WebAsyncAuthJob(providerId + " authentication job", smAuthInfo.getAuthAttemptId(), linkWithActiveUser)
);
return new WebAsyncAuthStatus(smAuthInfo.getRedirectUrl(), authTask);
} catch (SMTooManySessionsException e) {
throw new DBWebException("User authentication failed", e.getErrorType(), e);
} catch (Exception e) {
throw new DBWebException("User authentication failed", e);
}
}
@Override
public WebAsyncAuthTaskResult federatedAuthTaskResult(@NotNull WebSession webSession, @NotNull String taskId) throws DBWebException {
WebAsyncTaskInfo taskInfo = webSession.asyncTaskStatus(taskId, true);
if (taskInfo == null) {
throw new DBWebException("Task '" + taskId + "' not found");
}
if (taskInfo.isRunning()) {
throw new DBWebException("Task '" + taskId + "' is running");
}
if (taskInfo.getJob() == null || !WebAsyncAuthJob.class.isAssignableFrom(taskInfo.getJob().getClass())) {
throw new DBWebException("Task '" + taskId + "' is not async auth task");
}
WebAsyncAuthJob job = (WebAsyncAuthJob) taskInfo.getJob();
List<WebAuthInfo> userTokens = job.getAuthResult();
if (CommonUtils.isEmpty(userTokens)) {
userTokens = List.of();
}
return new WebAsyncAuthTaskResult(userTokens);
}
private static SMAuthInfo initiateAuthentication(
@NotNull WebSession webSession,
@NotNull String providerId,
@Nullable String providerConfigurationId,
@Nullable Map<String, Object> authParameters,
boolean forceSessionsLogout
) throws DBException {
if (CommonUtils.isEmpty(providerId)) {
throw new DBWebException("Missing auth provider parameter");
}
@@ -87,34 +168,17 @@ public class WebServiceAuthImpl implements DBWServiceAuth {
String currentSmSessionId = (webSession.getUser() == null || CBApplication.getInstance().isConfigurationMode())
? null
: webSession.getUserContext().getSmSessionId();
try {
var smAuthInfo = securityController.authenticate(
webSession.getSessionId(),
currentSmSessionId,
webSession.getSessionParameters(),
WebSession.CB_SESSION_TYPE,
providerId,
providerConfigurationId,
authParameters,
forceSessionsLogout
);
linkWithActiveUser = linkWithActiveUser && CBApplication.getInstance().getAppConfiguration().isLinkExternalCredentialsWithUser();
if (smAuthInfo.getAuthStatus() == SMAuthStatus.IN_PROGRESS) {
//run async auth process
return new WebAuthStatus(smAuthInfo.getAuthAttemptId(), smAuthInfo.getRedirectUrl(), smAuthInfo.getAuthStatus());
} else {
//run it sync
var authProcessor = new WebSessionAuthProcessor(webSession, smAuthInfo, linkWithActiveUser);
return new WebAuthStatus(smAuthInfo.getAuthStatus(), authProcessor.authenticateSession());
}
} catch (SMTooManySessionsException e) {
throw new DBWebException("User authentication failed", e.getErrorType(), e);
} catch (Exception e) {
throw new DBWebException("User authentication failed", e);
}
var smAuthInfo = securityController.authenticate(
webSession.getSessionId(),
currentSmSessionId,
webSession.getSessionParameters(),
WebSession.CB_SESSION_TYPE,
providerId,
providerConfigurationId,
authParameters,
forceSessionsLogout
);
return smAuthInfo;
}
@Override
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -8,11 +8,12 @@
import { injectable } from '@cloudbeaver/core-di';
import { AutoRunningTask, type ITask } from '@cloudbeaver/core-executor';
import { WindowsService } from '@cloudbeaver/core-routing';
import { type AuthInfo, AuthStatus, type UserInfo } from '@cloudbeaver/core-sdk';
import { type UserInfo } from '@cloudbeaver/core-sdk';
import { uuid } from '@cloudbeaver/core-utils';
import { AsyncTaskInfoService } from '@cloudbeaver/core-root';
import { type AuthProviderConfiguration, AuthProvidersResource } from './AuthProvidersResource.js';
import { type ILoginOptions, UserInfoResource } from './UserInfoResource.js';
import { type AuthProviderConfiguration } from './AuthProvidersResource.js';
import { type IFederatedLoginOptions, type ILoginOptions, UserInfoResource } from './UserInfoResource.js';
export interface IUserAuthConfiguration {
providerId: string;
@@ -27,59 +28,50 @@ export class AuthInfoService {
constructor(
private readonly userInfoResource: UserInfoResource,
private readonly authProvidersResource: AuthProvidersResource,
private readonly windowsService: WindowsService,
private readonly asyncTaskInfoService: AsyncTaskInfoService,
) {}
login(providerId: string, options: ILoginOptions): ITask<UserInfo | null> {
return new AutoRunningTask(async () => await this.userInfoResource.login(providerId, options)).then(authInfo =>
this.federatedAuthentication(providerId, options, authInfo),
);
async login(providerId: string, options: ILoginOptions): Promise<UserInfo | null> {
await this.userInfoResource.login(providerId, options);
return this.userInfoResource.data;
}
private federatedAuthentication(
providerId: string,
options: ILoginOptions,
{ redirectLink, authId, authStatus }: AuthInfo,
): ITask<UserInfo | null> {
let window: Window | null = null;
let id = providerId;
federatedLogin(providerId: string, options: IFederatedLoginOptions): ITask<UserInfo | null> {
let redirectWindow: Window | null = null;
if (options.configurationId) {
const configuration = this.authProvidersResource.getConfiguration(providerId, options.configurationId);
const task = this.asyncTaskInfoService.create(async () => {
const result = await this.userInfoResource.requestFederatedLogin(providerId, options);
if (configuration) {
id = configuration.id;
if (result.redirectLink) {
const id = uuid();
redirectWindow = this.windowsService.open(id, {
url: result.redirectLink,
target: id,
width: 600,
height: 700,
});
if (redirectWindow) {
redirectWindow.focus();
}
}
}
if (redirectLink) {
id = uuid();
window = this.windowsService.open(id, {
url: redirectLink,
target: id,
width: 600,
height: 700,
});
if (window) {
window.focus();
}
}
return result.taskInfo;
});
return new AutoRunningTask(
() => {
if (authId && authStatus === AuthStatus.InProgress) {
return this.userInfoResource.finishFederatedAuthentication(authId, options.linkUser);
async () => {
await this.asyncTaskInfoService.run(task);
await this.userInfoResource.syncData();
if (redirectWindow) {
this.windowsService.close(redirectWindow);
}
return AutoRunningTask.resolve(this.userInfoResource.data);
},
() => {
if (window) {
this.windowsService.close(window);
}
return this.userInfoResource.data;
},
() => this.asyncTaskInfoService.cancel(task.id),
);
}
}
@@ -12,6 +12,7 @@ import { AutoRunningTask, type ISyncExecutor, type ITask, SyncExecutor, whileTas
import { CachedDataResource, type ResourceKeySimple, ResourceKeyUtils } from '@cloudbeaver/core-resource';
import { SessionResource } from '@cloudbeaver/core-root';
import {
type FederatedAuthInfo,
type AuthInfo,
type AuthLogoutQuery,
AuthStatus,
@@ -36,6 +37,8 @@ export interface ILoginOptions {
forceSessionsLogout?: boolean;
}
export type IFederatedLoginOptions = Omit<ILoginOptions, 'credentials'>;
export const ANONYMOUS_USER_ID = 'anonymous';
@injectable()
@@ -126,15 +129,27 @@ export class UserInfoResource extends CachedDataResource<UserInfo | null, void,
});
if (authInfo.userTokens && authInfo.authStatus === AuthStatus.Success) {
this.resetIncludes();
this.setData(await this.loader());
this.sessionResource.markOutdated();
await this.syncData();
}
return authInfo as AuthInfo;
}
finishFederatedAuthentication(authId: string, linkUser?: boolean): ITask<UserInfo | null> {
async requestFederatedLogin(
provider: string,
{ configurationId, linkUser, forceSessionsLogout }: IFederatedLoginOptions,
): Promise<FederatedAuthInfo> {
const { result } = await this.graphQLService.sdk.federatedLogin({
provider,
configuration: configurationId,
linkUser,
forceSessionsLogout,
});
return result;
}
autoLogin(authId: string, linkUser?: boolean): ITask<UserInfo | null> {
let activeTask: ITask<AuthInfo> | undefined;
return new AutoRunningTask<UserInfo | null>(
@@ -164,9 +179,7 @@ export class UserInfoResource extends CachedDataResource<UserInfo | null, void,
const authInfo = await activeTask;
if (authInfo.userTokens && authInfo.authStatus === AuthStatus.Success) {
this.resetIncludes();
this.setData(await this.loader());
this.sessionResource.markOutdated();
await this.syncData();
}
return this.data;
@@ -272,6 +285,12 @@ export class UserInfoResource extends CachedDataResource<UserInfo | null, void,
return this.data?.configurationParameters[key];
}
async syncData(): Promise<void> {
this.resetIncludes();
this.setData(await this.loader());
this.sessionResource.markOutdated();
}
protected async loader(key: void, includes?: ReadonlyArray<string>): Promise<UserInfo | null> {
try {
const { user } = await this.graphQLService.sdk.getActiveUser({
@@ -0,0 +1,8 @@
mutation federatedLogin($provider: ID!, $configuration: ID, $linkUser: Boolean, $forceSessionsLogout: Boolean) {
result: federatedLogin(provider: $provider, configuration: $configuration, linkUser: $linkUser, forceSessionsLogout: $forceSessionsLogout) {
redirectLink
taskInfo {
...AsyncTaskInfo
}
}
}
@@ -219,7 +219,7 @@ export class AuthenticationService extends Bootstrap {
const action = contexts.getContext(sessionActionContext);
if (isAutoLoginSessionAction(data)) {
const user = await this.userInfoResource.finishFederatedAuthentication(data['auth-id'], false);
const user = await this.userInfoResource.autoLogin(data['auth-id'], false);
if (user) {
//we request this method/request bc login form can be opened automatically.
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -40,7 +40,7 @@ interface IData {
tabIds: string[];
login: (linkUser: boolean, provider?: AuthProvider, configuration?: AuthProviderConfiguration) => Promise<void>;
loginFederated: (provider: AuthProvider, configuration: AuthProviderConfiguration, onClose?: () => void) => Promise<void>;
federatedLogin: (provider: AuthProvider, configuration: AuthProviderConfiguration) => Promise<void>;
}
interface IState {
@@ -223,22 +223,23 @@ export function useAuthDialogState(accessRequest: boolean, providerId: string |
try {
this.state.setActiveProvider(provider, configuration ?? null);
const loginTask = authInfoService.login(provider.id, {
configurationId: configuration?.id,
credentials: {
...state.credentials,
if (provider.federated && configuration) {
await this.federatedLogin(provider, configuration);
} else {
await authInfoService.login(provider.id, {
configurationId: configuration?.id,
credentials: {
...state.credentials.credentials,
user: state.credentials.credentials['user']?.trim(),
password: state.credentials.credentials['password']?.trim(),
...state.credentials,
credentials: {
...state.credentials.credentials,
user: state.credentials.credentials['user']?.trim(),
password: state.credentials.credentials['password']?.trim(),
},
},
},
forceSessionsLogout: state.forceSessionsLogout,
linkUser,
});
this.authTask = loginTask;
await loginTask;
forceSessionsLogout: state.forceSessionsLogout,
linkUser,
});
}
} catch (exception: any) {
const gqlError = errorOf(exception, GQLError);
@@ -265,6 +266,15 @@ export function useAuthDialogState(accessRequest: boolean, providerId: string |
return;
},
async federatedLogin(provider: AuthProvider, configuration: AuthProviderConfiguration): Promise<void> {
this.authTask = authInfoService.federatedLogin(provider.id, {
configurationId: configuration.id,
forceSessionsLogout: state.forceSessionsLogout,
linkUser: false,
});
await this.authTask;
},
}),
{
state: observable.ref,