Skip to content

Commit

Permalink
Feat: setup reserved workspaces when calling list workspaces API (ope…
Browse files Browse the repository at this point in the history
…nsearch-project#178)

* feat: setup reserved workspaces when calling list workspaces API

Signed-off-by: SuZhou-Joe <[email protected]>

* feat: fix test flow

Signed-off-by: SuZhou-Joe <[email protected]>

* feat: fix lint

Signed-off-by: SuZhou-Joe <[email protected]>

* feat: fix typos

Signed-off-by: SuZhou-Joe <[email protected]>

* fix: setup personal workspace using correct id

Signed-off-by: SuZhou-Joe <[email protected]>

* refractor: move function getPrincipalsFromRequest to a util function

Signed-off-by: SuZhou-Joe <[email protected]>

* feat: change code according to comment

Signed-off-by: SuZhou-Joe <[email protected]>

* feat: modify reserved workspace permission

Signed-off-by: SuZhou-Joe <[email protected]>

---------

Signed-off-by: SuZhou-Joe <[email protected]>
  • Loading branch information
SuZhou-Joe authored Sep 20, 2023
1 parent 93b1e44 commit 4bccdea
Show file tree
Hide file tree
Showing 10 changed files with 240 additions and 154 deletions.
1 change: 1 addition & 0 deletions src/core/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,7 @@ export {
PUBLIC_WORKSPACE_ID,
MANAGEMENT_WORKSPACE_ID,
WORKSPACE_TYPE,
PERSONAL_WORKSPACE_ID_PREFIX,
} from '../utils';

export {
Expand Down
2 changes: 2 additions & 0 deletions src/core/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,5 @@ export enum WorkspacePermissionMode {
export const PUBLIC_WORKSPACE_ID = 'public';

export const MANAGEMENT_WORKSPACE_ID = 'management';

export const PERSONAL_WORKSPACE_ID_PREFIX = 'personal';
1 change: 1 addition & 0 deletions src/core/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,5 @@ export {
PUBLIC_WORKSPACE_ID,
MANAGEMENT_WORKSPACE_ID,
WORKSPACE_TYPE,
PERSONAL_WORKSPACE_ID_PREFIX,
} from './constants';
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ export const savedObjectsPermissionControlMock: SavedObjectsPermissionControlCon
batchValidate: jest.fn(),
getPrincipalsOfObjects: jest.fn(),
getPermittedWorkspaceIds: jest.fn(),
getPrincipalsFromRequest: jest.fn(),
setup: jest.fn(),
};
41 changes: 4 additions & 37 deletions src/plugins/workspace/server/permission_control/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,17 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { i18n } from '@osd/i18n';
import { ensureRawRequest, OpenSearchDashboardsRequest } from '../../../../core/server';
import { OpenSearchDashboardsRequest } from '../../../../core/server';
import {
ACL,
Principals,
TransformedPermission,
PrincipalType,
SavedObjectsBulkGetObject,
SavedObjectsServiceStart,
Logger,
WORKSPACE_TYPE,
} from '../../../../core/server';
import { WORKSPACE_SAVED_OBJECTS_CLIENT_WRAPPER_ID } from '../../common/constants';
import { getPrincipalsFromRequest } from '../utils';

export type SavedObjectsPermissionControlContract = Pick<
SavedObjectsPermissionControl,
Expand All @@ -23,11 +22,6 @@ export type SavedObjectsPermissionControlContract = Pick<

export type SavedObjectsPermissionModes = string[];

export interface AuthInfo {
backend_roles?: string[];
user_name?: string;
}

export class SavedObjectsPermissionControl {
private readonly logger: Logger;
private _getScopedClient?: SavedObjectsServiceStart['getScopedClient'];
Expand All @@ -41,33 +35,6 @@ export class SavedObjectsPermissionControl {
this.logger = logger;
}

public getPrincipalsFromRequest(request: OpenSearchDashboardsRequest): Principals {
const rawRequest = ensureRawRequest(request);
const authInfo = rawRequest?.auth?.credentials?.authInfo as AuthInfo | null;
const payload: Principals = {};
if (!authInfo) {
/**
* Login user have access to all the workspaces when no authentication is presented.
* The logic will be used when users create workspaces with authentication enabled but turn off authentication for any reason.
*/
return payload;
}
if (!authInfo?.backend_roles?.length && !authInfo.user_name) {
/**
* It means OSD can not recognize who the user is even if authentication is enabled,
* use a fake user that won't be granted permission explicitly.
*/
payload[PrincipalType.Users] = [`_user_fake_${Date.now()}_`];
return payload;
}
if (authInfo?.backend_roles) {
payload[PrincipalType.Groups] = authInfo.backend_roles;
}
if (authInfo?.user_name) {
payload[PrincipalType.Users] = [authInfo.user_name];
}
return payload;
}
private async bulkGetSavedObjects(
request: OpenSearchDashboardsRequest,
savedObjects: SavedObjectsBulkGetObject[]
Expand Down Expand Up @@ -114,7 +81,7 @@ export class SavedObjectsPermissionControl {
};
}

const principals = this.getPrincipalsFromRequest(request);
const principals = getPrincipalsFromRequest(request);
let savedObjectsBasicInfo: any[] = [];
const hasAllPermission = savedObjectsGet.every((item) => {
// for object that doesn't contain ACL like config, return true
Expand Down Expand Up @@ -168,7 +135,7 @@ export class SavedObjectsPermissionControl {
request: OpenSearchDashboardsRequest,
permissionModes: SavedObjectsPermissionModes
) {
const principals = this.getPrincipalsFromRequest(request);
const principals = getPrincipalsFromRequest(request);
const savedObjectClient = this.getScopedClient?.(request);
try {
const result = await savedObjectClient?.find({
Expand Down
102 changes: 3 additions & 99 deletions src/plugins/workspace/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,19 @@
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/
import { i18n } from '@osd/i18n';
import { Observable } from 'rxjs';
import {
PluginInitializerContext,
CoreSetup,
CoreStart,
Plugin,
Logger,
ISavedObjectsRepository,
WORKSPACE_TYPE,
ACL,
PUBLIC_WORKSPACE_ID,
MANAGEMENT_WORKSPACE_ID,
Permissions,
WorkspacePermissionMode,
SavedObjectsClient,
WorkspaceAttribute,
DEFAULT_APP_CATEGORIES,
} from '../../../core/server';
import { IWorkspaceDBImpl } from './types';
import { WorkspaceClientWithSavedObject } from './workspace_client';
import { WorkspaceSavedObjectsClientWrapper } from './saved_objects';
import { registerRoutes } from './routes';
import {
WORKSPACE_OVERVIEW_APP_ID,
WORKSPACE_UPDATE_APP_ID,
WORKSPACE_SAVED_OBJECTS_CLIENT_WRAPPER_ID,
} from '../common/constants';
import { ConfigSchema } from '../config';
import { WORKSPACE_SAVED_OBJECTS_CLIENT_WRAPPER_ID } from '../common/constants';
import {
SavedObjectsPermissionControl,
SavedObjectsPermissionControlContract,
Expand All @@ -40,7 +24,6 @@ import { registerPermissionCheckRoutes } from './permission_control/routes';
export class WorkspacePlugin implements Plugin<{}, {}> {
private readonly logger: Logger;
private client?: IWorkspaceDBImpl;
private config$: Observable<ConfigSchema>;
private permissionControl?: SavedObjectsPermissionControlContract;

private proxyWorkspaceTrafficToRealHandler(setupDeps: CoreSetup) {
Expand All @@ -62,13 +45,12 @@ export class WorkspacePlugin implements Plugin<{}, {}> {

constructor(initializerContext: PluginInitializerContext) {
this.logger = initializerContext.logger.get('plugins', 'workspace');
this.config$ = initializerContext.config.create<ConfigSchema>();
}

public async setup(core: CoreSetup) {
this.logger.debug('Setting up Workspaces service');

this.client = new WorkspaceClientWithSavedObject(core);
this.client = new WorkspaceClientWithSavedObject(core, this.logger);

await this.client.setup(core);
this.permissionControl = new SavedObjectsPermissionControl(this.logger);
Expand All @@ -79,10 +61,7 @@ export class WorkspacePlugin implements Plugin<{}, {}> {
});

const workspaceSavedObjectsClientWrapper = new WorkspaceSavedObjectsClientWrapper(
this.permissionControl,
{
config$: this.config$,
}
this.permissionControl
);

core.savedObjects.addClientWrapper(
Expand All @@ -108,85 +87,10 @@ export class WorkspacePlugin implements Plugin<{}, {}> {
};
}

private async checkAndCreateWorkspace(
internalRepository: ISavedObjectsRepository,
workspaceId: string,
workspaceAttribute: Omit<WorkspaceAttribute, 'id' | 'permissions'>,
permissions?: Permissions
) {
/**
* Internal repository is attached to global tenant.
*/
try {
await internalRepository.get(WORKSPACE_TYPE, workspaceId);
} catch (error) {
this.logger.debug(error?.toString() || '');
this.logger.info(`Workspace ${workspaceId} is not found, create it by using internal user`);
try {
const createResult = await internalRepository.create(WORKSPACE_TYPE, workspaceAttribute, {
id: workspaceId,
permissions,
});
if (createResult.id) {
this.logger.info(`Created workspace ${createResult.id} in global tenant.`);
}
} catch (e) {
this.logger.error(`Create ${workspaceId} workspace error: ${e?.toString() || ''}`);
}
}
}

private async setupWorkspaces(startDeps: CoreStart) {
const internalRepository = startDeps.savedObjects.createInternalRepository();
const publicWorkspaceACL = new ACL().addPermission(
[WorkspacePermissionMode.LibraryRead, WorkspacePermissionMode.LibraryWrite],
{
users: ['*'],
}
);
const managementWorkspaceACL = new ACL().addPermission([WorkspacePermissionMode.LibraryRead], {
users: ['*'],
});
const DSM_APP_ID = 'dataSources';
const DEV_TOOLS_APP_ID = 'dev_tools';

await Promise.all([
this.checkAndCreateWorkspace(
internalRepository,
PUBLIC_WORKSPACE_ID,
{
name: i18n.translate('workspaces.public.workspace.default.name', {
defaultMessage: 'public',
}),
features: ['*', `!@${DEFAULT_APP_CATEGORIES.management.id}`],
},
publicWorkspaceACL.getPermissions()
),
this.checkAndCreateWorkspace(
internalRepository,
MANAGEMENT_WORKSPACE_ID,
{
name: i18n.translate('workspaces.management.workspace.default.name', {
defaultMessage: 'Management',
}),
features: [
`@${DEFAULT_APP_CATEGORIES.management.id}`,
WORKSPACE_OVERVIEW_APP_ID,
WORKSPACE_UPDATE_APP_ID,
DSM_APP_ID,
DEV_TOOLS_APP_ID,
],
},
managementWorkspaceACL.getPermissions()
),
]);
}

public start(core: CoreStart) {
this.logger.debug('Starting SavedObjects service');
this.permissionControl?.setup(core.savedObjects.getScopedClient);
this.client?.setSavedObjects(core.savedObjects);
this.setupWorkspaces(core);

return {
client: this.client as IWorkspaceDBImpl,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
} from '../../../../core/server';
import { SavedObjectsPermissionControlContract } from '../permission_control/client';
import { WorkspaceFindOptions } from '../types';
import { getPrincipalsFromRequest } from '../utils';

// Can't throw unauthorized for now, the page will be refreshed if unauthorized
const generateWorkspacePermissionError = () => {
Expand Down Expand Up @@ -355,7 +356,7 @@ export class WorkspaceSavedObjectsClientWrapper {
const findWithWorkspacePermissionControl = async <T = unknown>(
options: SavedObjectsFindOptions & Pick<WorkspaceFindOptions, 'permissionModes'>
) => {
const principals = this.permissionControl.getPrincipalsFromRequest(wrapperOptions.request);
const principals = getPrincipalsFromRequest(wrapperOptions.request);
if (!options.ACLSearchParams) {
options.ACLSearchParams = {};
}
Expand Down
5 changes: 5 additions & 0 deletions src/plugins/workspace/server/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,8 @@ export type WorkspaceRoutePermissionItem = {
| WorkspacePermissionMode.Management
>;
} & ({ type: 'user'; userId: string } | { type: 'group'; group: string });

export interface AuthInfo {
backend_roles?: string[];
user_name?: string;
}
35 changes: 35 additions & 0 deletions src/plugins/workspace/server/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,45 @@
*/

import crypto from 'crypto';
import {
ensureRawRequest,
OpenSearchDashboardsRequest,
Principals,
PrincipalType,
} from '../../../core/server';
import { AuthInfo } from './types';

/**
* Generate URL friendly random ID
*/
export const generateRandomId = (size: number) => {
return crypto.randomBytes(size).toString('base64url').slice(0, size);
};

export const getPrincipalsFromRequest = (request: OpenSearchDashboardsRequest): Principals => {
const rawRequest = ensureRawRequest(request);
const authInfo = rawRequest?.auth?.credentials?.authInfo as AuthInfo | null;
const payload: Principals = {};
if (!authInfo) {
/**
* Login user have access to all the workspaces when no authentication is presented.
* The logic will be used when users create workspaces with authentication enabled but turn off authentication for any reason.
*/
return payload;
}
if (!authInfo?.backend_roles?.length && !authInfo.user_name) {
/**
* It means OSD can not recognize who the user is even if authentication is enabled,
* use a fake user that won't be granted permission explicitly.
*/
payload[PrincipalType.Users] = [`_user_fake_${Date.now()}_`];
return payload;
}
if (authInfo?.backend_roles) {
payload[PrincipalType.Groups] = authInfo.backend_roles;
}
if (authInfo?.user_name) {
payload[PrincipalType.Users] = [authInfo.user_name];
}
return payload;
};
Loading

0 comments on commit 4bccdea

Please sign in to comment.