Skip to content

Commit

Permalink
fix: Use user permissions for iframes instead of query parameters (#1400
Browse files Browse the repository at this point in the history
)

- 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.
  • Loading branch information
mofojed authored Jul 7, 2023
1 parent 405f42f commit 8cf2bbd
Show file tree
Hide file tree
Showing 18 changed files with 319 additions and 78 deletions.
5 changes: 4 additions & 1 deletion packages/app-utils/src/components/AppBootstrap.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import AppBootstrap from './AppBootstrap';
const API_URL = 'http://mockserver.net:8111';
const PLUGINS_URL = 'http://mockserver.net:8111/plugins';

const mockGetServerConfigValues = jest.fn(() => Promise.resolve([]));
const mockPluginsPromise = Promise.resolve([]);
jest.mock('../plugins', () => ({
...jest.requireActual('../plugins'),
Expand Down Expand Up @@ -77,6 +78,7 @@ it('should display an error if no login plugin matches the provided auth handler
const mockLogin = jest.fn(() => Promise.resolve());
const client = TestUtils.createMockProxy<CoreClient>({
getAuthConfigValues: mockGetAuthConfigValues,
getServerConfigValues: mockGetServerConfigValues,
login: mockLogin,
});
renderComponent(client);
Expand Down Expand Up @@ -119,9 +121,10 @@ it('should log in automatically when the anonymous handler is supported', async
);
const mockConnection = TestUtils.createMockProxy<IdeConnection>({});
const client = TestUtils.createMockProxy<CoreClient>({
getAsIdeConnection: mockGetAsConnection,
getAuthConfigValues: mockGetAuthConfigValues,
getServerConfigValues: mockGetServerConfigValues,
login: mockLogin,
getAsIdeConnection: mockGetAsConnection,
});

renderComponent(client);
Expand Down
12 changes: 9 additions & 3 deletions packages/app-utils/src/components/AppBootstrap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import AuthBootstrap from './AuthBootstrap';
import ConnectionBootstrap from './ConnectionBootstrap';
import { getConnectOptions } from '../utils';
import FontsLoaded from './FontsLoaded';
import UserBootstrap from './UserBootstrap';
import ServerConfigBootstrap from './ServerConfigBootstrap';

export type AppBootstrapProps = {
/** URL of the server. */
Expand Down Expand Up @@ -57,9 +59,13 @@ export function AppBootstrap({
>
<RefreshTokenBootstrap>
<AuthBootstrap>
<ConnectionBootstrap>
<FontsLoaded>{children}</FontsLoaded>
</ConnectionBootstrap>
<ServerConfigBootstrap>
<UserBootstrap>
<ConnectionBootstrap>
<FontsLoaded>{children}</FontsLoaded>
</ConnectionBootstrap>
</UserBootstrap>
</ServerConfigBootstrap>
</AuthBootstrap>
</RefreshTokenBootstrap>
</ClientBootstrap>
Expand Down
68 changes: 68 additions & 0 deletions packages/app-utils/src/components/ServerConfigBootstrap.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import React, { createContext, useEffect, useState } from 'react';
import { LoadingOverlay } from '@deephaven/components';
import { useClient } from '@deephaven/jsapi-bootstrap';
import { getErrorMessage } from '@deephaven/utils';

export const ServerConfigContext = createContext<Map<string, string> | null>(
null
);

export type ServerConfigBootstrapProps = {
/**
* The children to render after server config is loaded.
*/
children: React.ReactNode;
};

/**
* ServerConfigBootstrap component. Handles loading the server config.
*/
export function ServerConfigBootstrap({
children,
}: ServerConfigBootstrapProps) {
const client = useClient();
const [serverConfig, setServerConfig] = useState<Map<string, string>>();
const [error, setError] = useState<unknown>();

useEffect(
function initServerConfigValues() {
let isCanceled = false;
async function loadServerConfigValues() {
try {
const newServerConfigValues = await client.getServerConfigValues();
if (!isCanceled) {
setServerConfig(new Map(newServerConfigValues));
}
} catch (e) {
if (!isCanceled) {
setError(e);
}
}
}
loadServerConfigValues();
return () => {
isCanceled = true;
};
},
[client]
);

const isLoading = serverConfig == null;

if (isLoading || error != null) {
return (
<LoadingOverlay
isLoading={isLoading && error == null}
errorMessage={getErrorMessage(error)}
/>
);
}

return (
<ServerConfigContext.Provider value={serverConfig}>
{children}
</ServerConfigContext.Provider>
);
}

export default ServerConfigBootstrap;
26 changes: 26 additions & 0 deletions packages/app-utils/src/components/UserBootstrap.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React, { useContext } from 'react';
import {
UserContext,
UserOverrideContext,
UserPermissionsOverrideContext,
getUserFromConfig,
} from '@deephaven/auth-plugins';
import useServerConfig from './useServerConfig';

export type UserBootstrapProps = {
/** The children to render */
children: React.ReactNode;
};

/**
* UserBootstrap component. Derives the UserContext from the ServerConfigContext, UserOverrideContext, and UserPermissionsOverrideContext.
*/
export function UserBootstrap({ children }: UserBootstrapProps) {
const serverConfig = useServerConfig();
const overrides = useContext(UserOverrideContext);
const permissionsOverrides = useContext(UserPermissionsOverrideContext);
const user = getUserFromConfig(serverConfig, overrides, permissionsOverrides);
return <UserContext.Provider value={user}>{children}</UserContext.Provider>;
}

export default UserBootstrap;
2 changes: 2 additions & 0 deletions packages/app-utils/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ export * from './FontsLoaded';
export * from './PluginsBootstrap';
export * from './usePlugins';
export * from './useConnection';
export * from './useServerConfig';
export * from './useUser';
11 changes: 11 additions & 0 deletions packages/app-utils/src/components/useServerConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { useContextOrThrow } from '@deephaven/react-hooks';
import { ServerConfigContext } from './ServerConfigBootstrap';

export function useServerConfig() {
return useContextOrThrow(
ServerConfigContext,
'No server config available in useServerConfig. Was code wrapped in ServerConfigBootstrap or ServerConfigContext.Provider?'
);
}

export default useServerConfig;
11 changes: 11 additions & 0 deletions packages/app-utils/src/components/useUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { useContextOrThrow } from '@deephaven/react-hooks';
import { UserContext } from '@deephaven/auth-plugins';

export function useUser() {
return useContextOrThrow(
UserContext,
'No user available in useUser. Was code wrapped in UserBootstrap or UserContext.Provider?'
);
}

export default useUser;
2 changes: 2 additions & 0 deletions packages/auth-plugins/src/UserContexts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ export const UserOverrideContext = createContext<UserOverride>({});
export const UserPermissionsOverrideContext = createContext<UserPermissionsOverride>(
{}
);

export const UserContext = createContext<User | null>(null);
91 changes: 91 additions & 0 deletions packages/auth-plugins/src/UserUtils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { getAppInitValue, getUserFromConfig } from './UserUtils';

it('returns the value for the AppInit key', () => {
const serverConfig = new Map<string, string>();
serverConfig.set('internal.webClient.appInit.name', 'test');
serverConfig.set('internal.webClient.appInit.foo', 'bar');
serverConfig.set('name', 'not-test');
expect(getAppInitValue(serverConfig, 'name')).toEqual('test');
expect(getAppInitValue(serverConfig, 'foo')).toEqual('bar');
expect(getAppInitValue(serverConfig, 'food')).toEqual(undefined);
expect(
getAppInitValue(serverConfig, 'internal.webClient.appInit.name')
).toEqual(undefined);
expect(getAppInitValue(serverConfig, '')).toEqual(undefined);
});

describe('getUser', () => {
it('returns the default user and permissions', () => {
const serverConfig = new Map<string, string>();
expect(getUserFromConfig(serverConfig)).toEqual({
name: '',
operateAs: '',
groups: [],
permissions: {
canUsePanels: true,
canCopy: true,
canDownloadCsv: true,
canLogout: true,
},
});
});

it('returns the values from the config correctly', () => {
const serverConfig = new Map<string, string>();
serverConfig.set('internal.webClient.appInit.name', 'test');
serverConfig.set('internal.webClient.appInit.operateAs', 'test-operator');
serverConfig.set('internal.webClient.appInit.groups', 'group1,group2');
serverConfig.set('internal.webClient.appInit.canUsePanels', 'false');
serverConfig.set('internal.webClient.appInit.canCopy', 'false');
serverConfig.set('internal.webClient.appInit.canDownloadCsv', 'false');
serverConfig.set('internal.webClient.appInit.canLogout', 'false');
expect(getUserFromConfig(serverConfig)).toEqual({
name: 'test',
operateAs: 'test-operator',
groups: ['group1', 'group2'],
permissions: {
canUsePanels: false,
canCopy: false,
canDownloadCsv: false,
canLogout: false,
},
});
});

it('overrides the default values correctly', () => {
const serverConfig = new Map<string, string>();
serverConfig.set('internal.webClient.appInit.name', 'test');
serverConfig.set('internal.webClient.appInit.operateAs', 'test-operator');
serverConfig.set('internal.webClient.appInit.groups', 'group1,group2');
serverConfig.set('internal.webClient.appInit.canUsePanels', 'false');
serverConfig.set('internal.webClient.appInit.canCopy', 'false');
serverConfig.set('internal.webClient.appInit.canDownloadCsv', 'false');
serverConfig.set('internal.webClient.appInit.canLogout', 'false');
expect(
getUserFromConfig(
serverConfig,
{
name: 'test2',
operateAs: 'test2-operator',
groups: ['group3', 'group4'],
},
{
canUsePanels: true,
canCopy: true,
canDownloadCsv: true,
canLogout: true,
}
)
).toEqual({
name: 'test2',
operateAs: 'test2-operator',
groups: ['group3', 'group4'],
permissions: {
canUsePanels: true,
canCopy: true,
canDownloadCsv: true,
canLogout: true,
},
});
});
});
70 changes: 70 additions & 0 deletions packages/auth-plugins/src/UserUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { User, UserPermissions } from '@deephaven/redux';
import Log from '@deephaven/log';

const log = Log.module('UserUtils');

/**
* Retrieve a value from the AppInit config
* @param serverConfig Server config map
* @param key The AppInit key to retrieve
* @returns The value for the AppInit key
*/
export function getAppInitValue(
serverConfig: Map<string, string>,
key: string
): string | undefined {
return serverConfig.get(`internal.webClient.appInit.${key}`);
}

/**
* Retrieve a user object provided the server config and overrides
* @param serverConfig Server config map
* @param overrides Override values for the user
* @param permissionsOverrides Override specific permissions for the user
* @returns The user object
*/
export function getUserFromConfig(
serverConfig: Map<string, string>,
overrides: Partial<Omit<User, 'permissions'>> = {},
permissionsOverrides: Partial<UserPermissions> = {}
): User {
function getValue(key: string): string | undefined {
return getAppInitValue(serverConfig, key);
}
function getBooleanValue(key: string, defaultValue: boolean): boolean {
const value = getValue(key);
if (value === 'true') {
return true;
}
if (value === 'false') {
return false;
}
if (value !== undefined) {
log.warn(`Unexpected value for ${key}: ${value}`);
}
return defaultValue;
}
const name = getValue('name') ?? '';
const operateAs = getValue('operateAs') ?? name;
const groups = getValue('groups')?.split(',') ?? [];
const canCopy = getBooleanValue('canCopy', true);
const canDownloadCsv = getBooleanValue('canDownloadCsv', true);
const canUsePanels = getBooleanValue('canUsePanels', true);
const canLogout = getBooleanValue('canLogout', true);

return {
name,
operateAs,
groups,
...overrides,
permissions: {
canUsePanels,
canCopy,
canDownloadCsv,
canLogout,
...permissionsOverrides,
},
};
}

export default { getAppInitValue, getUser: getUserFromConfig };
1 change: 1 addition & 0 deletions packages/auth-plugins/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ export { default as AuthPluginParent } from './AuthPluginParent';
export { default as AuthPluginPsk } from './AuthPluginPsk';
export * from './Login';
export * from './LoginForm';
export * from './UserUtils';
export * from './UserContexts';
Loading

0 comments on commit 8cf2bbd

Please sign in to comment.