Skip to content

Commit 3833778

Browse files
Added workspace ACL checks for Alerting and Monitor APIs
Signed-off-by: nishtham <nishtham@amazon.com>
1 parent 7469f74 commit 3833778

File tree

6 files changed

+448
-3
lines changed

6 files changed

+448
-3
lines changed

opensearch_dashboards.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"version": "3.6.0.0",
44
"opensearchDashboardsVersion": "3.6.0",
55
"configPath": ["opensearch_alerting"],
6-
"optionalPlugins": ["dataSource", "dataSourceManagement", "assistantDashboards", "explore"],
6+
"optionalPlugins": ["dataSource", "dataSourceManagement", "assistantDashboards", "explore", "workspace"],
77
"requiredPlugins": [
88
"uiActions",
99
"dashboard",

server/plugin.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export class AlertingPlugin {
3838
this.pluginConfig$ = initializerContext.config.create();
3939
this.core = null;
4040
this.featureFlagService = null;
41+
this.services = null;
4142
}
4243

4344
async setup(core, { dataSource }) {
@@ -89,6 +90,7 @@ export class AlertingPlugin {
8990
crossClusterService,
9091
commentsService,
9192
};
93+
this.services = services;
9294

9395
core.capabilities.registerProvider(() => ({
9496
alertingDashboards: {
@@ -170,7 +172,17 @@ export class AlertingPlugin {
170172
return {};
171173
}
172174

173-
async start(core) {
175+
async start(core, plugins) {
176+
if (this.services) {
177+
Object.values(this.services).forEach((service) => {
178+
if (typeof service.setLogger === 'function') {
179+
service.setLogger(this.logger);
180+
}
181+
if (plugins?.workspace && typeof service.setWorkspaceStart === 'function') {
182+
service.setWorkspaceStart(plugins.workspace);
183+
}
184+
});
185+
}
174186
return {};
175187
}
176188
}

server/services/AlertService.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,17 @@ export const GET_ALERTS_SORT_FILTERS = {
1414
};
1515

1616
export default class AlertService extends MDSEnabledClientService {
17+
_enforceWorkspaceAcl = async (context, req, res, permissionModes) => {
18+
const authorized = await this.checkWorkspaceAcl(context, req, permissionModes);
19+
if (!authorized) {
20+
return res.ok({ body: { ok: false, resp: 'Workspace ACL check failed: unauthorized' } });
21+
}
22+
return null;
23+
};
24+
1725
getAlerts = async (context, req, res) => {
26+
const aclResponse = await this._enforceWorkspaceAcl(context, req, res, ['library_write', 'library_read']);
27+
if (aclResponse) return aclResponse;
1828
const {
1929
from = 0,
2030
size = 20,
@@ -111,6 +121,8 @@ export default class AlertService extends MDSEnabledClientService {
111121
};
112122

113123
getWorkflowAlerts = async (context, req, res) => {
124+
const aclResponse = await this._enforceWorkspaceAcl(context, req, res, ['library_write', 'library_read']);
125+
if (aclResponse) return aclResponse;
114126
const client = this.getClientBasedOnDataSource(context, req);
115127
try {
116128
const resp = await client('alerting.getWorkflowAlerts', req.query);
Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,59 @@
1-
import { RequestHandlerContext, OpenSearchDashboardsRequest, ILegacyCustomClusterClient } from '../../../../src/core/server';
1+
import { RequestHandlerContext, OpenSearchDashboardsRequest, ILegacyCustomClusterClient, Logger } from '../../../../src/core/server';
2+
import { getWorkspaceState } from '../../../../src/core/server/utils';
3+
4+
interface WorkspaceAuthorizer {
5+
authorizeWorkspace: (
6+
request: OpenSearchDashboardsRequest,
7+
workspaceIds: string[],
8+
principal: string,
9+
permissionModes?: string[]
10+
) => Promise<{ authorized: boolean; unauthorizedWorkspaces?: string[] }>;
11+
}
212

313
export abstract class MDSEnabledClientService {
14+
private workspaceStart?: WorkspaceAuthorizer;
15+
private logger?: Logger;
16+
417
constructor(private osDriver: ILegacyCustomClusterClient, private dataSourceEnabled: boolean) {}
518

19+
public setWorkspaceStart(workspaceStart: WorkspaceAuthorizer) {
20+
this.workspaceStart = workspaceStart;
21+
}
22+
23+
public setLogger(logger: Logger) {
24+
this.logger = logger;
25+
}
26+
627
protected getClientBasedOnDataSource(context: RequestHandlerContext, request: OpenSearchDashboardsRequest) {
728
const dataSourceId = (request.query as any).dataSourceId;
829
return this.dataSourceEnabled && dataSourceId
930
? context.dataSource.opensearch.legacy.getClient(dataSourceId.toString()).callAPI
1031
: this.osDriver.asScoped(request).callAsCurrentUser;
1132
}
33+
34+
protected async checkWorkspaceAcl(context: RequestHandlerContext, request: OpenSearchDashboardsRequest, permissionModes: string[] = ['read']): Promise<boolean> {
35+
// Only run workspace ACL check for serverless (AOSS) data sources
36+
const dataSourceId = (request.query as any).dataSourceId;
37+
if (dataSourceId) {
38+
const savedObjectsClient = context.core.savedObjects.client;
39+
const dataSource = await savedObjectsClient.get('data-source', dataSourceId.toString());
40+
const endpoint = (dataSource.attributes as any).endpoint as string;
41+
if (!endpoint.includes('.aoss.amazonaws.com')) {
42+
return true;
43+
}
44+
} else {
45+
return true;
46+
}
47+
48+
const principal = request.headers['x-amzn-aosd-username'] as string;
49+
const workspaceId = getWorkspaceState(request).requestWorkspaceId;
50+
51+
if (!principal || !workspaceId || !this.workspaceStart) {
52+
return true;
53+
}
54+
55+
const result = await this.workspaceStart.authorizeWorkspace(request, [workspaceId], principal, permissionModes);
56+
this.logger?.info(`Workspace ACL check: workspace=${workspaceId}, authorized=${result.authorized}, permissionModes=${permissionModes.join(',')}`);
57+
return result.authorized;
58+
}
1259
}

server/services/MonitorService.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,23 @@ import { MDSEnabledClientService } from './MDSEnabledClientService';
1111
import { DEFAULT_HEADERS } from './utils/constants';
1212

1313
export default class MonitorService extends MDSEnabledClientService {
14+
15+
/**
16+
* Checks workspace ACL and returns an unauthorized response if check fails.
17+
* Returns null if authorized, or a response object if unauthorized.
18+
*/
19+
_enforceWorkspaceAcl = async (context, req, res, permissionModes) => {
20+
const authorized = await this.checkWorkspaceAcl(context, req, permissionModes);
21+
if (!authorized) {
22+
return res.ok({ body: { ok: false, resp: 'Workspace ACL check failed: unauthorized' } });
23+
}
24+
return null;
25+
};
26+
1427
createMonitor = async (context, req, res) => {
1528
try {
29+
const aclResponse = await this._enforceWorkspaceAcl(context, req, res, ['library_write']);
30+
if (aclResponse) return aclResponse;
1631
const params = { body: req.body };
1732
const client = this.getClientBasedOnDataSource(context, req);
1833
const createResponse = await client('alerting.createMonitor', params);
@@ -35,6 +50,8 @@ export default class MonitorService extends MDSEnabledClientService {
3550

3651
createWorkflow = async (context, req, res) => {
3752
try {
53+
const aclResponse = await this._enforceWorkspaceAcl(context, req, res, ['library_write']);
54+
if (aclResponse) return aclResponse;
3855
const params = { body: req.body };
3956
const client = this.getClientBasedOnDataSource(context, req);
4057
const createResponse = await client('alerting.createWorkflow', params);
@@ -57,6 +74,8 @@ export default class MonitorService extends MDSEnabledClientService {
5774

5875
deleteMonitor = async (context, req, res) => {
5976
try {
77+
const aclResponse = await this._enforceWorkspaceAcl(context, req, res, ['library_write']);
78+
if (aclResponse) return aclResponse;
6079
const { id } = req.params;
6180
const params = { monitorId: id };
6281
const client = this.getClientBasedOnDataSource(context, req);
@@ -80,6 +99,8 @@ export default class MonitorService extends MDSEnabledClientService {
8099

81100
deleteWorkflow = async (context, req, res) => {
82101
try {
102+
const aclResponse = await this._enforceWorkspaceAcl(context, req, res, ['library_write']);
103+
if (aclResponse) return aclResponse;
83104
const { id } = req.params;
84105
const params = { workflowId: id };
85106
const client = this.getClientBasedOnDataSource(context, req);
@@ -103,6 +124,11 @@ export default class MonitorService extends MDSEnabledClientService {
103124

104125
getMonitor = async (context, req, res) => {
105126
try {
127+
const aclResponse = await this._enforceWorkspaceAcl(context, req, res, [
128+
'library_write',
129+
'library_read',
130+
]);
131+
if (aclResponse) return aclResponse;
106132
const { id } = req.params;
107133
const params = { monitorId: id, headers: DEFAULT_HEADERS };
108134
const client = this.getClientBasedOnDataSource(context, req);
@@ -185,6 +211,11 @@ export default class MonitorService extends MDSEnabledClientService {
185211

186212
getWorkflow = async (context, req, res) => {
187213
try {
214+
const aclResponse = await this._enforceWorkspaceAcl(context, req, res, [
215+
'library_write',
216+
'library_read',
217+
]);
218+
if (aclResponse) return aclResponse;
188219
const { id } = req.params;
189220
const params = { monitorId: id };
190221
const client = this.getClientBasedOnDataSource(context, req);
@@ -225,6 +256,8 @@ export default class MonitorService extends MDSEnabledClientService {
225256

226257
updateMonitor = async (context, req, res) => {
227258
try {
259+
const aclResponse = await this._enforceWorkspaceAcl(context, req, res, ['library_write']);
260+
if (aclResponse) return aclResponse;
228261
const { id } = req.params;
229262
const params = { monitorId: id, body: req.body, refresh: 'wait_for' };
230263
const { type } = req.body;
@@ -262,6 +295,11 @@ export default class MonitorService extends MDSEnabledClientService {
262295

263296
getMonitors = async (context, req, res) => {
264297
try {
298+
const aclResponse = await this._enforceWorkspaceAcl(context, req, res, [
299+
'library_write',
300+
'library_read',
301+
]);
302+
if (aclResponse) return aclResponse;
265303
const { from, size, search, sortDirection, sortField, state, monitorIds } = req.query;
266304

267305
let must = { match_all: {} };
@@ -508,6 +546,8 @@ export default class MonitorService extends MDSEnabledClientService {
508546

509547
acknowledgeAlerts = async (context, req, res) => {
510548
try {
549+
const aclResponse = await this._enforceWorkspaceAcl(context, req, res, ['library_write']);
550+
if (aclResponse) return aclResponse;
511551
const { id } = req.params;
512552
const params = {
513553
monitorId: id,
@@ -534,6 +574,8 @@ export default class MonitorService extends MDSEnabledClientService {
534574

535575
acknowledgeChainedAlerts = async (context, req, res) => {
536576
try {
577+
const aclResponse = await this._enforceWorkspaceAcl(context, req, res, ['library_write']);
578+
if (aclResponse) return aclResponse;
537579
const { id } = req.params;
538580
const params = {
539581
workflowId: id,
@@ -565,6 +607,8 @@ export default class MonitorService extends MDSEnabledClientService {
565607

566608
executeMonitor = async (context, req, res) => {
567609
try {
610+
const aclResponse = await this._enforceWorkspaceAcl(context, req, res, ['library_write']);
611+
if (aclResponse) return aclResponse;
568612
const { dryrun = 'true' } = req.query;
569613
const params = {
570614
body: req.body,
@@ -592,6 +636,11 @@ export default class MonitorService extends MDSEnabledClientService {
592636
//TODO: This is temporarily a pass through call which needs to be deprecated
593637
searchMonitors = async (context, req, res) => {
594638
try {
639+
const aclResponse = await this._enforceWorkspaceAcl(context, req, res, [
640+
'library_write',
641+
'library_read',
642+
]);
643+
if (aclResponse) return aclResponse;
595644
const { query: queryBody, index, size, ...rest } = req.body || {};
596645
const body = { ...(queryBody ?? {}), ...rest };
597646
if (size !== undefined) {

0 commit comments

Comments
 (0)