Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
24 changes: 19 additions & 5 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 {
useAccessTotallyRestricted,
useCapabilitiesLoaded,
useCapabilitiesQuery,
} 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,6 +228,11 @@ interface ContentWrapperProps {

function ContentWrapper(props: ContentWrapperProps) {
const {singleClusterMode, isAuthenticated} = props;
const isForbidded = useAccessTotallyRestricted();

if (isForbidded) {
return <AccessDenied />;
}

return (
<Switch>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {useHistory} from 'react-router-dom';
import routes, {createHref} from '../../../routes';
import {authenticationApi} from '../../../store/reducers/authentication/authentication';
import {cn} from '../../../utils/cn';
import {useDatabaseFromQuery} from '../../../utils/hooks/useDatabaseFromQuery';
import i18n from '../i18n';

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

export function YdbInternalUser({login}: {login?: string}) {
const [logout] = authenticationApi.useLogoutMutation();
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,
}),
);
};

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 useAccessTotallyRestricted = () => {
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