From a084ba176fa0af51b0687d93544faf2597390b54 Mon Sep 17 00:00:00 2001 From: Naumov Alexey Date: Thu, 19 Aug 2021 23:44:56 +0300 Subject: [PATCH] feat(plugin-authentication): CB-1062 add support for identity providers (public) --- webapp/packages/core-blocks/src/Cell.tsx | 62 ++++++++++++ .../core-blocks/src/FormControls/Filter.tsx | 7 +- webapp/packages/core-blocks/src/Link.tsx | 23 ++++- webapp/packages/core-blocks/src/index.ts | 1 + .../authentication/getAuthProviders.gql | 10 ++ webapp/packages/core-sdk/src/sdk.ts | 74 ++++++++++++-- .../src/Dialog/AuthDialog.tsx | 59 +++++++++--- .../AuthProviderForm/ConfigurationsList.tsx | 96 +++++++++++++++++++ .../plugin-authentication/src/locales/en.ts | 2 + .../plugin-authentication/src/locales/ru.ts | 2 + 10 files changed, 312 insertions(+), 24 deletions(-) create mode 100644 webapp/packages/core-blocks/src/Cell.tsx create mode 100644 webapp/packages/plugin-authentication/src/Dialog/AuthProviderForm/ConfigurationsList.tsx diff --git a/webapp/packages/core-blocks/src/Cell.tsx b/webapp/packages/core-blocks/src/Cell.tsx new file mode 100644 index 0000000000..567400088e --- /dev/null +++ b/webapp/packages/core-blocks/src/Cell.tsx @@ -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 = function Cell({ before, description, className, children }) { + return styled(useStyles(styles))( + +
+ {before && before} + + {children} + {description && {description}} + +
+
+ ); +}; diff --git a/webapp/packages/core-blocks/src/FormControls/Filter.tsx b/webapp/packages/core-blocks/src/FormControls/Filter.tsx index 01b9ae9d45..1e5782b1f4 100644 --- a/webapp/packages/core-blocks/src/FormControls/Filter.tsx +++ b/webapp/packages/core-blocks/src/FormControls/Filter.tsx @@ -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) => void; @@ -93,6 +97,7 @@ export const Filter: React.FC> = observ toggleMode, placeholder, disabled, + max, className, onFilter, onToggle, @@ -152,7 +157,7 @@ export const Filter: React.FC> = observ value={value} onChange={filter} onKeyDown={onKeyDown} - {...use({ toggled })} + {...use({ toggled, max })} /> > = function Link({ +import styled, { css, use } from 'reshadow'; + +interface Props extends React.AnchorHTMLAttributes { + wrapper?: boolean; +} + +const styles = css` + a[|wrapper] { + &, &:hover, &:focus, &:active { + color: inherit; + text-decoration: none; + outline: none; + } + } +`; + +export const Link: React.FC = function Link({ + wrapper, className, children, ...rest }) { - return ( + return styled(styles)( ); }; diff --git a/webapp/packages/core-blocks/src/index.ts b/webapp/packages/core-blocks/src/index.ts index b600a6a073..36bd013e14 100644 --- a/webapp/packages/core-blocks/src/index.ts +++ b/webapp/packages/core-blocks/src/index.ts @@ -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'; diff --git a/webapp/packages/core-sdk/src/queries/authentication/getAuthProviders.gql b/webapp/packages/core-sdk/src/queries/authentication/getAuthProviders.gql index 79906897e7..7bb87db332 100644 --- a/webapp/packages/core-sdk/src/queries/authentication/getAuthProviders.gql +++ b/webapp/packages/core-sdk/src/queries/authentication/getAuthProviders.gql @@ -18,6 +18,16 @@ query getAuthProviders { # features #} + configurable + configurations { + id + displayName + iconURL + description + signInLink + signOutLink + } + credentialParameters { id displayName diff --git a/webapp/packages/core-sdk/src/sdk.ts b/webapp/packages/core-sdk/src/sdk.ts index 16ee2ae459..698f20dc23 100644 --- a/webapp/packages/core-sdk/src/sdk.ts +++ b/webapp/packages/core-sdk/src/sdk.ts @@ -16,6 +16,15 @@ export interface Scalars { Object: any; } +export interface AdminAuthProviderConfiguration { + provider: Scalars['ID']; + id: Scalars['ID']; + displayName: Scalars['String']; + iconURL?: Maybe; + description?: Maybe; + 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; - admin?: Maybe; - user?: Maybe; - identifying?: Maybe; + admin: Scalars['Boolean']; + user: Scalars['Boolean']; + identifying: Scalars['Boolean']; possibleValues?: Maybe>>; encryption?: Maybe; } +export interface AuthProviderConfiguration { + id: Scalars['ID']; + displayName: Scalars['String']; + iconURL?: Maybe; + description?: Maybe; + signInLink?: Maybe; + signOutLink?: Maybe; +} + export interface AuthProviderInfo { id: Scalars['ID']; label: Scalars['String']; icon?: Maybe; description?: Maybe; - defaultProvider?: Maybe; + defaultProvider: Scalars['Boolean']; + configurable: Scalars['Boolean']; + configurations?: Maybe; credentialParameters: AuthCredentialInfo[]; } @@ -579,6 +599,7 @@ export interface Query { dataTransferExportDataFromContainer: AsyncTaskInfo; dataTransferExportDataFromResults: AsyncTaskInfo; dataTransferRemoveDataFile?: Maybe; + deleteAuthProviderConfiguration: Scalars['Boolean']; deleteConnectionConfiguration?: Maybe; deleteRole?: Maybe; deleteUser?: Maybe; @@ -586,6 +607,8 @@ export interface Query { getConnectionSubjectAccess: AdminConnectionGrantInfo[]; getSubjectConnectionAccess: AdminConnectionGrantInfo[]; grantUserRole?: Maybe; + listAuthProviderConfigurationParameters: ObjectPropertyInfo[]; + listAuthProviderConfigurations: AdminAuthProviderConfiguration[]; listPermissions: Array>; listRoles: Array>; listUsers: Array>; @@ -597,6 +620,7 @@ export interface Query { networkHandlers: NetworkHandlerDescriptor[]; readSessionLog: LogEntry[]; revokeUserRole?: Maybe; + saveAuthProviderConfiguration: Scalars['Boolean']; searchConnections: AdminConnectionSearchInfo[]; serverConfig: ServerConfig; sessionPermissions: Array>; @@ -611,6 +635,7 @@ export interface Query { sqlListContexts: Array>; 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; credentials: Scalars['Object']; linkUser?: Maybe; } export interface QueryAuthLogoutArgs { provider?: Maybe; + configuration?: Maybe; } 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; +} + export interface QueryListRolesArgs { roleId?: Maybe; } @@ -755,6 +794,15 @@ export interface QueryRevokeUserRoleArgs { roleId: Scalars['ID']; } +export interface QuerySaveAuthProviderConfigurationArgs { + providerId: Scalars['ID']; + id: Scalars['ID']; + displayName?: Maybe; + iconURL?: Maybe; + description?: Maybe; + parameters?: Maybe; +} + export interface QuerySearchConnectionsArgs { hostNames: Array; } @@ -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; 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 - & { credentialParameters: Array> } + Pick + & { configurations?: Maybe>>; credentialParameters: Array> } )>; } @@ -1977,6 +2030,15 @@ export const GetAuthProvidersDocument = ` icon description defaultProvider + configurable + configurations { + id + displayName + iconURL + description + signInLink + signOutLink + } credentialParameters { id displayName diff --git a/webapp/packages/plugin-authentication/src/Dialog/AuthDialog.tsx b/webapp/packages/plugin-authentication/src/Dialog/AuthDialog.tsx index c2be7c71d2..c7adb037d8 100644 --- a/webapp/packages/plugin-authentication/src/Dialog/AuthDialog.tsx +++ b/webapp/packages/plugin-authentication/src/Dialog/AuthDialog.tsx @@ -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 = 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))( = observer(function > {showTabs && ( - {controller.providers.map(provider => ( + {providers.map(provider => ( = observer(function ))} )} - + {controller.provider && ( - + configurable ? ( + + ) : ( + + ) )} {controller.isLoading && } {!controller.isLoading && !controller.provider && <>Select available provider} diff --git a/webapp/packages/plugin-authentication/src/Dialog/AuthProviderForm/ConfigurationsList.tsx b/webapp/packages/plugin-authentication/src/Dialog/AuthProviderForm/ConfigurationsList.tsx new file mode 100644 index 0000000000..b3050a9ea5 --- /dev/null +++ b/webapp/packages/plugin-authentication/src/Dialog/AuthProviderForm/ConfigurationsList.tsx @@ -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 = 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))( + + {configurations.length >= 10 && ( + + )} + + {filteredConfigurations.get().map(configuration => { + const icon = configuration.iconURL || providerIcon; + const title = `${configuration.displayName}\n${configuration.description || ''}`; + return ( + + : undefined} + description={configuration.description} + > + {configuration.displayName} + + + ); + })} + + + ); +}); diff --git a/webapp/packages/plugin-authentication/src/locales/en.ts b/webapp/packages/plugin-authentication/src/locales/en.ts index 09990007da..635a391d4d 100644 --- a/webapp/packages/plugin-authentication/src/locales/en.ts +++ b/webapp/packages/plugin-authentication/src/locales/en.ts @@ -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'], diff --git a/webapp/packages/plugin-authentication/src/locales/ru.ts b/webapp/packages/plugin-authentication/src/locales/ru.ts index 69d2c06a25..31e8f23784 100644 --- a/webapp/packages/plugin-authentication/src/locales/ru.ts +++ b/webapp/packages/plugin-authentication/src/locales/ru.ts @@ -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', 'Не удалось загрузить доступные пользователю коннекшены'],