Skip to content

Commit

Permalink
Add multi datasource support for internal users tab
Browse files Browse the repository at this point in the history
Signed-off-by: Derek Ho <[email protected]>
  • Loading branch information
derek-ho committed Mar 25, 2024
1 parent eae331a commit 39b12b1
Show file tree
Hide file tree
Showing 22 changed files with 244 additions and 81 deletions.
12 changes: 8 additions & 4 deletions public/apps/account/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export async function fetchAccountInfoSafe(http: HttpStart): Promise<AccountInfo
}

export async function logout(http: HttpStart, logoutUrl?: string): Promise<void> {
await httpPost(http, API_AUTH_LOGOUT);
await httpPost({ http, url: API_AUTH_LOGOUT });

Check warning on line 37 in public/apps/account/utils.tsx

View check run for this annotation

Codecov / codecov/patch

public/apps/account/utils.tsx#L37

Added line #L37 was not covered by tests
setShouldShowTenantPopup(null);
// Clear everything in the sessionStorage since they can contain sensitive information
sessionStorage.clear();
Expand Down Expand Up @@ -70,8 +70,12 @@ export async function updateNewPassword(
newPassword: string,
currentPassword: string
): Promise<void> {
await httpPost(http, API_ENDPOINT_ACCOUNT_INFO, {
password: newPassword,
current_password: currentPassword,
await httpPost({

Check warning on line 73 in public/apps/account/utils.tsx

View check run for this annotation

Codecov / codecov/patch

public/apps/account/utils.tsx#L73

Added line #L73 was not covered by tests
http,
url: API_ENDPOINT_ACCOUNT_INFO,
body: {
password: newPassword,
current_password: currentPassword,
},
});
}
8 changes: 5 additions & 3 deletions public/apps/configuration/panels/auth-view/auth-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { getSecurityConfig } from '../../utils/auth-view-utils';
import { InstructionView } from './instruction-view';
import { DataSourceContext } from '../../app-router';
import { SecurityPluginTopNavMenu } from '../../top-nav-menu';
import { createDataSourceQuery } from '../../../../utils/datasource-utils';

export function AuthView(props: AppDependencies) {
const [authentication, setAuthentication] = React.useState([]);
Expand All @@ -36,9 +37,10 @@ export function AuthView(props: AppDependencies) {
const fetchData = async () => {
try {
setLoading(true);
const config = await getSecurityConfig(props.coreStart.http, {
dataSourceId: dataSource.id,
});
const config = await getSecurityConfig(
props.coreStart.http,
createDataSourceQuery(dataSource.id)
);

setAuthentication(config.authc);
setAuthorization(config.authz);
Expand Down
12 changes: 2 additions & 10 deletions public/apps/configuration/panels/get-started.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { httpDelete } from '../utils/request-utils';
import { createSuccessToast, createUnknownErrorToast, useToastState } from '../utils/toast-utils';
import { SecurityPluginTopNavMenu } from '../top-nav-menu';
import { DataSourceContext } from '../app-router';
import { getClusterInfoIfEnabled, createDataSourceQuery } from '../../../utils/datasource-utils';

const addBackendStep = {
title: 'Add backends',
Expand Down Expand Up @@ -161,13 +162,6 @@ const setOfSteps = [
},
];

export function getClusterInfoIfEnabled(dataSourceEnabled: boolean, cluster: DataSourceOption) {
if (dataSourceEnabled) {
return `for ${cluster.label || 'Local cluster'}`;
}
return '';
}

export function GetStarted(props: AppDependencies) {
const dataSourceEnabled = !!props.depsStart.dataSource?.dataSourceEnabled;
const { dataSource, setDataSource } = useContext(DataSourceContext)!;
Expand Down Expand Up @@ -258,9 +252,7 @@ export function GetStarted(props: AppDependencies) {
await httpDelete({
http: props.coreStart.http,
url: API_ENDPOINT_CACHE,
query: {
dataSourceId: dataSource.id,
},
query: createDataSourceQuery(dataSource.id),
});
addToast(
createSuccessToast(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
EuiText,
EuiTitle,
} from '@elastic/eui';
import React, { useState } from 'react';
import React, { useContext, useState } from 'react';
import { BreadcrumbsPageDependencies } from '../../../types';
import { InternalUserUpdate } from '../../types';
import { ResourceType } from '../../../../../common';
Expand All @@ -47,6 +47,9 @@ import { NameRow } from '../../utils/name-row';
import { DocLinks } from '../../constants';
import { constructErrorMessageAndLog } from '../../../error-utils';
import { BackendRolePanel } from './backend-role-panel';
import { DataSourceContext } from '../../app-router';
import { SecurityPluginTopNavMenu } from '../../top-nav-menu';
import { createDataSourceQuery } from '../../../../utils/datasource-utils';

interface InternalUserEditDeps extends BreadcrumbsPageDependencies {
action: 'create' | 'edit' | 'duplicate';
Expand All @@ -72,13 +75,18 @@ export function InternalUserEdit(props: InternalUserEditDeps) {
const [toasts, addToast, removeToast] = useToastState();

const [isFormValid, setIsFormValid] = useState<boolean>(true);
const { dataSource, setDataSource } = useContext(DataSourceContext)!;

React.useEffect(() => {
const action = props.action;
if (action === 'edit' || action === 'duplicate') {
const fetchData = async () => {
try {
const user = await getUserDetail(props.coreStart.http, props.sourceUserName);
const user = await getUserDetail(
props.coreStart.http,
props.sourceUserName,
createDataSourceQuery(dataSource.id)
);
setAttributes(buildAttributeState(user.attributes));
setBackendRoles(user.backend_roles);
setUserName(generateResourceName(action, props.sourceUserName));
Expand All @@ -90,7 +98,7 @@ export function InternalUserEdit(props: InternalUserEditDeps) {

fetchData();
}
}, [addToast, props.action, props.coreStart.http, props.sourceUserName]);
}, [addToast, props.action, props.coreStart.http, props.sourceUserName, dataSource.id]);

const updateUserHandler = async () => {
try {
Expand All @@ -117,7 +125,12 @@ export function InternalUserEdit(props: InternalUserEditDeps) {
updateObject.password = password;
}

await updateUser(props.coreStart.http, userName, updateObject);
await updateUser(
props.coreStart.http,
userName,
updateObject,
createDataSourceQuery(dataSource.id)
);

setCrossPageToast(buildUrl(ResourceType.users), {
id: 'updateUserSucceeded',
Expand All @@ -135,6 +148,12 @@ export function InternalUserEdit(props: InternalUserEditDeps) {

return (
<>
<SecurityPluginTopNavMenu
{...props}
dataSourcePickerReadOnly={true}
setDataSource={setDataSource}
selectedDataSource={dataSource}
/>
{props.buildBreadcrumbs(TITLE_TEXT_DICT[props.action])}
<EuiSpacer />
<EuiPageHeader>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ jest.mock('../../../utils/toast-utils', () => ({
createUnknownErrorToast: jest.fn(),
useToastState: jest.fn().mockReturnValue([[], jest.fn(), jest.fn()]),
}));
jest.mock('react', () => ({
...jest.requireActual('react'),
useContext: jest.fn().mockReturnValue({ dataSource: { id: 'test' }, setDataSource: jest.fn() }), // Mock the useContext hook to return dummy datasource and setdatasource function
}));

describe('Internal user edit', () => {
const sampleUsername = 'user1';
Expand Down Expand Up @@ -79,7 +83,9 @@ describe('Internal user edit', () => {
/>
);

expect(getUserDetail).toBeCalledWith(mockCoreStart.http, sampleUsername);
expect(getUserDetail).toBeCalledWith(mockCoreStart.http, sampleUsername, {
dataSourceId: 'test',
});
});

it('should not submit if password is empty on creation', () => {
Expand Down
6 changes: 0 additions & 6 deletions public/apps/configuration/panels/test/get-started.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -175,11 +175,5 @@ describe('Get started (landing page)', () => {
await button.props().onClick(); // Simulate button click
expect(ToastUtils.createSuccessToast).toHaveBeenCalledTimes(1);
});

it('Tests the GetClusterDescription helper function', () => {
expect(getClusterInfoIfEnabled(false, { id: 'blah', label: 'blah' })).toBe('');
expect(getClusterInfoIfEnabled(true, { id: '', label: '' })).toBe('for Local cluster');
expect(getClusterInfoIfEnabled(true, { id: 'test', label: 'test' })).toBe('for test');
});
});
});
4 changes: 4 additions & 0 deletions public/apps/configuration/panels/test/user-list.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ jest.mock('../../utils/context-menu', () => ({
.fn()
.mockImplementation((buttonText, buttonProps, children) => [children, jest.fn()]),
}));
jest.mock('react', () => ({
...jest.requireActual('react'),
useContext: jest.fn().mockReturnValue({ dataSource: { id: 'test' }, setDataSource: jest.fn() }), // Mock the useContext hook to return dummy datasource and setdatasource function
}));

import { getAuthInfo } from '../../../../utils/auth-info-utils';
import { buildHashUrl } from '../../utils/url-builder';
Expand Down
26 changes: 22 additions & 4 deletions public/apps/configuration/panels/user-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import {
Query,
} from '@elastic/eui';
import { Dictionary, difference, isEmpty, map } from 'lodash';
import React, { useState } from 'react';
import React, { useContext, useState } from 'react';
import { getAuthInfo } from '../../../utils/auth-info-utils';
import { AppDependencies } from '../../types';
import { API_ENDPOINT_INTERNALUSERS, DocLinks } from '../constants';
Expand All @@ -48,6 +48,9 @@ import {
} from '../utils/internal-user-list-utils';
import { showTableStatusMessage } from '../utils/loading-spinner-utils';
import { buildHashUrl } from '../utils/url-builder';
import { DataSourceContext } from '../app-router';
import { SecurityPluginTopNavMenu } from '../top-nav-menu';
import { createDataSourceQuery } from '../../../utils/datasource-utils';

export function dictView(items: Dictionary<string>) {
if (isEmpty(items)) {
Expand Down Expand Up @@ -103,12 +106,17 @@ export function UserList(props: AppDependencies) {
const [currentUsername, setCurrentUsername] = useState('');
const [loading, setLoading] = useState(false);
const [query, setQuery] = useState<Query | null>(null);
const { dataSource, setDataSource } = useContext(DataSourceContext)!;

React.useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
const userDataPromise = getUserList(props.coreStart.http, ResourceType.users);
const userDataPromise = getUserList(
props.coreStart.http,
ResourceType.users,
createDataSourceQuery(dataSource.id)
);
setCurrentUsername((await getAuthInfo(props.coreStart.http)).user_name);
setUserData(await userDataPromise);
} catch (e) {
Expand All @@ -120,12 +128,16 @@ export function UserList(props: AppDependencies) {
};

fetchData();
}, [props.coreStart.http]);
}, [props.coreStart.http, dataSource.id]);

const handleDelete = async () => {
const usersToDelete: string[] = selection.map((r) => r.username);
try {
await requestDeleteUsers(props.coreStart.http, usersToDelete);
await requestDeleteUsers(
props.coreStart.http,
usersToDelete,
createDataSourceQuery(dataSource.id)
);
// Refresh from server (calling fetchData) does not work here, the server still return the users
// that had been just deleted, probably because ES takes some time to sync to all nodes.
// So here remove the selected users from local memory directly.
Expand Down Expand Up @@ -194,6 +206,12 @@ export function UserList(props: AppDependencies) {

return (
<>
<SecurityPluginTopNavMenu
{...props}
dataSourcePickerReadOnly={false}
setDataSource={setDataSource}
selectedDataSource={dataSource}
/>
<EuiPageHeader>
<EuiTitle size="l">
<h1>Internal users</h1>
Expand Down
2 changes: 2 additions & 0 deletions public/apps/configuration/top-nav-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export const SecurityPluginTopNavMenu = (props: TopNavMenuProps) => {
dataSourceManagement,
setDataSource,
selectedDataSource,
dataSourcePickerReadOnly,
} = props;
const { setHeaderActionMenu } = params;
const DataSourceMenu = dataSourceManagement?.ui.DataSourceMenu;
Expand All @@ -45,6 +46,7 @@ export const SecurityPluginTopNavMenu = (props: TopNavMenuProps) => {
setMenuMountPoint={setHeaderActionMenu}
notifications={coreStart.notifications}
dataSourceCallBackFunc={setDataSource}
disableDataSourceSelectable={dataSourcePickerReadOnly}
// Single select for now
selectedOption={[selectedDataSource]}
hideLocalCluster={false}
Expand Down
6 changes: 5 additions & 1 deletion public/apps/configuration/utils/action-groups-utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,11 @@ export async function updateActionGroup(
groupName: string,
updateObject: ActionGroupUpdate
): Promise<ActionGroupUpdate> {
return await httpPost(http, getResourceUrl(API_ENDPOINT_ACTIONGROUPS, groupName), updateObject);
return await httpPost({

Check warning on line 96 in public/apps/configuration/utils/action-groups-utils.tsx

View check run for this annotation

Codecov / codecov/patch

public/apps/configuration/utils/action-groups-utils.tsx#L96

Added line #L96 was not covered by tests
http,
url: getResourceUrl(API_ENDPOINT_ACTIONGROUPS, groupName),
body: updateObject,
});
}

export async function requestDeleteActionGroups(http: HttpStart, groups: string[]) {
Expand Down
2 changes: 1 addition & 1 deletion public/apps/configuration/utils/audit-logging-utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { API_ENDPOINT_AUDITLOGGING, API_ENDPOINT_AUDITLOGGING_UPDATE } from '../
import { httpGet, httpPost } from './request-utils';

export async function updateAuditLogging(http: HttpStart, updateObject: AuditLoggingSettings) {
return await httpPost(http, API_ENDPOINT_AUDITLOGGING_UPDATE, updateObject);
return await httpPost({ http, url: API_ENDPOINT_AUDITLOGGING_UPDATE, body: updateObject });

Check warning on line 22 in public/apps/configuration/utils/audit-logging-utils.tsx

View check run for this annotation

Codecov / codecov/patch

public/apps/configuration/utils/audit-logging-utils.tsx#L22

Added line #L22 was not covered by tests
}

export async function getAuditLogging(http: HttpStart): Promise<AuditLoggingSettings> {
Expand Down
19 changes: 15 additions & 4 deletions public/apps/configuration/utils/internal-user-detail-utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,34 @@
* permissions and limitations under the License.
*/

import { HttpStart } from 'opensearch-dashboards/public';
import { HttpFetchQuery, HttpStart } from 'opensearch-dashboards/public';
import { API_ENDPOINT_INTERNALUSERS } from '../constants';
import { InternalUser, InternalUserUpdate } from '../types';
import { httpGet, httpPost } from './request-utils';
import { getResourceUrl } from './resource-utils';

export async function getUserDetail(http: HttpStart, username: string): Promise<InternalUser> {
export async function getUserDetail(

Check warning on line 22 in public/apps/configuration/utils/internal-user-detail-utils.tsx

View check run for this annotation

Codecov / codecov/patch

public/apps/configuration/utils/internal-user-detail-utils.tsx#L22

Added line #L22 was not covered by tests
http: HttpStart,
username: string,
query: HttpFetchQuery
): Promise<InternalUser> {
return await httpGet<InternalUser>({
http,
url: getResourceUrl(API_ENDPOINT_INTERNALUSERS, username),
query,
});
}

export async function updateUser(
http: HttpStart,
username: string,
updateObject: InternalUserUpdate
updateObject: InternalUserUpdate,
query: HttpFetchQuery
): Promise<InternalUser> {
return await httpPost(http, getResourceUrl(API_ENDPOINT_INTERNALUSERS, username), updateObject);
return await httpPost({

Check warning on line 40 in public/apps/configuration/utils/internal-user-detail-utils.tsx

View check run for this annotation

Codecov / codecov/patch

public/apps/configuration/utils/internal-user-detail-utils.tsx#L40

Added line #L40 was not covered by tests
http,
url: getResourceUrl(API_ENDPOINT_INTERNALUSERS, username),
body: updateObject,
query,
});
}
16 changes: 9 additions & 7 deletions public/apps/configuration/utils/internal-user-list-utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
*/

import { map } from 'lodash';
import { HttpStart } from '../../../../../../src/core/public';
import { HttpFetchQuery, HttpStart } from '../../../../../../src/core/public';
import {
API_ENDPOINT_INTERNALACCOUNTS,
API_ENDPOINT_INTERNALUSERS,
Expand All @@ -37,29 +37,31 @@ export function transformUserData(rawData: DataObject<InternalUser>): InternalUs
}));
}

export async function requestDeleteUsers(http: HttpStart, users: string[]) {
export async function requestDeleteUsers(http: HttpStart, users: string[], query: HttpFetchQuery) {

Check warning on line 40 in public/apps/configuration/utils/internal-user-list-utils.tsx

View check run for this annotation

Codecov / codecov/patch

public/apps/configuration/utils/internal-user-list-utils.tsx#L40

Added line #L40 was not covered by tests
for (const user of users) {
await httpDelete({ http, url: getResourceUrl(API_ENDPOINT_INTERNALUSERS, user) });
await httpDelete({ http, url: getResourceUrl(API_ENDPOINT_INTERNALUSERS, user), query });

Check warning on line 42 in public/apps/configuration/utils/internal-user-list-utils.tsx

View check run for this annotation

Codecov / codecov/patch

public/apps/configuration/utils/internal-user-list-utils.tsx#L42

Added line #L42 was not covered by tests
}
}

async function getUserListRaw(
http: HttpStart,
userType: string
userType: string,
query?: HttpFetchQuery
): Promise<ObjectsMessage<InternalUser>> {
let ENDPOINT = API_ENDPOINT_INTERNALACCOUNTS;
if (userType === ResourceType.serviceAccounts) {
ENDPOINT = API_ENDPOINT_SERVICEACCOUNTS;
}

return await httpGet<ObjectsMessage<InternalUser>>({ http, url: ENDPOINT });
return await httpGet<ObjectsMessage<InternalUser>>({ http, url: ENDPOINT, query });

Check warning on line 56 in public/apps/configuration/utils/internal-user-list-utils.tsx

View check run for this annotation

Codecov / codecov/patch

public/apps/configuration/utils/internal-user-list-utils.tsx#L56

Added line #L56 was not covered by tests
}

export async function getUserList(
http: HttpStart,
userType: string
userType: string,
query?: HttpFetchQuery
): Promise<InternalUsersListing[]> {
const rawData = await getUserListRaw(http, userType);
const rawData = await getUserListRaw(http, userType, query);

Check warning on line 64 in public/apps/configuration/utils/internal-user-list-utils.tsx

View check run for this annotation

Codecov / codecov/patch

public/apps/configuration/utils/internal-user-list-utils.tsx#L64

Added line #L64 was not covered by tests
return transformUserData(rawData.data);
}

Expand Down
Loading

0 comments on commit 39b12b1

Please sign in to comment.