[ResponseOps][Alerting] Fix stackAlerts plugin missing rac API auth scope (#193948)

## Summary

Adds the `['rac']` API access scope to the Stack Alerts feature to
correctly authenticate alerts API endpoints with the `stackAlerts`
permission.
Also adds a dedicated API integration test for the impacted endpoint and
permission set.

## Release note

Fix Stack Alerts feature API access control

## To verify

1. Create rules that fire alerts in Stack management
2. Wait for alerts to be created
3. Create a role with only `Stack Management > Rules : Read` privilege
4. Create a user with that role
5. In another window, open Kibana with the newly created user
6. Check that the Stack Management > Alerts page renders correctly, not
showing any missing 403 error toasts
This commit is contained in:
Umberto Pepato 2024-10-07 17:17:31 +02:00 committed by GitHub
parent 71c8d6fddc
commit 17fcaa5c8e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 66 additions and 26 deletions

View file

@ -73,7 +73,7 @@ export const BUILT_IN_ALERTS_FEATURE: KibanaFeatureConfig = {
all: [],
read: [],
},
api: [],
api: ['rac'],
ui: [],
},
read: {
@ -108,7 +108,7 @@ export const BUILT_IN_ALERTS_FEATURE: KibanaFeatureConfig = {
all: [],
read: [],
},
api: [],
api: ['rac'],
ui: [],
},
},

View file

@ -126,6 +126,14 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
spaces: ['*'],
},
],
elasticsearch: {
indices: [
{
names: ['.alerts-*'],
privileges: ['read'],
},
],
},
},
only_actions_role: {
kibana: [

View file

@ -265,6 +265,23 @@ export const logsOnlyAllSpacesAll: Role = {
},
};
export const stackAlertsOnlyReadSpacesAll: Role = {
name: 'stack_alerts_only_read_spaces_all',
privileges: {
elasticsearch: {
indices: [],
},
kibana: [
{
feature: {
stackAlerts: ['read'],
},
spaces: ['*'],
},
],
},
};
export const stackAlertsOnlyAllSpacesAll: Role = {
name: 'stack_alerts_only_all_spaces_all',
privileges: {
@ -511,6 +528,7 @@ export const allRoles = [
securitySolutionOnlyReadSpacesAll,
observabilityOnlyAllSpacesAll,
logsOnlyAllSpacesAll,
stackAlertsOnlyReadSpacesAll,
stackAlertsOnlyAllSpacesAll,
observabilityOnlyReadSpacesAll,
observabilityOnlyAllSpacesAllWithReadESIndices,

View file

@ -30,7 +30,8 @@ import {
observabilityMinReadAlertsAllSpacesAll,
observabilityOnlyAllSpacesAllWithReadESIndices,
securitySolutionOnlyAllSpacesAllWithReadESIndices,
stackAlertsOnlyAllSpacesAll,
stackAlertsOnlyReadSpacesAll as stackAlertsOnlyReadSpacesAllRole,
stackAlertsOnlyAllSpacesAll as stackAlertsOnlyAllSpacesAllRole,
} from './roles';
import { User } from './types';
@ -130,6 +131,12 @@ export const obsOnlyReadSpacesAll: User = {
roles: [observabilityOnlyReadSpacesAll.name],
};
export const stackAlertsOnlyReadSpacesAll: User = {
username: 'stack_alerts_only_read_spaces_all',
password: 'stack_alerts_only_read_spaces_all',
roles: [stackAlertsOnlyReadSpacesAllRole.name],
};
export const users = [
superUser,
secOnly,
@ -177,10 +184,10 @@ export const logsOnlySpacesAll: User = {
roles: [logsOnlyAllSpacesAll.name],
};
export const stackAlertsOnlySpacesAll: User = {
export const stackAlertsOnlyAllSpacesAll: User = {
username: 'stack_alerts_only_all_spaces_all',
password: 'stack_alerts_only_all_spaces_all',
roles: [stackAlertsOnlyAllSpacesAll.name],
roles: [stackAlertsOnlyAllSpacesAllRole.name],
};
export const obsOnlySpacesAllEsRead: User = {
@ -297,7 +304,8 @@ export const allUsers = [
secOnlyReadSpacesAll,
obsOnlySpacesAll,
logsOnlySpacesAll,
stackAlertsOnlySpacesAll,
stackAlertsOnlyReadSpacesAll,
stackAlertsOnlyAllSpacesAll,
obsSecSpacesAll,
obsSecReadSpacesAll,
obsMinReadAlertsRead,

View file

@ -7,7 +7,12 @@
import expect from '@kbn/expect';
import { superUser, obsOnlySpacesAll, secOnlyRead } from '../../../common/lib/authentication/users';
import {
superUser,
obsOnlySpacesAll,
secOnlyRead,
stackAlertsOnlyReadSpacesAll,
} from '../../../common/lib/authentication/users';
import type { User } from '../../../common/lib/authentication/types';
import { FtrProviderContext } from '../../../common/ftr_provider_context';
import { getSpaceUrlPrefix } from '../../../common/lib/authentication/spaces';
@ -22,27 +27,19 @@ export default ({ getService }: FtrProviderContext) => {
const SPACE1 = 'space1';
const APM_ALERT_INDEX = '.alerts-observability.apm.alerts-default';
const SECURITY_SOLUTION_ALERT_INDEX = '.alerts-security.alerts';
const STACK_ALERT_INDEX = '.alerts-stack.alerts-default';
const getAPMIndexName = async (user: User, space: string, expectedStatusCode: number = 200) => {
const resp = await supertestWithoutAuth
.get(`${getSpaceUrlPrefix(space)}${ALERTS_INDEX_URL}?features=apm`)
.auth(user.username, user.password)
.set('kbn-xsrf', 'true')
.expect(expectedStatusCode);
return resp.body.index_name as string[];
};
const getSecuritySolutionIndexName = async (
const getIndexName = async (
featureIds: string[],
user: User,
space: string,
expectedStatusCode: number = 200
) => {
const resp = await supertestWithoutAuth
.get(`${getSpaceUrlPrefix(space)}${ALERTS_INDEX_URL}?features=siem`)
.get(`${getSpaceUrlPrefix(space)}${ALERTS_INDEX_URL}?features=${featureIds.join(',')}`)
.auth(user.username, user.password)
.set('kbn-xsrf', 'true')
.expect(expectedStatusCode);
return resp.body.index_name as string[];
};
@ -52,24 +49,33 @@ export default ({ getService }: FtrProviderContext) => {
});
describe('Users:', () => {
it(`${obsOnlySpacesAll.username} should be able to access the APM alert in ${SPACE1}`, async () => {
const indexNames = await getAPMIndexName(obsOnlySpacesAll, SPACE1);
const indexNames = await getIndexName(['apm'], obsOnlySpacesAll, SPACE1);
expect(indexNames.includes(APM_ALERT_INDEX)).to.eql(true); // assert this here so we can use constants in the dynamically-defined test cases below
});
it(`${superUser.username} should be able to access the APM alert in ${SPACE1}`, async () => {
const indexNames = await getAPMIndexName(superUser, SPACE1);
const indexNames = await getIndexName(['apm'], superUser, SPACE1);
expect(indexNames.includes(APM_ALERT_INDEX)).to.eql(true); // assert this here so we can use constants in the dynamically-defined test cases below
});
it(`${secOnlyRead.username} should NOT be able to access the APM alert in ${SPACE1}`, async () => {
const indexNames = await getAPMIndexName(secOnlyRead, SPACE1);
const indexNames = await getIndexName(['apm'], secOnlyRead, SPACE1);
expect(indexNames?.length).to.eql(0);
});
it(`${secOnlyRead.username} should be able to access the security solution alert in ${SPACE1}`, async () => {
const indexNames = await getSecuritySolutionIndexName(secOnlyRead, SPACE1);
const indexNames = await getIndexName(['siem'], secOnlyRead, SPACE1);
expect(indexNames.includes(`${SECURITY_SOLUTION_ALERT_INDEX}-${SPACE1}`)).to.eql(true); // assert this here so we can use constants in the dynamically-defined test cases below
});
it(`${stackAlertsOnlyReadSpacesAll.username} should be able to access the stack alert in ${SPACE1}`, async () => {
const indexNames = await getIndexName(
['stackAlerts'],
stackAlertsOnlyReadSpacesAll,
SPACE1
);
expect(indexNames.includes(STACK_ALERT_INDEX)).to.eql(true);
});
});
});
};

View file

@ -13,7 +13,7 @@ import {
obsOnlySpacesAll,
logsOnlySpacesAll,
secOnlySpacesAllEsReadAll,
stackAlertsOnlySpacesAll,
stackAlertsOnlyAllSpacesAll,
superUser,
} from '../../../common/lib/authentication/users';
@ -360,8 +360,8 @@ export default ({ getService }: FtrProviderContext) => {
const result = await secureBsearch.send<RuleRegistrySearchResponse>({
supertestWithoutAuth,
auth: {
username: stackAlertsOnlySpacesAll.username,
password: stackAlertsOnlySpacesAll.password,
username: stackAlertsOnlyAllSpacesAll.username,
password: stackAlertsOnlyAllSpacesAll.password,
},
referer: 'test',
kibanaVersion,