Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add database to authentication process #1976

Merged
merged 3 commits into from
Feb 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 28 additions & 9 deletions src/containers/App/Content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {connect} from 'react-redux';
import type {RedirectProps} from 'react-router-dom';
import {Redirect, Route, Switch} from 'react-router-dom';

import {AccessDenied} from '../../components/Errors/403';
import {PageError} from '../../components/Errors/PageError/PageError';
import {LoaderWrapper} from '../../components/LoaderWrapper/LoaderWrapper';
import {useSlots} from '../../components/slots';
Expand All @@ -12,7 +13,11 @@ import type {SlotComponent} from '../../components/slots/types';
import routes from '../../routes';
import type {RootState} from '../../store';
import {authenticationApi} from '../../store/reducers/authentication/authentication';
import {useCapabilitiesLoaded, useCapabilitiesQuery} from '../../store/reducers/capabilities/hooks';
import {
useCapabilitiesLoaded,
useCapabilitiesQuery,
useClusterWithoutAuthInUI,
} from '../../store/reducers/capabilities/hooks';
import {nodesListApi} from '../../store/reducers/nodesList';
import {cn} from '../../utils/cn';
import {useDatabaseFromQuery} from '../../utils/hooks/useDatabaseFromQuery';
Expand Down Expand Up @@ -177,10 +182,14 @@ export function Content(props: ContentProps) {

function DataWrapper({children}: {children: React.ReactNode}) {
return (
<GetUser>
<GetNodesList />
<GetCapabilities>{children}</GetCapabilities>
</GetUser>
// capabilities is going to work without authentication, but not all running systems are supporting this yet
<GetCapabilities>
<GetUser>
<GetNodesList />
{/* this GetCapabilities will be removed */}
<GetCapabilities>{children}</GetCapabilities>
</GetUser>
</GetCapabilities>
);
}

Expand Down Expand Up @@ -219,15 +228,25 @@ interface ContentWrapperProps {

function ContentWrapper(props: ContentWrapperProps) {
const {singleClusterMode, isAuthenticated} = props;
const authUnavailable = useClusterWithoutAuthInUI();

const renderNotAuthenticated = () => {
if (authUnavailable) {
return <AccessDenied />;
}
return <Authentication />;
};

return (
<Switch>
<Route path={routes.auth}>
<Authentication closable />
</Route>
{!authUnavailable && (
<Route path={routes.auth}>
<Authentication closable />
</Route>
)}
<Route>
<div className={b({embedded: singleClusterMode})}>
{isAuthenticated ? props.children : <Authentication />}
{isAuthenticated ? props.children : renderNotAuthenticated()}
</div>
</Route>
</Switch>
Expand Down
28 changes: 20 additions & 8 deletions src/containers/AsideNavigation/YdbInternalUser/YdbInternalUser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import {useHistory} from 'react-router-dom';

import routes, {createHref} from '../../../routes';
import {authenticationApi} from '../../../store/reducers/authentication/authentication';
import {useClusterWithoutAuthInUI} from '../../../store/reducers/capabilities/hooks';
import {cn} from '../../../utils/cn';
import {useDatabaseFromQuery} from '../../../utils/hooks/useDatabaseFromQuery';
import i18n from '../i18n';

import './YdbInternalUser.scss';
Expand All @@ -13,18 +15,34 @@ const b = cn('kv-ydb-internal-user');

export function YdbInternalUser({login}: {login?: string}) {
const [logout] = authenticationApi.useLogoutMutation();
const authUnavailable = useClusterWithoutAuthInUI();
const database = useDatabaseFromQuery();

const history = useHistory();
const handleLoginClick = () => {
history.push(
createHref(routes.auth, undefined, {returnUrl: encodeURIComponent(location.href)}),
createHref(routes.auth, undefined, {
returnUrl: encodeURIComponent(location.href),
database,
}),
);
};

const handleLogout = () => {
logout(undefined);
};

const renderLoginButton = () => {
if (authUnavailable) {
return null;
}
return (
<Button view="flat-secondary" title={i18n('account.login')} onClick={handleLoginClick}>
<Icon data={ArrowRightToSquare} />
</Button>
);
};

return (
<div className={b()}>
<div className={b('user-info-wrapper')}>
Expand All @@ -36,13 +54,7 @@ export function YdbInternalUser({login}: {login?: string}) {
<Icon data={ArrowRightFromSquare} />
</Button>
) : (
<Button
view="flat-secondary"
title={i18n('account.login')}
onClick={handleLoginClick}
>
<Icon data={ArrowRightToSquare} />
</Button>
renderLoginButton()
)}
</div>
);
Expand Down
30 changes: 27 additions & 3 deletions src/containers/Authentication/Authentication.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ import {useHistory, useLocation} from 'react-router-dom';

import {parseQuery} from '../../routes';
import {authenticationApi} from '../../store/reducers/authentication/authentication';
import {useLoginWithDatabase} from '../../store/reducers/capabilities/hooks';
import {cn} from '../../utils/cn';

import {isPasswordError, isUserError} from './utils';
import {isDatabaseError, isPasswordError, isUserError} from './utils';

import ydbLogoIcon from '../../assets/icons/ydb.svg';

Expand All @@ -24,28 +25,36 @@ function Authentication({closable = false}: AuthenticationProps) {
const history = useHistory();
const location = useLocation();

const needDatabase = useLoginWithDatabase();

const [authenticate, {isLoading}] = authenticationApi.useAuthenticateMutation(undefined);

const {returnUrl} = parseQuery(location);
const {returnUrl, database: databaseFromQuery} = parseQuery(location);

const [login, setLogin] = React.useState('');
const [database, setDatabase] = React.useState(databaseFromQuery?.toString() ?? '');
const [password, setPass] = React.useState('');
const [loginError, setLoginError] = React.useState('');
const [passwordError, setPasswordError] = React.useState('');
const [databaseError, setDatabaseError] = React.useState('');
const [showPassword, setShowPassword] = React.useState(false);

const onLoginUpdate = (value: string) => {
setLogin(value);
setLoginError('');
};
const onDatabaseUpdate = (value: string) => {
setDatabase(value);
setDatabaseError('');
};

const onPassUpdate = (value: string) => {
setPass(value);
setPasswordError('');
};

const onLoginClick = () => {
authenticate({user: login, password})
authenticate({user: login, password, database})
.unwrap()
.then(() => {
if (returnUrl) {
Expand All @@ -66,6 +75,9 @@ function Authentication({closable = false}: AuthenticationProps) {
if (isPasswordError(error)) {
setPasswordError(error.data.error);
}
if (isDatabaseError(error)) {
setDatabaseError(error.data.error);
}
});
};

Expand Down Expand Up @@ -125,6 +137,18 @@ function Authentication({closable = false}: AuthenticationProps) {
<Icon data={showPassword ? EyeSlash : Eye} size={16} />
</Button>
</div>
{needDatabase && (
<div className={b('field-wrapper')}>
<TextInput
value={database}
onUpdate={onDatabaseUpdate}
placeholder={'Database'}
error={databaseError}
onKeyDown={onEnterClick}
size="l"
/>
</div>
)}
<Button
view="action"
onClick={onLoginClick}
Expand Down
3 changes: 3 additions & 0 deletions src/containers/Authentication/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,6 @@ export function isUserError(error: unknown): error is AuthError {
export function isPasswordError(error: unknown): error is AuthError {
return isAuthError(error) && error.data.error.includes('password');
}
export function isDatabaseError(error: unknown): error is AuthError {
return isAuthError(error) && error.data.error.includes('database');
}
2 changes: 1 addition & 1 deletion src/services/api/auth.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {BaseYdbAPI} from './base';

export class AuthAPI extends BaseYdbAPI {
authenticate(params: {user: string; password: string}) {
authenticate(params: {user: string; password: string; database?: string}) {
return this.post(this.getPath('/login'), params, {});
}

Expand Down
5 changes: 4 additions & 1 deletion src/store/reducers/authentication/authentication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,10 @@ export const authenticationApi = api.injectEndpoints({
providesTags: ['UserData'],
}),
authenticate: build.mutation({
queryFn: async (params: {user: string; password: string}, {dispatch}) => {
queryFn: async (
params: {user: string; password: string; database?: string},
{dispatch},
) => {
try {
const data = await window.api.auth.authenticate(params);
dispatch(setIsAuthenticated(true));
Expand Down
16 changes: 12 additions & 4 deletions src/store/reducers/capabilities/capabilities.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {createSelector} from '@reduxjs/toolkit';

import type {Capability} from '../../../types/api/capabilities';
import type {Capability, SecuritySetting} from '../../../types/api/capabilities';
import type {AppDispatch, RootState} from '../../defaultStore';

import {api} from './../api';
Expand Down Expand Up @@ -38,8 +38,16 @@ export const selectCapabilityVersion = createSelector(
(state: RootState) => state,
(_state: RootState, capability: Capability) => capability,
(_state: RootState, _capability: Capability, database?: string) => database,
(state, capability, database) =>
selectDatabaseCapabilities(state, database).data?.Capabilities?.[capability],
(state, capability, database) => {
return selectDatabaseCapabilities(state, database).data?.Capabilities?.[capability];
},
);
export const selectSecuritySetting = createSelector(
(state: RootState) => state,
(_state: RootState, setting: SecuritySetting) => setting,
(_state: RootState, _setting: SecuritySetting, database?: string) => database,
(state, setting, database) =>
selectDatabaseCapabilities(state, database).data?.Settings?.Security?.[setting],
);

export async function queryCapability(
Expand All @@ -50,5 +58,5 @@ export async function queryCapability(
const thunk = capabilitiesApi.util.getRunningQueryThunk('getClusterCapabilities', {database});
await dispatch(thunk);

return selectCapabilityVersion(getState(), capability) || 0;
return selectCapabilityVersion(getState(), capability, database) || 0;
}
23 changes: 21 additions & 2 deletions src/store/reducers/capabilities/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import type {Capability} from '../../../types/api/capabilities';
import type {Capability, SecuritySetting} from '../../../types/api/capabilities';
import {useTypedSelector} from '../../../utils/hooks';
import {useDatabaseFromQuery} from '../../../utils/hooks/useDatabaseFromQuery';

import {capabilitiesApi, selectCapabilityVersion, selectDatabaseCapabilities} from './capabilities';
import {
capabilitiesApi,
selectCapabilityVersion,
selectDatabaseCapabilities,
selectSecuritySetting,
} from './capabilities';

export function useCapabilitiesQuery() {
const database = useDatabaseFromQuery();
Expand Down Expand Up @@ -68,3 +73,17 @@ export const useClusterDashboardAvailable = () => {
export const useStreamingAvailable = () => {
return useGetFeatureVersion('/viewer/query') >= 7;
};

const useGetSecuritySetting = (feature: SecuritySetting) => {
const database = useDatabaseFromQuery();

return useTypedSelector((state) => selectSecuritySetting(state, feature, database));
};

export const useClusterWithoutAuthInUI = () => {
return useGetSecuritySetting('UseLoginProvider') === false;
};

export const useLoginWithDatabase = () => {
return useGetSecuritySetting('DomainLoginOnly') === false;
};
5 changes: 5 additions & 0 deletions src/types/api/capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
*/
export interface CapabilitiesResponse {
Capabilities: Record<Partial<Capability>, number>;
Settings?: {
Security?: Record<Partial<SecuritySetting>, boolean>;
};
}

// Add feature name before using it
Expand All @@ -14,3 +17,5 @@ export type Capability =
| '/viewer/feature_flags'
| '/viewer/cluster'
| '/viewer/nodes';

export type SecuritySetting = 'UseLoginProvider' | 'DomainLoginOnly';
Loading