Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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);
},
Expand All @@ -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(
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
},
Expand Down
20 changes: 17 additions & 3 deletions x-pack/test_serverless/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 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:
```
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
Expand Down Expand Up @@ -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 })
Expand Down Expand Up @@ -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 })
Expand All @@ -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 })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down