Merge remote-tracking branch 'origin/devel' into devel

This commit is contained in:
serge-rider
2022-06-30 19:38:10 +02:00
34 changed files with 549 additions and 158 deletions
@@ -2,6 +2,7 @@ package io.cloudbeaver.server.servlets;
import io.cloudbeaver.DBWConstants;
import io.cloudbeaver.auth.SMWAuthProviderFederated;
import io.cloudbeaver.model.session.WebActionParameters;
import io.cloudbeaver.model.session.WebSession;
import io.cloudbeaver.registry.WebHandlerRegistry;
import io.cloudbeaver.registry.WebServletHandlerDescriptor;
@@ -16,6 +17,7 @@ import org.eclipse.jetty.servlet.DefaultServlet;
import org.eclipse.jetty.util.resource.Resource;
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.security.SMAuthProviderCustomConfiguration;
import org.jkiss.dbeaver.registry.auth.AuthProviderDescriptor;
@@ -29,8 +31,8 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Map;
@WebServlet(urlPatterns = "/")
public class CBStaticServlet extends DefaultServlet {
@@ -99,14 +101,28 @@ public class CBStaticServlet extends DefaultServlet {
WebSession webSession = CBPlatform.getInstance().getSessionManager().getWebSession(
request, response, false);
if (webSession.getUser() == null) {
String signInLink = ((SMWAuthProviderFederated) authProviderInstance).getSignInLink(
activeAuthConfig.getId(), Collections.emptyMap());
var securityController = webSession.getSecurityController();
SMAuthInfo authInfo = securityController.authenticate(
webSession.getSessionId(),
null,
webSession.getSessionParameters(),
WebSession.CB_SESSION_TYPE,
authProvider.getId(),
activeAuthConfig.getId(),
Map.of()
);
String signInLink = authInfo.getRedirectUrl();
//ignore current routing if non-root page is open
if (!signInLink.endsWith("#")) {
signInLink += "#";
}
if (!CommonUtils.isEmpty(signInLink)) {
// Redirect to it
Map<String, Object> authActionParams = Map.of(
"action", "auto-login",
"auth-id", authInfo.getAuthAttemptId()
);
WebActionParameters.saveToSession(webSession, authActionParams);
request.getSession().setAttribute(DBWConstants.STATE_ATTR_SIGN_IN_STATE, DBWConstants.SignInState.GLOBAL);
response.sendRedirect(signInLink);
return true;
@@ -106,6 +106,8 @@ public class WebServiceAuthImpl implements DBWServiceAuth {
return new WebAuthStatus(smAuthInfo.getAuthAttemptId(), smAuthInfo.getRedirectUrl(), smAuthInfo.getAuthStatus());
case ERROR:
throw new DBWebException(smAuthInfo.getError());
case EXPIRED:
throw new DBException("Authorization has already been processed");
default:
throw new DBWebException("Unknown auth status:" + smAuthInfo.getAuthStatus());
}
@@ -1067,13 +1067,18 @@ public class CBEmbeddedSecurityController implements SMAdminController, SMAuthen
return SMAuthInfo.inProgress(authId, redirectUrl, authData);
case ERROR:
return SMAuthInfo.error(authId, authError);
case EXPIRED:
return SMAuthInfo.expired(authId);
default:
throw new SMException("Unknown auth status:" + smAuthStatus);
}
}
String smToken = findTokenBySmSession(smSessionId);
return SMAuthInfo.success(authId, smToken, getTokenPermissions(smToken), authData);
SMAuthPermissions authPermissions = getTokenPermissions(smToken);
var successAuthStatus = SMAuthInfo.success(authId, smToken, authPermissions, authData);
updateAuthStatus(authId, SMAuthStatus.EXPIRED, authData, null, authPermissions.getSessionId());
return successAuthStatus;
} catch (SQLException e) {
throw new DBException("Error while read auth info", e);
@@ -31,6 +31,9 @@ const tabsStyles = css`
`;
const administrationStyles = css`
SlideElement {
composes: theme-background-background theme-text-on-background from global;
}
container {
composes: theme-background-secondary theme-text-on-secondary from global;
display: flex;
+7 -1
View File
@@ -32,6 +32,12 @@ const bodyStyles = css`
}
`;
const loaderStyle = css`
ExceptionMessage {
padding: 24px;
}
`;
export const Body = observer(function Body() {
useAppLoadingScreen();
const themeService = useService(ThemeService);
@@ -53,7 +59,7 @@ export const Body = observer(function Body() {
return styled(style)(
<DNDProvider>
<theme ref={ref} className={`theme-${themeService.currentTheme.id}`}>
<Loader state={permissionsService}>{() => styled(style)(
<Loader state={permissionsService} style={loaderStyle}>{() => styled(style)(
<>
{Screen && <Screen {...screenService.routerService.params} />}
</>
@@ -26,8 +26,8 @@ export class AppAuthService extends Bootstrap {
readonly auth: IExecutor<boolean>;
constructor(
private serverService: ServerService,
private userInfoResource: UserInfoResource,
private readonly serverService: ServerService,
private readonly userInfoResource: UserInfoResource,
) {
super();
this.auth = new Executor();
@@ -7,8 +7,10 @@
*/
import { injectable } from '@cloudbeaver/core-di';
import { type ITask, AutoRunningTask } from '@cloudbeaver/core-executor';
import { AuthInfo, AuthProviderConfiguration, AuthStatus, UserInfo } from '@cloudbeaver/core-sdk';
import { WindowsService } from '@cloudbeaver/core-ui';
import { uuid } from '@cloudbeaver/core-utils';
import { AuthProvidersResource } from './AuthProvidersResource';
import { type ILoginOptions, UserInfoResource } from './UserInfoResource';
@@ -55,23 +57,21 @@ export class AuthInfoService {
) {
}
async login(providerId: string, options: ILoginOptions): Promise<UserInfo | null> {
const authInfo = await this.userInfoResource.login(providerId, options);
await this.federatedAuthentication(providerId, options, authInfo);
return this.userInfoResource.data;
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 logout(): Promise<void> {
await this.userInfoResource.logout();
}
private async federatedAuthentication(
private federatedAuthentication(
providerId: string,
options: ILoginOptions,
{ redirectLink, authId, authStatus }: AuthInfo
): Promise<void> {
): ITask<UserInfo | null> {
let window: Window | null = null;
let id = providerId;
if (options.configurationId) {
@@ -83,7 +83,8 @@ export class AuthInfoService {
}
if (redirectLink) {
const window = this.windowsService.open(id, {
id = uuid();
window = this.windowsService.open(id, {
url: redirectLink,
target: id,
width: 600,
@@ -95,8 +96,16 @@ export class AuthInfoService {
}
}
if (authId && authStatus === AuthStatus.InProgress) {
await this.userInfoResource.finishFederatedAuthentication(authId, options.linkUser);
}
return new AutoRunningTask(() => {
if (authId && authStatus === AuthStatus.InProgress) {
return this.userInfoResource.finishFederatedAuthentication(authId, options.linkUser);
}
return AutoRunningTask.resolve(this.userInfoResource.data);
}, () => {
if (window) {
this.windowsService.close(window);
}
});
}
}
@@ -9,7 +9,7 @@
import { computed, makeObservable, runInAction } from 'mobx';
import { injectable } from '@cloudbeaver/core-di';
import { SyncExecutor, ISyncExecutor } from '@cloudbeaver/core-executor';
import { SyncExecutor, ISyncExecutor, ITask, AutoRunningTask, whileTask } from '@cloudbeaver/core-executor';
import { SessionResource } from '@cloudbeaver/core-root';
import { AuthInfo, AuthStatus, CachedDataResource, GetActiveUserQueryVariables, GraphQLService, isResourceKeyList, ObjectOrigin, ResourceKey, UserAuthToken, UserInfo } from '@cloudbeaver/core-sdk';
@@ -121,41 +121,51 @@ UserInfoIncludes
});
}
async finishFederatedAuthentication(authId: string, link?: boolean): Promise<void> {
return await this.performUpdate(undefined, [], async () => {
await new Promise<void>((resolve, reject) => {
const interval = setInterval(async () => {
try {
finishFederatedAuthentication(authId: string, link?: boolean): ITask<UserInfo | null> {
let activeTask: ITask<AuthInfo> | undefined;
return new AutoRunningTask<UserInfo | null>(() => this.performUpdate(
undefined,
[],
async () => {
activeTask = whileTask<AuthInfo>(
authInfo => {
if (authInfo.authStatus === AuthStatus.Success) {
return true;
} else if (authInfo.authStatus === AuthStatus.Error) {
throw new Error('Authentication error');
}
return false;
},
async () => {
const { authInfo } = await this.graphQLService.sdk.getAuthStatus({
authId,
linkUser: link,
customIncludeOriginDetails: true,
});
return authInfo as AuthInfo;
},
1000
);
if (authInfo.userTokens && authInfo.authStatus === AuthStatus.Success) {
if (this.data === null) {
this.resetIncludes();
this.setData(await this.loader());
} else {
this.data.authTokens.push(...authInfo.userTokens as UserAuthToken[]);
}
const authInfo = await activeTask;
this.sessionResource.markOutdated();
}
if (authInfo.authStatus === AuthStatus.Success) {
resolve();
clearInterval(interval);
} else if (authInfo.authStatus === AuthStatus.Error) {
reject(new Error('Authentication error'));
clearInterval(interval);
}
} catch (exception: any) {
reject(exception);
clearInterval(interval);
if (authInfo.userTokens && authInfo.authStatus === AuthStatus.Success) {
if (this.data === null) {
this.resetIncludes();
this.setData(await this.loader());
} else {
this.data.authTokens.push(...authInfo.userTokens as UserAuthToken[]);
}
}, 1000);
});
this.sessionResource.markOutdated();
}
return this.data;
}
), () => {
activeTask?.cancel();
});
}
@@ -111,7 +111,7 @@ export const ExceptionMessage = observer<Props>(function ExceptionMessage({
<error-name><span>{name || error.details?.name}</span></error-name>
<error-message>{message || error.details?.message}</error-message>
<error-actions>
{exception && (
{exception && error.details?.hasDetails && (
<Button type='button' mod={['outlined']} disabled={error.isOpen} onClick={error.open}>
{translate('ui_errors_details')}
</Button>
@@ -9,6 +9,9 @@
export interface ILoadableState {
isLoading: () => boolean;
isLoaded: () => boolean;
exception?: Error[] | Error | null;
readonly exception?: Error[] | Error | null;
reload?: () => void;
isCancelled?: () => boolean;
cancel?: () => void;
}
@@ -18,16 +18,10 @@ import { uuid } from '@cloudbeaver/core-utils';
import { Button } from '../Button';
import { ExceptionMessage } from '../ExceptionMessage';
import { StaticImage } from '../StaticImage';
import type { ILoadableState } from './ILoadableState';
import { ILoaderContext, LoaderContext } from './LoaderContext';
import { loaderStyles, overlayStyles } from './loaderStyles';
export interface ILoadableState {
isLoading: () => boolean;
isLoaded: () => boolean;
readonly exception?: Error[] | Error | null;
reload?: () => void;
}
type LoaderState = ILoadableState | {
loading: boolean;
};
@@ -41,6 +35,8 @@ interface Props {
message?: string;
/** hides message */
hideMessage?: boolean;
/** hides error message */
hideException?: boolean;
/** render loader as overlay with white spinner */
overlay?: boolean;
/** loader with white spinner */
@@ -69,6 +65,7 @@ export const Loader = observer<Props>(function Loader({
overlay,
message,
hideMessage,
hideException,
secondary,
small,
inline,
@@ -101,6 +98,16 @@ export const Loader = observer<Props>(function Loader({
) {
loaded = element.isLoaded();
loading = element.isLoading();
if (loading) {
if (element.cancel) {
onCancel = element.cancel;
}
if (element.isCancelled) {
cancelDisabled = element.isCancelled();
}
}
} else {
loading = element.loading;
loaded = !loading;
@@ -168,7 +175,7 @@ export const Loader = observer<Props>(function Loader({
}
});
if (exception && !loading) {
if (exception && !loading && !hideException) {
return styled(style)(
<ExceptionMessage
exception={exception}
+2
View File
@@ -17,6 +17,7 @@ export * from './layout/BASE_LAYOUT_GRID_STYLES';
export * from './layout/TopAppBar';
export * from './Loader/Loader';
export * from './Loader/ILoadableState';
export * from './Marks/ConnectionMark';
@@ -164,6 +165,7 @@ export * from './useMapResource';
export * from './useMouse';
export * from './useObjectRef';
export * from './useObservableRef';
export * from './usePromiseState';
export * from './useStateDelay';
export * from './useErrorDetails';
export * from './useAppLoadingScreen';
@@ -12,7 +12,7 @@ import { IServiceConstructor, useService } from '@cloudbeaver/core-di';
import { NotificationService } from '@cloudbeaver/core-events';
import { CachedDataResource, CachedDataResourceContext, CachedDataResourceGetter, CachedDataResourceParam, CachedResourceData, CachedResourceIncludeArgs, isResourceKeyList } from '@cloudbeaver/core-sdk';
import type { ILoadableState } from './Loader/Loader';
import type { ILoadableState } from './Loader/ILoadableState';
import { useObjectRef } from './useObjectRef';
interface KeyWithIncludes<TKey, TIncludes> {
@@ -13,7 +13,7 @@ import { IServiceConstructor, useService } from '@cloudbeaver/core-di';
import { NotificationService } from '@cloudbeaver/core-events';
import { CachedResourceIncludeArgs, CachedMapResource, CachedMapResourceGetter, ResourceKey, CachedMapResourceValue, CachedMapResourceKey, CachedMapResourceArguments, CachedMapResourceLoader, ResourceKeyList, CachedMapResourceListGetter, isResourceKeyList } from '@cloudbeaver/core-sdk';
import type { ILoadableState } from './Loader/Loader';
import type { ILoadableState } from './Loader/ILoadableState';
import { useObjectRef } from './useObjectRef';
import { useObservableRef } from './useObservableRef';
@@ -0,0 +1,70 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2022 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { computed, observable } from 'mobx';
import { useEffect } from 'react';
import { Task } from '@cloudbeaver/core-executor';
import type { ILoadableState } from './Loader/ILoadableState';
import { useObservableRef } from './useObservableRef';
export function usePromiseState<T>(promise: Promise<T> | null): ILoadableState {
const state = useObservableRef(() => ({
exception: null,
loading: true,
get cancel() {
if (this.promise instanceof Task && this.promise.cancellable) {
return this.promise.cancel.bind(this.promise);
}
return undefined;
},
isCancelled(): boolean {
if (this.promise instanceof Task) {
return this.promise.cancelled;
}
return false;
},
isLoading(): boolean {
if (this.promise instanceof Task) {
return this.promise.executing;
}
return this.promise !== null && this.loading;
},
isLoaded(): boolean {
return this.promise === null;
},
}), {
cancel: computed,
promise: observable.ref,
exception: observable.ref,
loading: observable.ref,
}, { promise });
useEffect(() => {
if (promise) {
state.loading = true;
state.exception = null;
promise
.catch(exception => {
if (promise === state.promise) {
state.exception = exception;
}
})
.finally(() => {
if (promise === state.promise) {
state.loading = false;
}
});
}
}, [promise]);
return state;
}
@@ -21,7 +21,7 @@ export enum ENotificationType {
Error = 'Error',
Success = 'Success',
Loading = 'Loading',
Custom =' Custom'
Custom = ' Custom'
}
export interface INotificationExtraProps<T = never> {
@@ -53,7 +53,7 @@ export interface INotification<TProps extends INotificationExtraProps<any> = INo
title: string;
message?: string;
timestamp: number;
details?: string | Error;
details?: string | Error | null;
persistent?: boolean;
state: { deleteDelay: number };
isSilent: boolean;
@@ -67,7 +67,7 @@ export interface INotificationOptions<TProps extends INotificationExtraProps<any
title: string;
uuid?: string;
message?: string;
details?: string | Error;
details?: string | Error | null;
isSilent?: boolean;
persistent?: boolean;
extraProps?: TProps;
@@ -39,7 +39,7 @@ export class NotificationService {
}
constructor(
private settings: EventsSettingsService
private readonly settings: EventsSettingsService
) {
this.notificationList = new OrderedMap<number, INotification<any>>(({ id }) => id);
this.closeTask = new Executor();
@@ -147,7 +147,12 @@ export class NotificationService {
return this.notify(notification, ENotificationType.Error);
}
logException(exception: Error | GQLError, title?: string, message?: string, silent?: boolean): void {
logException(
exception: Error | GQLError | undefined | null,
title?: string,
message?: string,
silent?: boolean
): void {
const errorDetails = getErrorDetails(exception);
if (!silent) {
@@ -0,0 +1,28 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2022 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import type { ITask } from './ITask';
import { Task } from './Task';
export class AutoRunningTask<TValue> extends Task<TValue> {
constructor(
task: () => Promise<TValue>,
externalCancel?: () => Promise<void> | void
) {
super(task, externalCancel);
this.run();
}
static resolve<TValue>(value: TValue): ITask<TValue> {
return new AutoRunningTask(() => Promise.resolve(value));
}
static reject<TValue>(exception: any): ITask<TValue> {
return new AutoRunningTask(() => Promise.reject(exception));
}
}
@@ -35,7 +35,7 @@ export class Task<TValue> implements ITask<TValue> {
private resolve!: (value: TValue) => void;
private reject!: (reason?: any) => void;
private innerPromise: Promise<TValue>;
private readonly innerPromise: Promise<TValue>;
private sourcePromise: Promise<TValue> | null;
get [Symbol.toStringTag](): string {
@@ -44,7 +44,7 @@ export class Task<TValue> implements ITask<TValue> {
constructor(
readonly task: () => Promise<TValue>,
private externalCancel?: () => Promise<void> | void
private readonly externalCancel?: () => Promise<void> | void
) {
this.innerPromise = new Promise((resolve, reject) => {
this.reject = reject;
@@ -66,17 +66,24 @@ export class Task<TValue> implements ITask<TValue> {
onfulfilled?: ((value: TValue) => TResult1 | PromiseLike<TResult1>) | null,
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null
): ITask<TResult1 | TResult2> {
let cancel = this.cancel.bind(this);
return new Task(async () => {
try {
const value = await this.innerPromise;
return await onfulfilled?.(value) as TResult1;
} catch (e: any) {
if (onrejected) {
return await onrejected(e);
}
throw e;
const value = await this.innerPromise;
const task = onfulfilled?.(value);
if (task instanceof Task) {
cancel = async () => {
await task.cancel();
await this.cancel();
};
}
}, () => this.cancel()).run();
return await task as TResult1;
}, () => cancel())
.run()
.catch(onrejected);
}
catch<TResult = never>(
@@ -127,7 +134,7 @@ export class Task<TValue> implements ITask<TValue> {
return this;
}
cancel(): Promise<void> | void {
async cancel(): Promise<void> {
if (this.cancelled) {
return;
}
@@ -140,11 +147,11 @@ export class Task<TValue> implements ITask<TValue> {
}
if (this.externalCancel) {
return this.externalCancel();
await this.externalCancel();
}
if (this.sourcePromise instanceof Task) {
return this.sourcePromise.cancel();
await this.sourcePromise.cancel();
}
}
}
@@ -1,4 +1,6 @@
export * from './TaskScheduler/AutoRunningTask';
export * from './TaskScheduler/ITask';
export * from './TaskScheduler/Task';
export * from './TaskScheduler/TaskScheduler';
export * from './ExecutionContext';
export * from './Executor';
@@ -10,3 +12,4 @@ export * from './IExecutorHandler';
export * from './IExecutorHandlersCollection';
export * from './ISyncExecutor';
export * from './SyncExecutor';
export * from './whileTask';
@@ -0,0 +1,73 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2022 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { AutoRunningTask } from './TaskScheduler/AutoRunningTask';
import type { ITask } from './TaskScheduler/ITask';
import { Task } from './TaskScheduler/Task';
export function whileTask<T>(
callback: (value: T) => Promise<boolean> | boolean,
task: () => Promise<T>,
interval: number,
cancelMessage?: string
): ITask<T> {
let resolve: (value: T | PromiseLike<T>) => void;
let reject: (reason?: any) => void;
const lockPromise = new Promise<T>((_resolve, _reject) => {
resolve = _resolve;
reject = _reject;
});
let timeoutId: NodeJS.Timeout | null;
let activeTask: Promise<T> | null;
let stopped = false;
function stop() {
if (timeoutId) {
clearTimeout(timeoutId);
}
stopped = true;
}
async function cancelTask(exception?: any) {
stop();
if (activeTask instanceof Task) {
await activeTask.cancel();
}
reject(exception);
}
function runTask() {
activeTask = task();
activeTask
.finally(() => {
activeTask = null;
})
.then(async value => {
const state = await callback(value);
if (state) {
resolve(value);
} else if (!stopped) {
timeoutId = setTimeout(runTask, interval);
}
})
.catch(cancelTask);
}
return new AutoRunningTask(
() => {
runTask();
return lockPromise;
},
() => cancelTask(new Error(cancelMessage ?? 'Task was cancelled'))
);
}
@@ -16,15 +16,15 @@ export interface IErrorDetails {
errorType?: ServerErrorType;
}
export function hasDetails(error: Error): error is DetailsError {
export function hasDetails(error: Error | undefined | null): error is DetailsError {
return error instanceof DetailsError && error.hasDetails();
}
export function getErrorDetails(error: Error | DetailsError): IErrorDetails {
const exceptionMessage = hasDetails(error) ? error.errorMessage : error.message || error.name;
export function getErrorDetails(error: Error | DetailsError | undefined | null): IErrorDetails {
const exceptionMessage = (hasDetails(error) ? error.errorMessage : error?.message) || error?.name || 'Unknown error';
const details: IErrorDetails = {
name: error.name,
name: error?.name ?? 'Error',
message: exceptionMessage,
hasDetails: hasDetails(error),
};
@@ -60,6 +60,13 @@ export class WindowsService {
return null;
}
close(window: Window) {
const current = Array.from(this.windows.values())
.find(info => info.window === window);
current?.window.close();
}
async waitWindowsClose(window: Window): Promise<void> {
const current = Array.from(this.windows.values())
.find(info => info.window === window);
@@ -81,6 +88,7 @@ export class WindowsService {
if (this.windows.size === 0) {
clearInterval(this.trackWindowClose);
this.trackWindowClose = undefined;
}
});
}
@@ -6,6 +6,8 @@
* you may not use this file except in compliance with the License.
*/
import { observable } from 'mobx';
import { AdministrationScreenService } from '@cloudbeaver/core-administration';
import { AppAuthService, AuthInfoService, AuthProviderContext, AuthProviderService, AuthProvidersResource, AUTH_PROVIDER_LOCAL_ID, UserInfoResource } from '@cloudbeaver/core-authentication';
import { injectable, Bootstrap } from '@cloudbeaver/core-di';
@@ -18,6 +20,7 @@ import type { ObjectOrigin } from '@cloudbeaver/core-sdk';
import { WindowsService } from '@cloudbeaver/core-ui';
import { AuthDialogService } from './Dialog/AuthDialogService';
import type { IAuthOptions } from './IAuthOptions';
export type LogoutEventType = 'before' | 'after';
@@ -125,12 +128,17 @@ export class AuthenticationService extends Bootstrap {
}
}
private async auth(persistent: boolean, provider: string | null = null, link?: boolean) {
private async auth(persistent: boolean, providerId: string | null = null, link?: boolean) {
if (this.authPromise) {
return this.authPromise;
}
this.authPromise = this.authDialogService.showLoginForm(persistent, provider, link);
const loginFormOptions: IAuthOptions = observable({
providerId,
link,
});
this.authPromise = this.authDialogService.showLoginForm(persistent, loginFormOptions);
if (this.serverConfigResource.redirectOnFederatedAuth) {
await this.authProvidersResource.loadAll();
@@ -143,6 +151,10 @@ export class AuthenticationService extends Bootstrap {
if (configurableProvider?.configurations?.length === 1) {
const configuration = configurableProvider.configurations[0];
loginFormOptions.providerId = configurableProvider.id;
loginFormOptions.configurationId = configuration.id;
const user = await this.authInfoService.login(configurableProvider.id, {
configurationId: configuration.id,
linkUser: link,
@@ -18,6 +18,7 @@ import { useStyles } from '@cloudbeaver/core-theming';
import { TabsState, TabList, Tab, TabTitle, UNDERLINE_TAB_STYLES, BASE_TAB_STYLES } from '@cloudbeaver/core-ui';
import { AuthenticationService } from '../AuthenticationService';
import type { IAuthOptions } from '../IAuthOptions';
import { AuthDialogFooter } from './AuthDialogFooter';
import { AuthProviderForm } from './AuthProviderForm/AuthProviderForm';
import { ConfigurationsList } from './AuthProviderForm/ConfigurationsList';
@@ -62,41 +63,44 @@ const styles = css`
}
`;
interface IAuthPayload {
providerId: string | null;
link?: boolean;
}
export const AuthDialog: DialogComponent<IAuthPayload, null> = observer(function AuthDialog({
export const AuthDialog: DialogComponent<IAuthOptions, null> = observer(function AuthDialog({
payload: {
providerId,
configurationId,
link = false,
},
options,
rejectDialog,
}) {
const state = useAuthDialogState(providerId);
const errorDetails = useErrorDetails(state.exception);
const dialogData = useAuthDialogState(providerId, configurationId);
const errorDetails = useErrorDetails(dialogData.exception);
const authenticationService = useService(AuthenticationService);
const userInfo = useService(UserInfoResource);
const translate = useTranslate();
const state = dialogData.state;
const additional = userInfo.data !== null
&& state.activeProvider?.id !== undefined
&& !userInfo.hasToken(state.activeProvider.id);
const showTabs = (state.providers.length + state.configurations.length) > 1;
const showTabs = (dialogData.providers.length + dialogData.configurations.length) > 1;
const federate = state.tabId === FEDERATED_AUTH;
let dialogTitle = translate('authentication_login_dialog_title');
let subTitle: string | undefined;
let icon: string | undefined;
if (state.activeProvider) {
dialogTitle += `: ${state.activeProvider.label}`;
subTitle = state.activeProvider.description;
}
icon = state.activeProvider.icon;
if (federate) {
if (state.activeConfiguration) {
dialogTitle += `: ${state.activeConfiguration.displayName}`;
subTitle = state.activeConfiguration.description;
icon = state.activeConfiguration.iconURL || icon;
}
} else if (federate) {
dialogTitle += `: ${translate('authentication_auth_federated')}`;
subTitle = 'authentication_identity_provider_dialog_subtitle';
}
@@ -106,7 +110,7 @@ export const AuthDialog: DialogComponent<IAuthPayload, null> = observer(function
}
async function login() {
await state.login(link);
await dialogData.login(link);
rejectDialog();
}
@@ -120,7 +124,7 @@ export const AuthDialog: DialogComponent<IAuthPayload, null> = observer(function
return <TextPlaceholder>{translate('authentication_select_provider')}</TextPlaceholder>;
}
if (state.configure) {
if (dialogData.configure) {
return (
<TextPlaceholder>
{translate('authentication_provider_disabled')}
@@ -137,7 +141,7 @@ export const AuthDialog: DialogComponent<IAuthPayload, null> = observer(function
<AuthProviderForm
provider={provider}
credentials={state.credentials}
authenticate={state.authenticating}
authenticate={dialogData.authenticating}
/>
);
}
@@ -147,15 +151,15 @@ export const AuthDialog: DialogComponent<IAuthPayload, null> = observer(function
<CommonDialogWrapper
size='large'
title={dialogTitle}
icon={state.activeProvider?.icon}
icon={icon}
subTitle={subTitle}
footer={!federate && (
<AuthDialogFooter
authAvailable={!state.configure}
isAuthenticating={state.authenticating}
authAvailable={!dialogData.configure}
isAuthenticating={dialogData.authenticating}
onLogin={login}
>
{state.exception && (
{dialogData.exception && (
<ErrorMessage
text={errorDetails.details?.message || ''}
hasDetails={errorDetails.details?.hasDetails}
@@ -169,23 +173,23 @@ export const AuthDialog: DialogComponent<IAuthPayload, null> = observer(function
>
{showTabs && (
<TabList aria-label='Auth providers'>
{state.providers.map(provider => (
{dialogData.providers.map(provider => (
<Tab
key={provider.id}
tabId={provider.id}
title={provider.description || provider.label}
disabled={state.authenticating}
disabled={dialogData.authenticating}
onClick={() => { state.setActiveProvider(provider); }}
>
<TabTitle>{provider.label}</TabTitle>
</Tab>
))}
{state.configurations.length > 0 && (
{dialogData.configurations.length > 0 && (
<Tab
key={FEDERATED_AUTH}
tabId={FEDERATED_AUTH}
title={translate('authentication_auth_federated')}
disabled={state.authenticating}
disabled={dialogData.authenticating}
onClick={() => { state.setActiveProvider(null); }}
>
<TabTitle>{translate('authentication_auth_federated')}</TabTitle>
@@ -194,10 +198,18 @@ export const AuthDialog: DialogComponent<IAuthPayload, null> = observer(function
</TabList>
)}
<SubmittingForm {...use({ form: !federate })} onSubmit={login}>
<Loader state={state.loadingState}>
<Loader state={dialogData.loadingState}>
{() => federate
? (
<ConfigurationsList providers={state.configurations} onClose={rejectDialog} />
<ConfigurationsList
activeProvider={state.activeProvider}
activeConfiguration={state.activeConfiguration}
providers={dialogData.configurations}
onAuthorize={(provider, configuration) => {
state.setActiveConfiguration(provider, configuration);
}}
onClose={rejectDialog}
/>
)
: renderForm(state.activeProvider)}
</Loader>
@@ -9,6 +9,7 @@
import { injectable } from '@cloudbeaver/core-di';
import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs';
import type { IAuthOptions } from '../IAuthOptions';
import { AuthDialog } from './AuthDialog';
@injectable()
@@ -19,10 +20,11 @@ export class AuthDialogService {
showLoginForm(
persistent = false,
providerId: string | null = null,
link?: boolean
options: IAuthOptions = {
providerId: null,
}
): Promise<DialogueStateResult | null> {
return this.commonDialogService.open(AuthDialog, { providerId, link }, { persistent });
return this.commonDialogService.open(AuthDialog, options, { persistent });
}
closeLoginForm(promise: Promise<DialogueStateResult | null>): void {
@@ -10,12 +10,13 @@ import { observer } from 'mobx-react-lite';
import { useState } from 'react';
import styled, { css } from 'reshadow';
import { AuthInfoService, AuthProvider, comparePublicAuthConfigurations } from '@cloudbeaver/core-authentication';
import { Filter, IconOrImage, Link, Cell, getComputed, TextPlaceholder } from '@cloudbeaver/core-blocks';
import { AuthInfoService, AuthProvider, AuthProviderConfiguration, comparePublicAuthConfigurations } from '@cloudbeaver/core-authentication';
import { Filter, IconOrImage, Link, Cell, getComputed, TextPlaceholder, usePromiseState, Loader, Button } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { NotificationService } from '@cloudbeaver/core-events';
import { useTranslate } from '@cloudbeaver/core-localization';
import type { AuthProviderConfiguration } from '@cloudbeaver/core-sdk';
import type { ITask } from '@cloudbeaver/core-executor';
import { Translate, useTranslate } from '@cloudbeaver/core-localization';
import type { AuthProviderInfo, UserInfo } from '@cloudbeaver/core-sdk';
import { useStyles } from '@cloudbeaver/core-theming';
import { AuthenticationService } from '../../AuthenticationService';
@@ -25,6 +26,7 @@ const styles = css`
display: flex;
flex-direction: column;
overflow: auto;
flex: 1;
}
Filter {
margin: 0 24px 12px 24px;
@@ -41,20 +43,39 @@ const styles = css`
width: 100%;
height: 100%;
}
center {
margin: auto;
}
`;
const loaderStyle = css`
ExceptionMessage {
padding: 24px;
}
`;
interface IProviderConfiguration {
provider: AuthProvider;
configuration: AuthProviderConfiguration;
}
interface Props {
activeProvider: AuthProviderInfo | null;
activeConfiguration: AuthProviderConfiguration | null;
providers: AuthProvider[];
onAuthorize?: (provider: AuthProvider | null, configuration: AuthProviderConfiguration | null) => void;
onClose?: () => void;
className?: string;
}
export const ConfigurationsList = observer<Props>(function ConfigurationsList({ providers, onClose, className }) {
export const ConfigurationsList = observer<Props>(function ConfigurationsList({
activeProvider,
activeConfiguration,
providers,
onAuthorize,
onClose,
className,
}) {
const authInfoService = useService(AuthInfoService);
const authenticationService = useService(AuthenticationService);
const notificationService = useService(NotificationService);
@@ -62,6 +83,8 @@ export const ConfigurationsList = observer<Props>(function ConfigurationsList({
const style = useStyles(styles);
const [search, setSearch] = useState('');
const [authTask, setAuthTask] = useState<ITask<UserInfo | null> | null>(null);
const authTaskState = usePromiseState(authTask);
const configurations = getComputed<IProviderConfiguration[]>(() => providers.map(
provider => (
(provider.configurations || [])
@@ -87,15 +110,23 @@ export const ConfigurationsList = observer<Props>(function ConfigurationsList({
async function auth({ provider, configuration }: IProviderConfiguration) {
try {
const user = await authInfoService.login(provider.id, {
onAuthorize?.(provider, configuration);
const authTask = authInfoService.login(provider.id, {
configurationId: configuration.id,
});
setAuthTask(authTask);
const user = await authTask;
if (user) {
onClose?.();
}
setAuthTask(null);
} catch (exception: any) {
notificationService.logException(exception, 'Federated authentication error');
} finally {
onAuthorize?.(null, null);
}
}
@@ -117,6 +148,29 @@ export const ConfigurationsList = observer<Props>(function ConfigurationsList({
);
}
if (activeProvider && activeConfiguration) {
return styled(style)(
<container className={className}>
<Loader
state={authTaskState}
style={loaderStyle}
message="authentication_authorizing"
hideException
>
<center>
<Button
type="button"
mod={['unelevated']}
onClick={() => auth({ provider: activeProvider, configuration: activeConfiguration })}
>
<Translate token='authentication_login' />
</Button>
</center>
</Loader>
</container>
);
}
return styled(style)(
<container className={className}>
{configurations.length >= 10 && (
@@ -148,6 +202,13 @@ export const ConfigurationsList = observer<Props>(function ConfigurationsList({
);
})}
</list>
<Loader
state={authTaskState}
style={loaderStyle}
message="authentication_authorizing"
overlay
hideException
/>
</container>
);
});
@@ -6,11 +6,11 @@
* you may not use this file except in compliance with the License.
*/
import { computed, observable } from 'mobx';
import { action, computed, observable } from 'mobx';
import { useEffect } from 'react';
import { AdministrationScreenService } from '@cloudbeaver/core-administration';
import { AuthInfoService, AuthProvider, AuthProvidersResource, IAuthCredentials } from '@cloudbeaver/core-authentication';
import { AuthInfoService, AuthProvider, AuthProviderConfiguration, AuthProvidersResource, IAuthCredentials } from '@cloudbeaver/core-authentication';
import { ILoadableState, useMapResource, useObservableRef } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { NotificationService } from '@cloudbeaver/core-events';
@@ -18,25 +18,32 @@ import { CachedMapAllKey } from '@cloudbeaver/core-sdk';
import { FEDERATED_AUTH } from './FEDERATED_AUTH';
interface IState {
tabId: string | null;
activeProvider: AuthProvider | null;
interface IData {
state: IState;
exception: Error | null;
authenticating: boolean;
destroyed: boolean;
configure: boolean;
adminPageActive: boolean;
credentials: IAuthCredentials;
loadingState: ILoadableState;
providers: AuthProvider[];
configurations: AuthProvider[];
setTabId: (tabId: string) => void;
setActiveProvider: (provider: AuthProvider | null) => void;
login: (link: boolean) => Promise<void>;
}
export function useAuthDialogState(providerId: string | null): IState {
interface IState {
tabId: string | null;
activeProvider: AuthProvider | null;
activeConfiguration: AuthProviderConfiguration | null;
credentials: IAuthCredentials;
setTabId: (tabId: string) => void;
setActiveProvider: (provider: AuthProvider | null) => void;
setActiveConfiguration: (provider: AuthProvider | null, configuration: AuthProviderConfiguration | null) => void;
}
export function useAuthDialogState(providerId: string | null, configurationId?: string): IData {
const authProvidersResource = useMapResource(useAuthDialogState, AuthProvidersResource, CachedMapAllKey);
const administrationScreenService = useService(AdministrationScreenService);
const authInfoService = useService(AuthInfoService);
@@ -48,9 +55,43 @@ export function useAuthDialogState(providerId: string | null): IState {
.filter(notEmptyProvider)
.sort(compareProviders);
const state = useObservableRef<IState>(() => ({
tabId: null,
activeProvider: null,
activeConfiguration: null,
credentials: {
profile: '0',
credentials: {},
},
setTabId(tabId: string): void {
this.tabId = tabId;
},
setActiveProvider(provider: AuthProvider | null): void {
this.activeProvider = provider;
this.credentials.profile = '0';
this.credentials.credentials = {};
this.activeConfiguration = null;
},
setActiveConfiguration(
provider: AuthProvider | null,
configuration: AuthProviderConfiguration | null
): void {
this.setActiveProvider(provider);
this.activeConfiguration = configuration;
},
}), {
tabId: observable.ref,
activeProvider: observable.ref,
activeConfiguration: observable.ref,
credentials: observable,
setActiveProvider: action,
setActiveConfiguration: action,
}, false);
const activeProviders = providers
.filter(provider => {
if (provider.configurable) {
if (provider.configurable || state.activeConfiguration) {
return false;
}
@@ -83,45 +124,30 @@ export function useAuthDialogState(providerId: string | null): IState {
tabIds.push(FEDERATED_AUTH);
}
const state = useObservableRef<IState>(() => ({
tabId: null,
activeProvider: null,
const data = useObservableRef<IData>(() => ({
exception: null,
authenticating: false,
destroyed: false,
credentials: {
profile: '0',
credentials: {},
},
loadingState: authProvidersResource,
get configure(): boolean {
if (this.activeProvider) {
if (this.adminPageActive && authProvidersResource.resource.isPrimary(this.activeProvider.id)) {
if (state.activeProvider) {
if (this.adminPageActive && authProvidersResource.resource.isPrimary(state.activeProvider.id)) {
return false;
}
return !authProvidersResource.resource.isAuthEnabled(this.activeProvider.id);
return !authProvidersResource.resource.isAuthEnabled(state.activeProvider.id);
}
return false;
},
setTabId(tabId: string): void {
this.tabId = tabId;
},
setActiveProvider(provider: AuthProvider | null): void {
this.activeProvider = provider;
this.credentials.profile = '0';
this.credentials.credentials = {};
},
async login(link: boolean): Promise<void> {
if (!this.activeProvider || this.authenticating) {
if (!state.activeProvider || this.authenticating) {
return;
}
this.authenticating = true;
try {
await authInfoService.login(this.activeProvider.id, {
credentials: this.credentials,
await authInfoService.login(state.activeProvider.id, {
credentials: state.credentials,
linkUser: link,
});
} catch (exception: any) {
@@ -136,29 +162,33 @@ export function useAuthDialogState(providerId: string | null): IState {
}
},
}), {
tabId: observable.ref,
activeProvider: observable.ref,
state: observable.ref,
exception: observable.ref,
authenticating: observable.ref,
configure: computed,
adminPageActive: observable.ref,
credentials: observable,
}, {
state,
adminPageActive,
providers: activeProviders,
configurations,
});
useEffect(() => () => { state.destroyed = true; }, []);
useEffect(() => () => { data.destroyed = true; }, []);
if (tabIds.length > 0 && (state.tabId === null || !tabIds.includes(state.tabId))
) {
if (tabIds.length > 0 && (state.tabId === null || !tabIds.includes(state.tabId))) {
const tabId = tabIds[0];
state.setTabId(tabId);
state.setActiveProvider(activeProviders.find(provider => provider.id === tabId) || null);
const provider = activeProviders.find(provider => provider.id === tabId) || null;
const configuration = provider?.configurations?.find(
configuration => configuration.id === configurationId
) ?? null;
state.setActiveConfiguration(provider, configuration);
}
return state;
return data;
}
function notEmptyProvider(obj: any): obj is AuthProvider {
@@ -0,0 +1,13 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2022 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
export interface IAuthOptions {
providerId: string | null;
configurationId?: string;
link?: boolean;
}
@@ -40,7 +40,7 @@ export class PluginBootstrap extends Bootstrap {
label: 'authentication_login',
tooltip: 'authentication_login',
},
{ onSelect: () => this.authDialogService.showLoginForm(false, null, true) }
{ onSelect: () => this.authDialogService.showLoginForm(false, { providerId: null, link: true }) }
),
];
}
@@ -3,6 +3,7 @@ export default [
['authentication_login', 'Login'],
['authentication_logout', 'Logout'],
['authentication_authenticate', 'Authenticate'],
['authentication_authorizing', 'Authorizing...'],
['authentication_auth_federated', 'Federated'],
['authentication_select_provider', 'Select available provider'],
['authentication_configure', 'Please configure authentication methods'],
@@ -3,6 +3,7 @@ export default [
['authentication_login', 'Login'],
['authentication_logout', 'Logout'],
['authentication_authenticate', 'Autentica'],
['authentication_authorizing', 'Authorizing...'],
['authentication_auth_federated', 'Federated'],
['authentication_request_token', 'Autenticazione addizionale richiesta'],
['authentication_user_name', 'Nome utente'],
@@ -3,6 +3,7 @@ export default [
['authentication_login', 'Войти'],
['authentication_logout', 'Выйти'],
['authentication_authenticate', 'Аутентифицироваться'],
['authentication_authorizing', 'Авторизация...'],
['authentication_auth_federated', 'Федеративная'],
['authentication_select_provider', 'Выберите способ аутентификации'],
['authentication_configure', 'Пожалуйста настройте способы аутентификации'],
@@ -3,6 +3,7 @@ export default [
['authentication_login', '登录'],
['authentication_logout', '登出'],
['authentication_authenticate', '认证'],
['authentication_authorizing', 'Authorizing...'],
['authentication_auth_federated', '联合认证'],
['authentication_select_provider', '选择可用提供者'],
['authentication_configure', '请配置认证方法'],