[9.0] [FTR] unify custom role name with Scout (#217882) (#218152)

# Backport

This will backport the following commits from `main` to `9.0`:
- [[FTR] unify custom role name with Scout
(#217882)](https://github.com/elastic/kibana/pull/217882)

<!--- Backport version: 9.6.6 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sorenlouv/backport)

<!--BACKPORT [{"author":{"name":"Dzmitry
Lemechko","email":"dzmitry.lemechko@elastic.co"},"sourceCommit":{"committedDate":"2025-04-14T17:21:49Z","message":"[FTR]
unify custom role name with Scout (#217882)\n\n## Summary\n\nIn QAF
David added a possibility to spin up MKI project with custom role\nset
and ready to use.\n\nOriginally FTR was using reserved name
`'customRole'` for internal\nKibana role to be mapped with native custom
role in the project.\n\nBoth Scout and FTR use `kbn/test` to simulate
SAML authentication, but\nthe new framework will allow to run the tests
in parallel. That said, we\nneed to support multiple custom role
credentials (one pair per worker)\nand for simplicity we decided to use
the same keys:\n\nTo run your tests locally against MKI you need to add
a new Cloud user\nentry in
`user_roles.json`:\n\n```\n\"custom_role_worker_1\": { \"username\":
..., \"password\": ... }, // FTR requires only the first
entry\n\"custom_role_worker_2\": { \"username\": ..., \"password\": ...
},\n...\n```\n\nThe test change is minimal:\n<img width=\"559\"
alt=\"image\"\nsrc=\"https://github.com/user-attachments/assets/572103a3-13b2-4e6c-b9d2-5e55b03ac51c\"\n/>\n\n---------\n\nCo-authored-by:
Cesare de Cal
<cesare.decal@elastic.co>","sha":"c4a97e51e3c9040fb0c955913b06aa0e3b5ba791","branchLabelMapping":{"^v9.1.0$":"main","^v8.19.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","FTR","backport:version","v9.1.0","v8.19.0","v9.0.1"],"title":"[FTR]
unify custom role name with
Scout","number":217882,"url":"https://github.com/elastic/kibana/pull/217882","mergeCommit":{"message":"[FTR]
unify custom role name with Scout (#217882)\n\n## Summary\n\nIn QAF
David added a possibility to spin up MKI project with custom role\nset
and ready to use.\n\nOriginally FTR was using reserved name
`'customRole'` for internal\nKibana role to be mapped with native custom
role in the project.\n\nBoth Scout and FTR use `kbn/test` to simulate
SAML authentication, but\nthe new framework will allow to run the tests
in parallel. That said, we\nneed to support multiple custom role
credentials (one pair per worker)\nand for simplicity we decided to use
the same keys:\n\nTo run your tests locally against MKI you need to add
a new Cloud user\nentry in
`user_roles.json`:\n\n```\n\"custom_role_worker_1\": { \"username\":
..., \"password\": ... }, // FTR requires only the first
entry\n\"custom_role_worker_2\": { \"username\": ..., \"password\": ...
},\n...\n```\n\nThe test change is minimal:\n<img width=\"559\"
alt=\"image\"\nsrc=\"https://github.com/user-attachments/assets/572103a3-13b2-4e6c-b9d2-5e55b03ac51c\"\n/>\n\n---------\n\nCo-authored-by:
Cesare de Cal
<cesare.decal@elastic.co>","sha":"c4a97e51e3c9040fb0c955913b06aa0e3b5ba791"}},"sourceBranch":"main","suggestedTargetBranches":["8.x","9.0"],"targetPullRequestStates":[{"branch":"main","label":"v9.1.0","branchLabelMappingKey":"^v9.1.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/217882","number":217882,"mergeCommit":{"message":"[FTR]
unify custom role name with Scout (#217882)\n\n## Summary\n\nIn QAF
David added a possibility to spin up MKI project with custom role\nset
and ready to use.\n\nOriginally FTR was using reserved name
`'customRole'` for internal\nKibana role to be mapped with native custom
role in the project.\n\nBoth Scout and FTR use `kbn/test` to simulate
SAML authentication, but\nthe new framework will allow to run the tests
in parallel. That said, we\nneed to support multiple custom role
credentials (one pair per worker)\nand for simplicity we decided to use
the same keys:\n\nTo run your tests locally against MKI you need to add
a new Cloud user\nentry in
`user_roles.json`:\n\n```\n\"custom_role_worker_1\": { \"username\":
..., \"password\": ... }, // FTR requires only the first
entry\n\"custom_role_worker_2\": { \"username\": ..., \"password\": ...
},\n...\n```\n\nThe test change is minimal:\n<img width=\"559\"
alt=\"image\"\nsrc=\"https://github.com/user-attachments/assets/572103a3-13b2-4e6c-b9d2-5e55b03ac51c\"\n/>\n\n---------\n\nCo-authored-by:
Cesare de Cal
<cesare.decal@elastic.co>","sha":"c4a97e51e3c9040fb0c955913b06aa0e3b5ba791"}},{"branch":"8.x","label":"v8.19.0","branchLabelMappingKey":"^v8.19.0$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"9.0","label":"v9.0.1","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: Dzmitry Lemechko <dzmitry.lemechko@elastic.co>
This commit is contained in:
Kibana Machine 2025-04-14 21:23:01 +02:00 committed by GitHub
parent 06e5e580eb
commit 086804391a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 101 additions and 22 deletions

View file

@ -12,6 +12,8 @@ import { ServerlessAuthProvider } from './serverless/auth_provider';
import { StatefulAuthProvider } from './stateful/auth_provider';
export interface AuthProvider {
isServerless(): boolean;
getProjectType(): string | undefined;
getSupportedRoleDescriptors(): Map<string, any>;
getDefaultRole(): string;
isCustomRoleEnabled(): boolean;

View file

@ -75,6 +75,9 @@ export function SamlAuthProvider({ getService }: FtrProviderContext) {
const INTERNAL_REQUEST_HEADERS = authRoleProvider.getInternalRequestHeader();
const CUSTOM_ROLE = authRoleProvider.getCustomRole();
const isCustomRoleEnabled = authRoleProvider.isCustomRoleEnabled();
const distroName = authRoleProvider.isServerless()
? `serverless ${authRoleProvider.getProjectType()} project`
: 'stateful deployment';
const getAdminCredentials = async () => {
return await sessionManager.getApiCredentialsForRole('admin');
@ -126,6 +129,12 @@ export function SamlAuthProvider({ getService }: FtrProviderContext) {
return sessionManager.getApiCredentialsForRole(role, options);
},
async getM2MApiCookieCredentialsWithCustomRoleScope(
options?: GetCookieOptions
): Promise<CookieCredentials> {
return this.getM2MApiCookieCredentialsWithRoleScope(CUSTOM_ROLE, options);
},
async getEmail(role: string) {
return sessionManager.getEmail(role);
},
@ -134,26 +143,39 @@ export function SamlAuthProvider({ getService }: FtrProviderContext) {
return sessionManager.getUserData(role);
},
checkRoleIsSupported(role: string): void {
if (role === CUSTOM_ROLE && !isCustomRoleEnabled) {
throw new Error(
`Custom roles are disabled for the current ${distroName}. Please use built-in roles or update the FTR config file to enable custom roles.`
);
}
if (!supportedRoles.includes(role)) {
throw new Error(
`The '${role}' role is not supported for the current ${distroName}. Supported roles are: ${supportedRoles.join(
', '
)}. Default roles are defined in '${authRoleProvider.getRolesDefinitionPath()}'. If you need to use a custom role, use 'samlAuth.CUSTOM_ROLE' instead.`
);
}
},
async createM2mApiKeyWithDefaultRoleScope() {
log.debug(`Creating API key for default role: [${DEFAULT_ROLE}]`);
return this.createM2mApiKeyWithRoleScope(DEFAULT_ROLE);
},
async createM2mApiKeyWithRoleScope(role: string): Promise<RoleCredentials> {
this.checkRoleIsSupported(role);
// Get admin credentials in order to create the API key
const adminCookieHeader = await getAdminCredentials();
let roleDescriptors = {};
if (role !== 'admin') {
if (role === CUSTOM_ROLE && !isCustomRoleEnabled) {
throw new Error(`Custom roles are not supported for the current deployment`);
}
const roleDescriptor = supportedRoleDescriptors.get(role);
if (!roleDescriptor) {
throw new Error(
role === CUSTOM_ROLE
? `Before creating API key for '${CUSTOM_ROLE}', use 'samlAuth.setCustomRole' to set the role privileges`
: `Cannot create API key for non-existent role "${role}"`
: `Cannot create API key for role "${role}", role descriptor is not defined in '${authRoleProvider.getRolesDefinitionPath()}'`
);
}
log.debug(
@ -182,6 +204,10 @@ export function SamlAuthProvider({ getService }: FtrProviderContext) {
return { apiKey, apiKeyHeader };
},
async createM2mApiKeyWithCustomRoleScope() {
return this.createM2mApiKeyWithRoleScope(CUSTOM_ROLE);
},
async invalidateM2mApiKeyWithRoleScope(roleCredentials: RoleCredentials) {
// Get admin credentials in order to invalidate the API key
const adminCookieHeader = await getAdminCredentials();
@ -210,13 +236,22 @@ export function SamlAuthProvider({ getService }: FtrProviderContext) {
elasticsearch: descriptors.elasticsearch ?? [],
};
const { status } = await supertestWithoutAuth
const response = await supertestWithoutAuth
.put(`/api/security/role/${CUSTOM_ROLE}`)
.set(INTERNAL_REQUEST_HEADERS)
.set(adminCookieHeader)
.send(customRoleDescriptors);
expect(status).to.be(204);
if (response.status !== 204) {
const baseErrorMessage = `Failed to update custom role, status code: ${response.status}.`;
const additionalMessage =
response.status === 403
? isCloud
? ` \nEnsure the user listed as 'admin' in '${cloudUsersFilePath}' has the required privileges.`
: ` \nEnsure the 'admin' role has the required privileges in '${authRoleProvider.getRolesDefinitionPath()}'.`
: '';
throw new Error(baseErrorMessage + additionalMessage);
}
// Update descriptors for the custom role, it will be used to create API key
supportedRoleDescriptors.set(CUSTOM_ROLE, customRoleDescriptors);

View file

@ -60,6 +60,14 @@ export class ServerlessAuthProvider implements AuthProvider {
this.rolesDefinitionPath = resolve(SERVERLESS_ROLES_ROOT_PATH, this.projectType, 'roles.yml');
}
isServerless(): boolean {
return true;
}
getProjectType() {
return this.projectType;
}
getSupportedRoleDescriptors() {
const roleDescriptors = new Map<string, any>(
Object.entries(
@ -83,8 +91,9 @@ export class ServerlessAuthProvider implements AuthProvider {
);
}
// For compatibility with the Scout test framework we use the same name for the custom role
getCustomRole() {
return 'customRole';
return 'custom_role_worker_1';
}
getRolesDefinitionPath(): string {

View file

@ -19,6 +19,14 @@ import {
export class StatefulAuthProvider implements AuthProvider {
private readonly rolesDefinitionPath = resolve(REPO_ROOT, STATEFUL_ROLES_ROOT_PATH, 'roles.yml');
isServerless() {
return false;
}
getProjectType() {
return undefined;
}
getSupportedRoleDescriptors() {
const roleDescriptors = new Map<string, any>(
Object.entries(
@ -39,8 +47,9 @@ export class StatefulAuthProvider implements AuthProvider {
return true;
}
// For compatibility with the Scout test framework we use the same name for the custom role
getCustomRole() {
return 'customRole';
return 'custom_role_worker_1';
}
getRolesDefinitionPath() {

View file

@ -131,9 +131,18 @@ export const coreWorkerFixtures = base.extend<
*/
samlAuth: [
({ log, config, esClient, kbnClient }, use, workerInfo) => {
let customRoleHash = '';
const customRoleName = `custom_role_worker_${workerInfo.parallelIndex}`;
/**
* When running tests against Cloud, ensure the `.ftr/role_users.json` file is populated with the required roles
* and credentials. Each worker uses a unique custom role named `custom_role_worker_<index>`.
* If running tests in parallel, make sure the file contains enough entries to accommodate all workers.
* The file should be structured as follows:
* {
* "custom_role_worker_1": { "username": ..., "password": ... },
* "custom_role_worker_2": { "username": ..., "password": ... },
*/
const customRoleName = `custom_role_worker_${workerInfo.parallelIndex + 1}`;
const session = createSamlSessionManager(config, log, customRoleName);
let customRoleHash = '';
const isCustomRoleSet = (roleHash: string) => roleHash === customRoleHash;

View file

@ -86,7 +86,7 @@ export function SecuritySolutionServerlessUtils({
throw new Error(`Could not find a role definition for ${userRoleName}`);
}
await svlUserManager.setCustomRole(roleDefinition.privileges);
const roleAuthc = await svlUserManager.createM2mApiKeyWithRoleScope('customRole');
const roleAuthc = await svlUserManager.createM2mApiKeyWithCustomRoleScope();
const superTest = supertest
.agent(kbnUrl)
.set(svlCommonApi.getInternalRequestHeader())

View file

@ -42,14 +42,14 @@ export function RoleScopedSupertestProvider({ getService }: DeploymentAgnosticFt
if (options.useCookieHeader) {
const cookieHeader = await samlAuth.getM2MApiCookieCredentialsWithRoleScope(
isBuiltIn ? user.role : 'customRole'
isBuiltIn ? user.role : samlAuth.CUSTOM_ROLE
);
return new SupertestWithRoleScope(cookieHeader, supertestWithoutAuth, samlAuth, options);
}
// HTTP requests will be called with API key in header by default
const roleAuthc = await samlAuth.createM2mApiKeyWithRoleScope(
isBuiltIn ? user.role : 'customRole'
isBuiltIn ? user.role : samlAuth.CUSTOM_ROLE
);
return new SupertestWithRoleScope(roleAuthc, supertestWithoutAuth, samlAuth, options);
},

View file

@ -209,9 +209,23 @@ defining and authenticating with custom roles in both UI functional tests and AP
To test role management within the Observability project, you can execute the tests using the existing [config.feature_flags.ts](x-pack/test_serverless/functional/test_suites/observability/config.feature_flags.ts), where this functionality is explicitly enabled. Though the config is not run on MKI, it provides the ability to test custom roles in Kibana CI before the functionality is enabled in MKI. When roles management is enabled on MKI, these tests can be migrated to the regular FTR config and will be run on MKI.
For compatibility with MKI, the role name `customRole` is reserved for use in tests. The test user is automatically assigned to this role, but before logging in via the browser, generating a cookie header, or creating an API key in each test suite, the roles privileges must be updated.
When running tests locally against MKI, ensure that the `.ftr/role_users.json` file includes the reserved role name `custom_role_worker_1` along with its credentials. This role name has been updated for compatibility with Scout, which supports parallel test execution and allows multiple credential pairs to be passed.
Note: We are still working on a solution to run these tests against MKI. In the meantime, please tag the suite with `skipMKI`.
```json
{
"viewer": {
"email": ...,
"password": ..."
},
...
"custom_role_worker_1": {
"email": ...,
"password": ...
}
}
```
When using QAF to create a project with a custom native role, ensure that the role name `custom_role_worker_1` is configured as a Kibana role. While the test user is automatically assigned to the custom role, you must update the role's privileges before performing actions such as logging in via the browser, generating a cookie header, or creating an API key within each test suite.
FTR UI test example:
```
@ -254,7 +268,7 @@ await samlAuth.setCustomRole({
});
// Then, generate an API key with the newly defined privileges
const roleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('customRole');
const roleAuthc = await samlAuth.createM2mApiKeyWithCustomRoleScope();
// Remember to invalidate the API key after use and delete the custom role
await samlAuth.invalidateM2mApiKeyWithRoleScope(roleAuthc);

View file

@ -53,7 +53,7 @@ export default function ({ getService }: FtrProviderContext) {
},
],
});
roleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('customRole');
roleAuthc = await samlAuth.createM2mApiKeyWithCustomRoleScope();
const res = await supertestWithoutAuth
.get(DATA_USAGE_DATA_STREAMS_API_ROUTE)
.query({ includeZeroStorage: true })
@ -83,7 +83,7 @@ export default function ({ getService }: FtrProviderContext) {
},
],
});
roleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('customRole');
roleAuthc = await samlAuth.createM2mApiKeyWithCustomRoleScope();
const res = await supertestWithoutAuth
.get(DATA_USAGE_DATA_STREAMS_API_ROUTE)
.query({ includeZeroStorage: true })
@ -115,7 +115,7 @@ export default function ({ getService }: FtrProviderContext) {
},
],
});
roleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('customRole');
roleAuthc = await samlAuth.createM2mApiKeyWithCustomRoleScope();
const res = await supertestWithoutAuth
.get(DATA_USAGE_DATA_STREAMS_API_ROUTE)
.query({ includeZeroStorage: true })
@ -140,7 +140,7 @@ export default function ({ getService }: FtrProviderContext) {
},
],
});
roleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('customRole');
roleAuthc = await samlAuth.createM2mApiKeyWithCustomRoleScope();
const res = await supertestWithoutAuth
.get(DATA_USAGE_DATA_STREAMS_API_ROUTE)
.query({ includeZeroStorage: true })

View file

@ -68,6 +68,7 @@ export function SvlCommonPageProvider({ getService, getPageObjects }: FtrProvide
* Login to Kibana using SAML authentication with provided project-specfic role
*/
async loginWithRole(role: string) {
svlUserManager.checkRoleIsSupported(role);
log.debug(`Fetch the cookie for '${role}' role`);
const sidCookie = await svlUserManager.getInteractiveUserSessionCookieWithRoleScope(role);
await retry.waitForWithTimeout(

View file

@ -87,7 +87,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
});
it('should access console with API key', async () => {
roleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('customRole');
roleAuthc = await samlAuth.createM2mApiKeyWithCustomRoleScope();
const { body } = await supertestWithoutAuth
.get('/api/console/api_server')
.set(roleAuthc.apiKeyHeader)

View file

@ -79,7 +79,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
});
it('should access console with API key', async () => {
roleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('customRole');
roleAuthc = await samlAuth.createM2mApiKeyWithCustomRoleScope();
const { body } = await supertestWithoutAuth
.get('/api/console/api_server')
.set(roleAuthc.apiKeyHeader)