Merge pull request #449 from dbeaver/feat/identity-providers-public

feat(plugin-authentication): CB-1062 add support for identity provide…
This commit is contained in:
Alexey
2021-08-20 01:22:46 +03:00
committed by GitHub
10 changed files with 312 additions and 24 deletions
+62
View File
@@ -0,0 +1,62 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2021 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 styled, { css } from 'reshadow';
import { composes, useStyles } from '@cloudbeaver/core-theming';
interface Props {
description?: string;
before?: JSX.Element;
className?: string;
}
const styles = composes(
css`
cell {
composes: theme-ripple from global;
}
`,
css`
main {
display: flex;
align-items: center;
padding: 12px;
}
before {
margin-right: 12px;
width: 24px;
height: 24px;
flex-shrink: 0;
}
info {
composes: theme-typography--body2 from global;
line-height: 1.4;
display: flex;
flex-direction: column;
font-weight: 500;
}
description {
composes: theme-typography--caption from global;
line-height: 1.2;
}
`);
export const Cell: React.FC<Props> = function Cell({ before, description, className, children }) {
return styled(useStyles(styles))(
<cell className={className}>
<main>
<before>{before && before}</before>
<info>
{children}
{description && <description>{description}</description>}
</info>
</main>
</cell>
);
};
@@ -25,6 +25,9 @@ const styles = css`
InputFieldNew {
display: none;
width: 300px;
&[|max] {
width: 100%;
}
&[|toggled] {
display: block;
}
@@ -67,6 +70,7 @@ interface BaseProps {
toggleMode?: boolean;
placeholder?: string;
disabled?: boolean;
max?: boolean;
className?: string;
onToggle?: (status: boolean) => void;
onKeyDown?: (event: React.KeyboardEvent<HTMLInputElement>) => void;
@@ -93,6 +97,7 @@ export const Filter: React.FC<ControlledProps | ObjectsProps<any, any>> = observ
toggleMode,
placeholder,
disabled,
max,
className,
onFilter,
onToggle,
@@ -152,7 +157,7 @@ export const Filter: React.FC<ControlledProps | ObjectsProps<any, any>> = observ
value={value}
onChange={filter}
onKeyDown={onKeyDown}
{...use({ toggled })}
{...use({ toggled, max })}
/>
<IconButton
name='search'
+20 -3
View File
@@ -6,14 +6,31 @@
* you may not use this file except in compliance with the License.
*/
export const Link: React.FC<React.AnchorHTMLAttributes<HTMLAnchorElement>> = function Link({
import styled, { css, use } from 'reshadow';
interface Props extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
wrapper?: boolean;
}
const styles = css`
a[|wrapper] {
&, &:hover, &:focus, &:active {
color: inherit;
text-decoration: none;
outline: none;
}
}
`;
export const Link: React.FC<Props> = function Link({
wrapper,
className,
children,
...rest
}) {
return (
return styled(styles)(
<div className={className}>
<a {...rest}>{children}</a>
<a {...use({ wrapper })} {...rest}>{children}</a>
</div>
);
};
+1
View File
@@ -127,6 +127,7 @@ export * from './FormControls/SubmittingForm';
export * from './FormControls/Textarea';
export * from './FormControls/TextareaNew';
export * from './Link';
export * from './Cell';
export * from './ErrorMessage';
export * from './StatusMessage';
export * from './ExceptionMessage';
@@ -18,6 +18,16 @@ query getAuthProviders {
# features
#}
configurable
configurations {
id
displayName
iconURL
description
signInLink
signOutLink
}
credentialParameters {
id
displayName
+68 -6
View File
@@ -16,6 +16,15 @@ export interface Scalars {
Object: any;
}
export interface AdminAuthProviderConfiguration {
provider: Scalars['ID'];
id: Scalars['ID'];
displayName: Scalars['String'];
iconURL?: Maybe<Scalars['String']>;
description?: Maybe<Scalars['String']>;
parameters: Scalars['Object'];
}
export interface AdminConnectionGrantInfo {
connectionId: Scalars['ID'];
subjectId: Scalars['ID'];
@@ -81,19 +90,30 @@ export interface AuthCredentialInfo {
id: Scalars['ID'];
displayName: Scalars['String'];
description?: Maybe<Scalars['String']>;
admin?: Maybe<Scalars['Boolean']>;
user?: Maybe<Scalars['Boolean']>;
identifying?: Maybe<Scalars['Boolean']>;
admin: Scalars['Boolean'];
user: Scalars['Boolean'];
identifying: Scalars['Boolean'];
possibleValues?: Maybe<Array<Maybe<Scalars['String']>>>;
encryption?: Maybe<AuthCredentialEncryption>;
}
export interface AuthProviderConfiguration {
id: Scalars['ID'];
displayName: Scalars['String'];
iconURL?: Maybe<Scalars['String']>;
description?: Maybe<Scalars['String']>;
signInLink?: Maybe<Scalars['String']>;
signOutLink?: Maybe<Scalars['String']>;
}
export interface AuthProviderInfo {
id: Scalars['ID'];
label: Scalars['String'];
icon?: Maybe<Scalars['ID']>;
description?: Maybe<Scalars['String']>;
defaultProvider?: Maybe<Scalars['Boolean']>;
defaultProvider: Scalars['Boolean'];
configurable: Scalars['Boolean'];
configurations?: Maybe<AuthProviderConfiguration[]>;
credentialParameters: AuthCredentialInfo[];
}
@@ -579,6 +599,7 @@ export interface Query {
dataTransferExportDataFromContainer: AsyncTaskInfo;
dataTransferExportDataFromResults: AsyncTaskInfo;
dataTransferRemoveDataFile?: Maybe<Scalars['Boolean']>;
deleteAuthProviderConfiguration: Scalars['Boolean'];
deleteConnectionConfiguration?: Maybe<Scalars['Boolean']>;
deleteRole?: Maybe<Scalars['Boolean']>;
deleteUser?: Maybe<Scalars['Boolean']>;
@@ -586,6 +607,8 @@ export interface Query {
getConnectionSubjectAccess: AdminConnectionGrantInfo[];
getSubjectConnectionAccess: AdminConnectionGrantInfo[];
grantUserRole?: Maybe<Scalars['Boolean']>;
listAuthProviderConfigurationParameters: ObjectPropertyInfo[];
listAuthProviderConfigurations: AdminAuthProviderConfiguration[];
listPermissions: Array<Maybe<AdminPermissionInfo>>;
listRoles: Array<Maybe<AdminRoleInfo>>;
listUsers: Array<Maybe<AdminUserInfo>>;
@@ -597,6 +620,7 @@ export interface Query {
networkHandlers: NetworkHandlerDescriptor[];
readSessionLog: LogEntry[];
revokeUserRole?: Maybe<Scalars['Boolean']>;
saveAuthProviderConfiguration: Scalars['Boolean'];
searchConnections: AdminConnectionSearchInfo[];
serverConfig: ServerConfig;
sessionPermissions: Array<Maybe<Scalars['ID']>>;
@@ -611,6 +635,7 @@ export interface Query {
sqlListContexts: Array<Maybe<SqlContextInfo>>;
sqlSupportedOperations: DataTypeLogicalOperation[];
templateConnections: ConnectionInfo[];
tryFederatedLogin: UserAuthToken;
updateConnectionConfiguration: ConnectionInfo;
updateRole: AdminRoleInfo;
userConnections: ConnectionInfo[];
@@ -627,12 +652,14 @@ export interface QueryAuthChangeLocalPasswordArgs {
export interface QueryAuthLoginArgs {
provider: Scalars['ID'];
configuration?: Maybe<Scalars['ID']>;
credentials: Scalars['Object'];
linkUser?: Maybe<Scalars['Boolean']>;
}
export interface QueryAuthLogoutArgs {
provider?: Maybe<Scalars['ID']>;
configuration?: Maybe<Scalars['ID']>;
}
export interface QueryConfigureServerArgs {
@@ -683,6 +710,10 @@ export interface QueryDataTransferRemoveDataFileArgs {
dataFileId: Scalars['String'];
}
export interface QueryDeleteAuthProviderConfigurationArgs {
id: Scalars['ID'];
}
export interface QueryDeleteConnectionConfigurationArgs {
id: Scalars['ID'];
}
@@ -712,6 +743,14 @@ export interface QueryGrantUserRoleArgs {
roleId: Scalars['ID'];
}
export interface QueryListAuthProviderConfigurationParametersArgs {
providerId: Scalars['ID'];
}
export interface QueryListAuthProviderConfigurationsArgs {
providerId?: Maybe<Scalars['ID']>;
}
export interface QueryListRolesArgs {
roleId?: Maybe<Scalars['ID']>;
}
@@ -755,6 +794,15 @@ export interface QueryRevokeUserRoleArgs {
roleId: Scalars['ID'];
}
export interface QuerySaveAuthProviderConfigurationArgs {
providerId: Scalars['ID'];
id: Scalars['ID'];
displayName?: Maybe<Scalars['String']>;
iconURL?: Maybe<Scalars['String']>;
description?: Maybe<Scalars['String']>;
parameters?: Maybe<Scalars['Object']>;
}
export interface QuerySearchConnectionsArgs {
hostNames: Array<Scalars['String']>;
}
@@ -809,6 +857,10 @@ export interface QuerySqlSupportedOperationsArgs {
attributeIndex: Scalars['Int'];
}
export interface QueryTryFederatedLoginArgs {
provider: Scalars['ID'];
}
export interface QueryUpdateConnectionConfigurationArgs {
id: Scalars['ID'];
config: ConnectionConfig;
@@ -1013,6 +1065,7 @@ export interface SessionInfo {
export interface UserAuthToken {
authProvider: Scalars['ID'];
authConfiguration?: Maybe<Scalars['ID']>;
loginTime: Scalars['DateTime'];
userId: Scalars['String'];
displayName: Scalars['String'];
@@ -1075,8 +1128,8 @@ export type GetAuthProvidersQueryVariables = Exact<{ [key: string]: never }>;
export interface GetAuthProvidersQuery {
providers: Array<(
Pick<AuthProviderInfo, 'id' | 'label' | 'icon' | 'description' | 'defaultProvider'>
& { credentialParameters: Array<Pick<AuthCredentialInfo, 'id' | 'displayName' | 'description' | 'admin' | 'user' | 'identifying' | 'possibleValues' | 'encryption'>> }
Pick<AuthProviderInfo, 'id' | 'label' | 'icon' | 'description' | 'defaultProvider' | 'configurable'>
& { configurations?: Maybe<Array<Pick<AuthProviderConfiguration, 'id' | 'displayName' | 'iconURL' | 'description' | 'signInLink' | 'signOutLink'>>>; credentialParameters: Array<Pick<AuthCredentialInfo, 'id' | 'displayName' | 'description' | 'admin' | 'user' | 'identifying' | 'possibleValues' | 'encryption'>> }
)>;
}
@@ -1977,6 +2030,15 @@ export const GetAuthProvidersDocument = `
icon
description
defaultProvider
configurable
configurations {
id
displayName
iconURL
description
signInLink
signOutLink
}
credentialParameters {
id
displayName
@@ -6,8 +6,10 @@
* you may not use this file except in compliance with the License.
*/
import { computed } from 'mobx';
import { observer } from 'mobx-react-lite';
import styled, { css } from 'reshadow';
import { useMemo } from 'react';
import styled, { css, use } from 'reshadow';
import { AdministrationScreenService } from '@cloudbeaver/core-administration';
import { UserInfoResource } from '@cloudbeaver/core-authentication';
@@ -22,6 +24,7 @@ import { composes, useStyles } from '@cloudbeaver/core-theming';
import { AuthDialogController } from './AuthDialogController';
import { AuthDialogFooter } from './AuthDialogFooter';
import { AuthProviderForm } from './AuthProviderForm/AuthProviderForm';
import { ConfigurationsList } from './AuthProviderForm/ConfigurationsList';
const styles = composes(
css`
@@ -33,10 +36,13 @@ const styles = composes(
CommonDialogWrapper {
min-height: 400px;
min-width: 500px;
max-width: min-content;
}
SubmittingForm {
overflow: auto;
margin: auto;
&[|form] {
margin: auto;
}
}
SubmittingForm, AuthProviderForm {
flex: 1;
@@ -56,6 +62,9 @@ const styles = composes(
flex-direction: column;
padding: 18px 24px;
}
ConfigurationsList {
margin-top: 12px;
}
ErrorMessage {
flex: 1;
}
@@ -85,20 +94,35 @@ export const AuthDialog: DialogComponent<IAuthPayload, null> = observer(function
controller.setAdminMode(administrationScreenService.activeScreen !== null);
}
const showTabs = !provider && controller.providers.length > 1;
const dialogTitle = `${controller.provider?.label || ''} ${translate('authentication_login_dialog_title')}`;
const additional = userInfo.data !== null
&& controller.provider?.id !== undefined
&& !userInfo.hasToken(controller.provider.id);
const providers = useMemo(() => computed(() =>
controller.providers.filter(provider => !provider.configurable || !!provider.configurations?.length)
), [controller.providers]).get();
const showTabs = !provider && providers.length > 1;
const configurable = !!controller.provider?.configurable;
const dialogTitle = `${controller.provider?.label || ''} ${translate('authentication_login_dialog_title')}`;
let subTitle: string | undefined;
if (configurable) {
subTitle = translate('authentication_identity_provider_dialog_subtitle');
}
if (additional) {
subTitle = translate('authentication_request_token');
}
return styled(useStyles(styles, UNDERLINE_TAB_STYLES))(
<TabsState currentTabId={controller.provider?.id}>
<CommonDialogWrapper
title={dialogTitle}
icon={controller.provider?.icon}
subTitle={additional ? translate('authentication_request_token') : undefined}
footer={(
subTitle={subTitle}
footer={!configurable && (
<AuthDialogFooter
isAuthenticating={controller.isAuthenticating}
onLogin={controller.login}
@@ -116,7 +140,7 @@ export const AuthDialog: DialogComponent<IAuthPayload, null> = observer(function
>
{showTabs && (
<TabList aria-label='Auth providers'>
{controller.providers.map(provider => (
{providers.map(provider => (
<Tab
key={provider.id}
tabId={provider.id}
@@ -129,13 +153,20 @@ export const AuthDialog: DialogComponent<IAuthPayload, null> = observer(function
))}
</TabList>
)}
<SubmittingForm onSubmit={controller.login}>
<SubmittingForm {...use({ form: !configurable })} onSubmit={controller.login}>
{controller.provider && (
<AuthProviderForm
provider={controller.provider}
credentials={controller.credentials}
authenticate={controller.isAuthenticating}
/>
configurable ? (
<ConfigurationsList
configurations={controller.provider.configurations!}
providerIcon={controller.provider.icon}
/>
) : (
<AuthProviderForm
provider={controller.provider}
credentials={controller.credentials}
authenticate={controller.isAuthenticating}
/>
)
)}
{controller.isLoading && <Loader />}
{!controller.isLoading && !controller.provider && <>Select available provider</>}
@@ -0,0 +1,96 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2021 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 } from 'mobx';
import { observer } from 'mobx-react-lite';
import { useState, useMemo } from 'react';
import styled, { css } from 'reshadow';
import { Filter, IconOrImage, Link, Cell } from '@cloudbeaver/core-blocks';
import { useTranslate } from '@cloudbeaver/core-localization';
import type { AuthProviderConfiguration } from '@cloudbeaver/core-sdk';
import { composes, useStyles } from '@cloudbeaver/core-theming';
interface Props {
configurations: AuthProviderConfiguration[];
providerIcon?: string;
className?: string;
}
const styles = composes(
css`
Cell {
composes: theme-border-color-secondary from global;
}
`,
css`
container {
display: flex;
flex-direction: column;
overflow: auto;
}
Filter {
margin-bottom: 12px;
}
list {
overflow: auto;
}
Cell {
border-bottom: 1px solid;
}
IconOrImage {
width: 100%;
height: 100%;
}
`);
export const ConfigurationsList: React.FC<Props> = observer(function ConfigurationsList({ configurations, providerIcon, className }) {
const translate = useTranslate();
const [search, setSearch] = useState('');
const filteredConfigurations = useMemo(() => computed(() => {
if (!search) {
return configurations;
}
return configurations.filter(configuration => {
const target = `${configuration.displayName}${configuration.description || ''}`;
return target.toUpperCase().includes(search.toUpperCase());
});
}), [search, configurations]);
return styled(useStyles(styles))(
<container className={className}>
{configurations.length >= 10 && (
<Filter
placeholder={translate('authentication_identity_provider_search_placeholder')}
value={search}
max
onFilter={setSearch}
/>
)}
<list>
{filteredConfigurations.get().map(configuration => {
const icon = configuration.iconURL || providerIcon;
const title = `${configuration.displayName}\n${configuration.description || ''}`;
return (
<Link key={configuration.id} href={configuration.signInLink} title={title} wrapper>
<Cell
before={icon ? <IconOrImage icon={icon} /> : undefined}
description={configuration.description}
>
{configuration.displayName}
</Cell>
</Link>
);
})}
</list>
</container>
);
});
@@ -4,6 +4,8 @@ export default [
['authentication_logout', 'Logout'],
['authentication_authenticate', 'Authenticate'],
['authentication_request_token', 'Additional authentication is required'],
['authentication_identity_provider_search_placeholder', 'Search for configuration name or description...'],
['authentication_identity_provider_dialog_subtitle', 'Choose configuration you want to sign in with'],
['authentication_administration_user_connections_user_add', 'User Creation'],
['authentication_administration_user_connections_user_new', 'New user'],
['authentication_administration_user_connections_access_load_fail', 'User\'s granted connections loading failed'],
@@ -2,6 +2,8 @@ export default [
['authentication_login_dialog_title', 'Авторизация'],
['authentication_login', 'Войти'],
['authentication_logout', 'Выйти'],
['authentication_identity_provider_search_placeholder', 'Поиск по имени или описанию конфигурации...'],
['authentication_identity_provider_dialog_subtitle', 'Выберите конфигурацию с помощью которой вы хотите войти'],
['authentication_administration_user_connections_user_add', 'Создание пользователя'],
['authentication_administration_user_connections_user_new', 'Новый пользователь'],
['authentication_administration_user_connections_access_load_fail', 'Не удалось загрузить доступные пользователю коннекшены'],