mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[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:
parent
e25abef4e4
commit
c4a97e51e3
12 changed files with 101 additions and 22 deletions
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
|
|
|
@ -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 role’s 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);
|
||||
|
|
|
@ -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 })
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue