Skip to content

Commit 8cf2bbd

Browse files
authored
fix: Use user permissions for iframes instead of query parameters (#1400)
- Add UserUtils for fetching the User. - Add ServerConfigBootstrap and UserBootstrap to bootstrap those objects into context. - Remove query parameters for controlling user function in iframes; instead it is defined on the server. - Fixes #1337.
1 parent 405f42f commit 8cf2bbd

18 files changed

Lines changed: 319 additions & 78 deletions

File tree

packages/app-utils/src/components/AppBootstrap.test.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import AppBootstrap from './AppBootstrap';
1414
const API_URL = 'http://mockserver.net:8111';
1515
const PLUGINS_URL = 'http://mockserver.net:8111/plugins';
1616

17+
const mockGetServerConfigValues = jest.fn(() => Promise.resolve([]));
1718
const mockPluginsPromise = Promise.resolve([]);
1819
jest.mock('../plugins', () => ({
1920
...jest.requireActual('../plugins'),
@@ -77,6 +78,7 @@ it('should display an error if no login plugin matches the provided auth handler
7778
const mockLogin = jest.fn(() => Promise.resolve());
7879
const client = TestUtils.createMockProxy<CoreClient>({
7980
getAuthConfigValues: mockGetAuthConfigValues,
81+
getServerConfigValues: mockGetServerConfigValues,
8082
login: mockLogin,
8183
});
8284
renderComponent(client);
@@ -119,9 +121,10 @@ it('should log in automatically when the anonymous handler is supported', async
119121
);
120122
const mockConnection = TestUtils.createMockProxy<IdeConnection>({});
121123
const client = TestUtils.createMockProxy<CoreClient>({
124+
getAsIdeConnection: mockGetAsConnection,
122125
getAuthConfigValues: mockGetAuthConfigValues,
126+
getServerConfigValues: mockGetServerConfigValues,
123127
login: mockLogin,
124-
getAsIdeConnection: mockGetAsConnection,
125128
});
126129

127130
renderComponent(client);

packages/app-utils/src/components/AppBootstrap.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import AuthBootstrap from './AuthBootstrap';
1111
import ConnectionBootstrap from './ConnectionBootstrap';
1212
import { getConnectOptions } from '../utils';
1313
import FontsLoaded from './FontsLoaded';
14+
import UserBootstrap from './UserBootstrap';
15+
import ServerConfigBootstrap from './ServerConfigBootstrap';
1416

1517
export type AppBootstrapProps = {
1618
/** URL of the server. */
@@ -57,9 +59,13 @@ export function AppBootstrap({
5759
>
5860
<RefreshTokenBootstrap>
5961
<AuthBootstrap>
60-
<ConnectionBootstrap>
61-
<FontsLoaded>{children}</FontsLoaded>
62-
</ConnectionBootstrap>
62+
<ServerConfigBootstrap>
63+
<UserBootstrap>
64+
<ConnectionBootstrap>
65+
<FontsLoaded>{children}</FontsLoaded>
66+
</ConnectionBootstrap>
67+
</UserBootstrap>
68+
</ServerConfigBootstrap>
6369
</AuthBootstrap>
6470
</RefreshTokenBootstrap>
6571
</ClientBootstrap>
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import React, { createContext, useEffect, useState } from 'react';
2+
import { LoadingOverlay } from '@deephaven/components';
3+
import { useClient } from '@deephaven/jsapi-bootstrap';
4+
import { getErrorMessage } from '@deephaven/utils';
5+
6+
export const ServerConfigContext = createContext<Map<string, string> | null>(
7+
null
8+
);
9+
10+
export type ServerConfigBootstrapProps = {
11+
/**
12+
* The children to render after server config is loaded.
13+
*/
14+
children: React.ReactNode;
15+
};
16+
17+
/**
18+
* ServerConfigBootstrap component. Handles loading the server config.
19+
*/
20+
export function ServerConfigBootstrap({
21+
children,
22+
}: ServerConfigBootstrapProps) {
23+
const client = useClient();
24+
const [serverConfig, setServerConfig] = useState<Map<string, string>>();
25+
const [error, setError] = useState<unknown>();
26+
27+
useEffect(
28+
function initServerConfigValues() {
29+
let isCanceled = false;
30+
async function loadServerConfigValues() {
31+
try {
32+
const newServerConfigValues = await client.getServerConfigValues();
33+
if (!isCanceled) {
34+
setServerConfig(new Map(newServerConfigValues));
35+
}
36+
} catch (e) {
37+
if (!isCanceled) {
38+
setError(e);
39+
}
40+
}
41+
}
42+
loadServerConfigValues();
43+
return () => {
44+
isCanceled = true;
45+
};
46+
},
47+
[client]
48+
);
49+
50+
const isLoading = serverConfig == null;
51+
52+
if (isLoading || error != null) {
53+
return (
54+
<LoadingOverlay
55+
isLoading={isLoading && error == null}
56+
errorMessage={getErrorMessage(error)}
57+
/>
58+
);
59+
}
60+
61+
return (
62+
<ServerConfigContext.Provider value={serverConfig}>
63+
{children}
64+
</ServerConfigContext.Provider>
65+
);
66+
}
67+
68+
export default ServerConfigBootstrap;
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import React, { useContext } from 'react';
2+
import {
3+
UserContext,
4+
UserOverrideContext,
5+
UserPermissionsOverrideContext,
6+
getUserFromConfig,
7+
} from '@deephaven/auth-plugins';
8+
import useServerConfig from './useServerConfig';
9+
10+
export type UserBootstrapProps = {
11+
/** The children to render */
12+
children: React.ReactNode;
13+
};
14+
15+
/**
16+
* UserBootstrap component. Derives the UserContext from the ServerConfigContext, UserOverrideContext, and UserPermissionsOverrideContext.
17+
*/
18+
export function UserBootstrap({ children }: UserBootstrapProps) {
19+
const serverConfig = useServerConfig();
20+
const overrides = useContext(UserOverrideContext);
21+
const permissionsOverrides = useContext(UserPermissionsOverrideContext);
22+
const user = getUserFromConfig(serverConfig, overrides, permissionsOverrides);
23+
return <UserContext.Provider value={user}>{children}</UserContext.Provider>;
24+
}
25+
26+
export default UserBootstrap;

packages/app-utils/src/components/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,5 @@ export * from './FontsLoaded';
66
export * from './PluginsBootstrap';
77
export * from './usePlugins';
88
export * from './useConnection';
9+
export * from './useServerConfig';
10+
export * from './useUser';
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { useContextOrThrow } from '@deephaven/react-hooks';
2+
import { ServerConfigContext } from './ServerConfigBootstrap';
3+
4+
export function useServerConfig() {
5+
return useContextOrThrow(
6+
ServerConfigContext,
7+
'No server config available in useServerConfig. Was code wrapped in ServerConfigBootstrap or ServerConfigContext.Provider?'
8+
);
9+
}
10+
11+
export default useServerConfig;
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { useContextOrThrow } from '@deephaven/react-hooks';
2+
import { UserContext } from '@deephaven/auth-plugins';
3+
4+
export function useUser() {
5+
return useContextOrThrow(
6+
UserContext,
7+
'No user available in useUser. Was code wrapped in UserBootstrap or UserContext.Provider?'
8+
);
9+
}
10+
11+
export default useUser;

packages/auth-plugins/src/UserContexts.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,5 @@ export const UserOverrideContext = createContext<UserOverride>({});
1010
export const UserPermissionsOverrideContext = createContext<UserPermissionsOverride>(
1111
{}
1212
);
13+
14+
export const UserContext = createContext<User | null>(null);
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { getAppInitValue, getUserFromConfig } from './UserUtils';
2+
3+
it('returns the value for the AppInit key', () => {
4+
const serverConfig = new Map<string, string>();
5+
serverConfig.set('internal.webClient.appInit.name', 'test');
6+
serverConfig.set('internal.webClient.appInit.foo', 'bar');
7+
serverConfig.set('name', 'not-test');
8+
expect(getAppInitValue(serverConfig, 'name')).toEqual('test');
9+
expect(getAppInitValue(serverConfig, 'foo')).toEqual('bar');
10+
expect(getAppInitValue(serverConfig, 'food')).toEqual(undefined);
11+
expect(
12+
getAppInitValue(serverConfig, 'internal.webClient.appInit.name')
13+
).toEqual(undefined);
14+
expect(getAppInitValue(serverConfig, '')).toEqual(undefined);
15+
});
16+
17+
describe('getUser', () => {
18+
it('returns the default user and permissions', () => {
19+
const serverConfig = new Map<string, string>();
20+
expect(getUserFromConfig(serverConfig)).toEqual({
21+
name: '',
22+
operateAs: '',
23+
groups: [],
24+
permissions: {
25+
canUsePanels: true,
26+
canCopy: true,
27+
canDownloadCsv: true,
28+
canLogout: true,
29+
},
30+
});
31+
});
32+
33+
it('returns the values from the config correctly', () => {
34+
const serverConfig = new Map<string, string>();
35+
serverConfig.set('internal.webClient.appInit.name', 'test');
36+
serverConfig.set('internal.webClient.appInit.operateAs', 'test-operator');
37+
serverConfig.set('internal.webClient.appInit.groups', 'group1,group2');
38+
serverConfig.set('internal.webClient.appInit.canUsePanels', 'false');
39+
serverConfig.set('internal.webClient.appInit.canCopy', 'false');
40+
serverConfig.set('internal.webClient.appInit.canDownloadCsv', 'false');
41+
serverConfig.set('internal.webClient.appInit.canLogout', 'false');
42+
expect(getUserFromConfig(serverConfig)).toEqual({
43+
name: 'test',
44+
operateAs: 'test-operator',
45+
groups: ['group1', 'group2'],
46+
permissions: {
47+
canUsePanels: false,
48+
canCopy: false,
49+
canDownloadCsv: false,
50+
canLogout: false,
51+
},
52+
});
53+
});
54+
55+
it('overrides the default values correctly', () => {
56+
const serverConfig = new Map<string, string>();
57+
serverConfig.set('internal.webClient.appInit.name', 'test');
58+
serverConfig.set('internal.webClient.appInit.operateAs', 'test-operator');
59+
serverConfig.set('internal.webClient.appInit.groups', 'group1,group2');
60+
serverConfig.set('internal.webClient.appInit.canUsePanels', 'false');
61+
serverConfig.set('internal.webClient.appInit.canCopy', 'false');
62+
serverConfig.set('internal.webClient.appInit.canDownloadCsv', 'false');
63+
serverConfig.set('internal.webClient.appInit.canLogout', 'false');
64+
expect(
65+
getUserFromConfig(
66+
serverConfig,
67+
{
68+
name: 'test2',
69+
operateAs: 'test2-operator',
70+
groups: ['group3', 'group4'],
71+
},
72+
{
73+
canUsePanels: true,
74+
canCopy: true,
75+
canDownloadCsv: true,
76+
canLogout: true,
77+
}
78+
)
79+
).toEqual({
80+
name: 'test2',
81+
operateAs: 'test2-operator',
82+
groups: ['group3', 'group4'],
83+
permissions: {
84+
canUsePanels: true,
85+
canCopy: true,
86+
canDownloadCsv: true,
87+
canLogout: true,
88+
},
89+
});
90+
});
91+
});
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { User, UserPermissions } from '@deephaven/redux';
2+
import Log from '@deephaven/log';
3+
4+
const log = Log.module('UserUtils');
5+
6+
/**
7+
* Retrieve a value from the AppInit config
8+
* @param serverConfig Server config map
9+
* @param key The AppInit key to retrieve
10+
* @returns The value for the AppInit key
11+
*/
12+
export function getAppInitValue(
13+
serverConfig: Map<string, string>,
14+
key: string
15+
): string | undefined {
16+
return serverConfig.get(`internal.webClient.appInit.${key}`);
17+
}
18+
19+
/**
20+
* Retrieve a user object provided the server config and overrides
21+
* @param serverConfig Server config map
22+
* @param overrides Override values for the user
23+
* @param permissionsOverrides Override specific permissions for the user
24+
* @returns The user object
25+
*/
26+
export function getUserFromConfig(
27+
serverConfig: Map<string, string>,
28+
overrides: Partial<Omit<User, 'permissions'>> = {},
29+
permissionsOverrides: Partial<UserPermissions> = {}
30+
): User {
31+
function getValue(key: string): string | undefined {
32+
return getAppInitValue(serverConfig, key);
33+
}
34+
function getBooleanValue(key: string, defaultValue: boolean): boolean {
35+
const value = getValue(key);
36+
if (value === 'true') {
37+
return true;
38+
}
39+
if (value === 'false') {
40+
return false;
41+
}
42+
if (value !== undefined) {
43+
log.warn(`Unexpected value for ${key}: ${value}`);
44+
}
45+
return defaultValue;
46+
}
47+
const name = getValue('name') ?? '';
48+
const operateAs = getValue('operateAs') ?? name;
49+
const groups = getValue('groups')?.split(',') ?? [];
50+
const canCopy = getBooleanValue('canCopy', true);
51+
const canDownloadCsv = getBooleanValue('canDownloadCsv', true);
52+
const canUsePanels = getBooleanValue('canUsePanels', true);
53+
const canLogout = getBooleanValue('canLogout', true);
54+
55+
return {
56+
name,
57+
operateAs,
58+
groups,
59+
...overrides,
60+
permissions: {
61+
canUsePanels,
62+
canCopy,
63+
canDownloadCsv,
64+
canLogout,
65+
...permissionsOverrides,
66+
},
67+
};
68+
}
69+
70+
export default { getAppInitValue, getUser: getUserFromConfig };

0 commit comments

Comments
 (0)