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

## Summary

In QAF David added a possibility to spin up MKI project with custom role
set and ready to use.

Originally FTR was using reserved name `'customRole'` for internal
Kibana role to be mapped with native custom role in the project.

Both Scout and FTR use `kbn/test` to simulate SAML authentication, but
the new framework will allow to run the tests in parallel. That said, we
need to support multiple custom role credentials (one pair per worker)
and for simplicity we decided to use the same keys:

To run your tests locally against MKI you need to add a new Cloud user
entry in `user_roles.json`:

```
"custom_role_worker_1": { "username": ..., "password": ... }, // FTR requires only the first entry
"custom_role_worker_2": { "username": ..., "password": ... },
...
```

The test change is minimal:
<img width="559" alt="image"
src="https://github.com/user-attachments/assets/572103a3-13b2-4e6c-b9d2-5e55b03ac51c"
/>

---------

Co-authored-by: Cesare de Cal <cesare.decal@elastic.co>
This commit is contained in:
Dzmitry Lemechko 2025-04-14 19:21:49 +02:00 committed by GitHub
parent e25abef4e4
commit c4a97e51e3
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

@ -61,6 +61,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(
@ -84,8 +92,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

@ -212,9 +212,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:
```
@ -257,7 +271,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

@ -95,7 +95,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

@ -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)