Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integration ACL check with saved objects. #74

Merged
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions src/core/public/saved_objects/saved_objects_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,7 @@ export class SavedObjectsClient {
namespaces: 'namespaces',
preference: 'preference',
workspaces: 'workspaces',
queryDSL: 'queryDSL',
};

const workspaces = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,13 +111,13 @@ describe('SavedObjectTypeRegistry', () => {
expect(result?.length).toEqual(3);
});

it('test genereate query DSL', () => {
it('test generate query DSL', () => {
const principals = {
users: ['user1'],
groups: ['group1'],
};
const result = ACL.genereateGetPermittedSavedObjectsQueryDSL(
PermissionMode.Read,
[PermissionMode.Read],
principals,
'workspace'
);
Expand Down
48 changes: 21 additions & 27 deletions src/core/server/saved_objects/permission_control/acl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export interface Principals {
groups?: string[];
}

export type Permissions = Partial<Record<string, Principals>>;
export type Permissions = Record<string, Principals>;

export interface TransformedPermission {
type: string;
Expand Down Expand Up @@ -121,11 +121,14 @@ export class ACL {
}

for (const permissionType of permissionTypes) {
this.permissions[permissionType] = deleteFromPrincipals(
const result = deleteFromPrincipals(
this.permissions![permissionType],
principals.users,
principals.groups
);
if (result) {
this.permissions[permissionType] = result;
}
}

return this;
Expand Down Expand Up @@ -204,11 +207,11 @@ export class ACL {
generate query DSL by the specific conditions, used for fetching saved objects from the saved objects index
*/
public static genereateGetPermittedSavedObjectsQueryDSL(
permissionType: string,
permissionTypes: string[],
principals: Principals,
savedObjectType?: string | string[]
) {
if (!principals || !permissionType) {
if (!principals || !permissionTypes) {
return {
query: {
match_none: {},
Expand All @@ -222,30 +225,21 @@ export class ACL {
const subBool: any = {
should: [],
};
if (!!principals.users) {
subBool.should.push({
terms: {
['permissions.' + permissionType + '.users']: principals.users,
},
});
subBool.should.push({
term: {
['permissions.' + permissionType + '.users']: '*',
},
});
}
if (!!principals.groups) {
subBool.should.push({
terms: {
['permissions.' + permissionType + '.groups']: principals.groups,
},
});
subBool.should.push({
term: {
['permissions.' + permissionType + '.groups']: '*',
},

permissionTypes.forEach((permissionType) => {
Object.entries(principals).forEach(([principalType, principalsInCurrentType]) => {
subBool.should.push({
terms: {
['permissions.' + permissionType + `.${principalType}`]: principalsInCurrentType,
},
});
subBool.should.push({
term: {
['permissions.' + permissionType + `.${principalType}`]: '*',
},
});
});
}
});

bool.filter.push({
bool: subBool,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,13 @@
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { SavedObjectsPermissionControlContract } from './client';

export const savedObjectsPermissionControlMock: SavedObjectsPermissionControlContract = {
setup: jest.fn(),
validate: jest.fn(),
batchValidate: jest.fn(),
addPrinciplesToObjects: jest.fn(),
removePrinciplesFromObjects: jest.fn(),
getPrinciplesOfObjects: jest.fn(),
getPrincipalsOfObjects: jest.fn(),
getPermittedWorkspaceIds: jest.fn(),
getPrincipalsFromRequest: jest.fn(),
};
129 changes: 91 additions & 38 deletions src/core/server/saved_objects/permission_control/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { i18n } from '@osd/i18n';
import { OpenSearchDashboardsRequest } from '../../http';
import { ensureRawRequest } from '../../http/router';
import { SavedObjectsServiceStart } from '../saved_objects_service';
import { SavedObjectsBulkGetObject } from '../service';
import { ACL, Principals, TransformedPermission } from './acl';
import { PrincipalType } from '../../../utils/constants';
import { WORKSPACE_TYPE } from '../../workspaces';

export type SavedObjectsPermissionControlContract = Pick<
SavedObjectsPermissionControl,
Expand All @@ -14,78 +18,127 @@ export type SavedObjectsPermissionControlContract = Pick<

export type SavedObjectsPermissionModes = string[];

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

export class SavedObjectsPermissionControl {
private getScopedClient?: SavedObjectsServiceStart['getScopedClient'];
private getScopedSavedObjectsClient(request: OpenSearchDashboardsRequest) {
return this.getScopedClient?.(request);
private createInternalRepository?: SavedObjectsServiceStart['createInternalRepository'];
private getInternalRepository() {
return this.createInternalRepository?.();
}
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[]
) {
return (
(await this.getScopedSavedObjectsClient(request)?.bulkGet(savedObjects))?.saved_objects || []
);
return (await this.getInternalRepository()?.bulkGet(savedObjects))?.saved_objects || [];
}
public async setup(getScopedClient: SavedObjectsServiceStart['getScopedClient']) {
this.getScopedClient = getScopedClient;
public async setup(
createInternalRepository: SavedObjectsServiceStart['createInternalRepository']
) {
this.createInternalRepository = createInternalRepository;
}
public async validate(
request: OpenSearchDashboardsRequest,
savedObject: SavedObjectsBulkGetObject,
permissionModeOrModes: SavedObjectsPermissionModes
permissionModes: SavedObjectsPermissionModes
) {
return await this.batchValidate(request, [savedObject], permissionModeOrModes);
return await this.batchValidate(request, [savedObject], permissionModes);
}

/**
* In batch validate case, the logic is a.withPermission && b.withPermission
* @param request
* @param savedObjects
* @param permissionModes
* @returns
*/
public async batchValidate(
request: OpenSearchDashboardsRequest,
savedObjects: SavedObjectsBulkGetObject[],
permissionModeOrModes: SavedObjectsPermissionModes
permissionModes: SavedObjectsPermissionModes
) {
const savedObjectsGet = await this.bulkGetSavedObjects(request, savedObjects);
if (savedObjectsGet) {
const principals = this.getPrincipalsFromRequest(request);
const hasAllPermission = savedObjectsGet.every((item) => {
// item.permissions
const aclInstance = new ACL(item.permissions);
return aclInstance.hasPermission(permissionModes, principals);
});
return {
success: true,
result: true,
result: hasAllPermission,
};
}

return {
success: true,
result: false,
success: false,
error: i18n.translate('savedObjects.permission.notFound', {
defaultMessage: 'Can not find target saved objects.',
}),
};
}

public async addPrinciplesToObjects(
request: OpenSearchDashboardsRequest,
savedObjects: SavedObjectsBulkGetObject[],
personas: string[],
permissionModeOrModes: SavedObjectsPermissionModes
): Promise<boolean> {
return true;
}

public async removePrinciplesFromObjects(
request: OpenSearchDashboardsRequest,
savedObjects: SavedObjectsBulkGetObject[],
personas: string[],
permissionModeOrModes: SavedObjectsPermissionModes
): Promise<boolean> {
return true;
}

public async getPrinciplesOfObjects(
public async getPrincipalsOfObjects(
request: OpenSearchDashboardsRequest,
savedObjects: SavedObjectsBulkGetObject[]
): Promise<Record<string, unknown>> {
return {};
): Promise<Record<string, TransformedPermission>> {
const detailedSavedObjects = await this.bulkGetSavedObjects(request, savedObjects);
return detailedSavedObjects.reduce((total, current) => {
return {
...total,
[current.id]: new ACL(current.permissions).transformPermissions(),
};
}, {});
}

public async getPermittedWorkspaceIds(
request: OpenSearchDashboardsRequest,
permissionModeOrModes: SavedObjectsPermissionModes
permissionModes: SavedObjectsPermissionModes
) {
return [];
const principals = this.getPrincipalsFromRequest(request);
SuZhou-Joe marked this conversation as resolved.
Show resolved Hide resolved
const queryDSL = ACL.genereateGetPermittedSavedObjectsQueryDSL(permissionModes, principals, [
WORKSPACE_TYPE,
]);
const repository = this.getInternalRepository();
try {
const result = await repository?.find({
type: [WORKSPACE_TYPE],
queryDSL,
SuZhou-Joe marked this conversation as resolved.
Show resolved Hide resolved
perPage: 999,
});
return result?.saved_objects.map((item) => item.id);
} catch (e) {
return [];
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export const registerListRoute = (
) => {
router.post(
{
path: '/principles',
path: '/principals',
validate: {
body: schema.object({
objects: schema.arrayOf(
Expand All @@ -26,7 +26,7 @@ export const registerListRoute = (
},
},
router.handleLegacyErrors(async (context, req, res) => {
const result = await permissionControl.getPrinciplesOfObjects(req, req.body.objects);
const result = await permissionControl.getPrincipalsOfObjects(req, req.body.objects);
return res.ok({ body: result });
})
);
Expand Down
2 changes: 1 addition & 1 deletion src/core/server/saved_objects/saved_objects_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -500,7 +500,7 @@ export class SavedObjectsService
this.started = true;

const getScopedClient = clientProvider.getClient.bind(clientProvider);
this.permissionControl?.setup(getScopedClient);
this.permissionControl?.setup(repositoryFactory.createInternalRepository);

return {
getScopedClient,
Expand Down
2 changes: 2 additions & 0 deletions src/core/server/saved_objects/serialization/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
* under the License.
*/

import { Permissions } from '../permission_control/acl';
import { SavedObjectsMigrationVersion, SavedObjectReference } from '../types';

/**
Expand All @@ -53,6 +54,7 @@ export interface SavedObjectsRawDocSource {
references?: SavedObjectReference[];
originId?: string;
workspaces?: string[];
permissions?: Permissions;

[typeMapping: string]: any;
}
Expand Down
5 changes: 4 additions & 1 deletion src/core/server/saved_objects/service/lib/repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -769,6 +769,7 @@ export class SavedObjectsRepository {
filter,
preference,
workspaces,
queryDSL,
} = options;

if (!type && !typeToNamespacesMap) {
Expand Down Expand Up @@ -843,6 +844,7 @@ export class SavedObjectsRepository {
hasReference,
kueryNode,
workspaces,
queryDSL,
}),
},
};
Expand Down Expand Up @@ -1897,7 +1899,7 @@ function getSavedObjectFromSource<T>(
id: string,
doc: { _seq_no?: number; _primary_term?: number; _source: SavedObjectsRawDocSource }
): SavedObject<T> {
const { originId, updated_at: updatedAt, workspaces } = doc._source;
const { originId, updated_at: updatedAt, workspaces, permissions } = doc._source;

let namespaces: string[] = [];
if (!registry.isNamespaceAgnostic(type)) {
Expand All @@ -1917,6 +1919,7 @@ function getSavedObjectFromSource<T>(
attributes: doc._source[type],
references: doc._source.references || [],
migrationVersion: doc._source.migrationVersion,
permissions,
};
}

Expand Down
Loading
Loading