Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
4e79028
[ftr] login with custom native role
dmlemeshko Oct 2, 2024
0195b9f
Merge remote-tracking branch 'upstream/main' into ftr/support-custom-…
dmlemeshko Oct 2, 2024
7adbe78
fix typo
dmlemeshko Oct 2, 2024
150f80d
fix typo, export interface
dmlemeshko Oct 2, 2024
fc86e90
fix typo
dmlemeshko Oct 2, 2024
30a8002
fix API key creation for custom role
dmlemeshko Oct 2, 2024
8e78de2
add comments
dmlemeshko Oct 2, 2024
53c60c6
Merge branch 'main' into ftr/support-custom-native-roles
dmlemeshko Oct 2, 2024
45369f5
Merge branch 'ftr/support-custom-native-roles' of github.com:dmlemesh…
dmlemeshko Oct 2, 2024
c383b2a
update security/authorization tests
dmlemeshko Oct 2, 2024
a22be26
Merge remote-tracking branch 'upstream/main' into ftr/support-custom-…
dmlemeshko Oct 4, 2024
f7b9752
skip tests
dmlemeshko Oct 4, 2024
2989f2a
Merge branch 'main' into ftr/support-custom-native-roles
dmlemeshko Oct 4, 2024
913942e
Merge branch 'ftr/support-custom-native-roles' of github.com:dmlemesh…
dmlemeshko Oct 4, 2024
d951b7c
update condition
dmlemeshko Oct 4, 2024
a0b1a79
update docs
dmlemeshko Oct 4, 2024
d393633
add check if custom role is supported
dmlemeshko Oct 7, 2024
b54710c
Merge remote-tracking branch 'upstream/main' into ftr/support-custom-…
dmlemeshko Oct 7, 2024
d4afd7b
post merge fix
dmlemeshko Oct 7, 2024
9e3f754
Update custom_role_access.ts
dmlemeshko Oct 7, 2024
d9347c8
Merge branch 'main' into ftr/support-custom-native-roles
dmlemeshko Oct 8, 2024
472d30f
Merge branch 'main' into ftr/support-custom-native-roles
dmlemeshko Oct 8, 2024
a8243e9
Merge branch 'main' into ftr/support-custom-native-roles
dmlemeshko Oct 9, 2024
403ea84
Merge branch 'main' into ftr/support-custom-native-roles
dmlemeshko Oct 9, 2024
c7cef60
move login to before hook
dmlemeshko Oct 11, 2024
7b57978
Merge branch 'main' into ftr/support-custom-native-roles
dmlemeshko Oct 11, 2024
898c2f3
Merge branch 'main' into ftr/support-custom-native-roles
dmlemeshko Oct 11, 2024
c6c4b32
Merge branch 'main' into ftr/support-custom-native-roles
dmlemeshko Oct 11, 2024
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
1 change: 1 addition & 0 deletions packages/kbn-ftr-common-functional-services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export type {
InternalRequestHeader,
RoleCredentials,
CookieCredentials,
KibanaRoleDescriptors,
} from './services/saml_auth';

import { SamlAuthProvider } from './services/saml_auth/saml_auth_provider';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ import { ServerlessAuthProvider } from './serverless/auth_provider';
import { StatefulAuthProvider } from './stateful/auth_provider';

export interface AuthProvider {
getSupportedRoleDescriptors(): Record<string, unknown>;
getSupportedRoleDescriptors(): Map<string, any>;
getDefaultRole(): string;
isCustomRoleEnabled(): boolean;
getCustomRole(): string;
getRolesDefinitionPath(): string;
getCommonRequestHeader(): { [key: string]: string };
getInternalRequestHeader(): { [key: string]: string };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,9 @@
*/

export { SamlAuthProvider } from './saml_auth_provider';
export type { RoleCredentials, CookieCredentials } from './saml_auth_provider';
export type {
RoleCredentials,
CookieCredentials,
KibanaRoleDescriptors,
} from './saml_auth_provider';
export type { InternalRequestHeader } from './default_request_headers';
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,19 @@ export interface CookieCredentials {
[header: string]: string;
}

export interface KibanaRoleDescriptors {
kibana: any;
elasticsearch?: any;
}

const throwIfRoleNotSet = (role: string, customRole: string, roleDescriptors: Map<string, any>) => {
if (role === customRole && !roleDescriptors.has(customRole)) {
throw new Error(
`Set privileges for '${customRole}' using 'samlAuth.setCustomRole' before authentication.`
);
}
};

export function SamlAuthProvider({ getService }: FtrProviderContext) {
const config = getService('config');
const log = getService('log');
Expand All @@ -35,9 +48,8 @@ export function SamlAuthProvider({ getService }: FtrProviderContext) {

const authRoleProvider = getAuthProvider({ config });
const supportedRoleDescriptors = authRoleProvider.getSupportedRoleDescriptors();
const supportedRoles = Object.keys(supportedRoleDescriptors);

const customRolesFileName: string | undefined = process.env.ROLES_FILENAME_OVERRIDE;
const supportedRoles = Array.from(supportedRoleDescriptors.keys());
const customRolesFileName = process.env.ROLES_FILENAME_OVERRIDE;
const cloudUsersFilePath = resolve(REPO_ROOT, '.ftr', customRolesFileName ?? 'role_users.json');

// Sharing the instance within FTR config run means cookies are persistent for each role between tests.
Expand All @@ -61,55 +73,78 @@ export function SamlAuthProvider({ getService }: FtrProviderContext) {
const DEFAULT_ROLE = authRoleProvider.getDefaultRole();
const COMMON_REQUEST_HEADERS = authRoleProvider.getCommonRequestHeader();
const INTERNAL_REQUEST_HEADERS = authRoleProvider.getInternalRequestHeader();
const CUSTOM_ROLE = authRoleProvider.getCustomRole();
const isCustomRoleEnabled = authRoleProvider.isCustomRoleEnabled();

const getAdminCredentials = async () => {
return await sessionManager.getApiCredentialsForRole('admin');
};

const createApiKeyPayload = (role: string, roleDescriptors: any) => {
return {
name: `myTestApiKey_${role}`,
metadata: {},
...(role === CUSTOM_ROLE
? { kibana_role_descriptors: roleDescriptors }
: { role_descriptors: roleDescriptors }),
Comment on lines +87 to +89
Copy link
Copy Markdown
Contributor Author

@dmlemeshko dmlemeshko Oct 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was getting 400 status code while trying to send payload with role_descriptors for custom role. I searched in Kibana repo for some code examples and found schema validation for kibana_role_description https://github.com/elastic/kibana/blob/main/x-pack/packages/security/plugin_types_server/src/authentication/api_keys/api_keys.ts#L29-L36

Would be good if @jeramysoucy or @azasypkin can confirm I'm doing it correctly

};
};

return {
async getInteractiveUserSessionCookieWithRoleScope(role: string) {
// Custom role has no descriptors by default, check if it was added before authentication
throwIfRoleNotSet(role, CUSTOM_ROLE, supportedRoleDescriptors);
return sessionManager.getInteractiveUserSessionCookieWithRoleScope(role);
},

async getM2MApiCookieCredentialsWithRoleScope(role: string): Promise<CookieCredentials> {
// Custom role has no descriptors by default, check if it was added before authentication
throwIfRoleNotSet(role, CUSTOM_ROLE, supportedRoleDescriptors);
return sessionManager.getApiCredentialsForRole(role);
},

async getEmail(role: string) {
return sessionManager.getEmail(role);
},

async getUserData(role: string) {
return sessionManager.getUserData(role);
},

async createM2mApiKeyWithDefaultRoleScope() {
log.debug(`Creating api key for default role: [${this.DEFAULT_ROLE}]`);
return this.createM2mApiKeyWithRoleScope(this.DEFAULT_ROLE);
log.debug(`Creating API key for default role: [${DEFAULT_ROLE}]`);
return this.createM2mApiKeyWithRoleScope(DEFAULT_ROLE);
},

async createM2mApiKeyWithRoleScope(role: string): Promise<RoleCredentials> {
// Get admin credentials in order to create the API key
const adminCookieHeader = await this.getM2MApiCookieCredentialsWithRoleScope('admin');

// Get the role descrtiptor for the role
const adminCookieHeader = await getAdminCredentials();
let roleDescriptors = {};

if (role !== 'admin') {
const roleDescriptor = supportedRoleDescriptors[role];
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(`Cannot create API key for non-existent role "${role}"`);
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}"`
);
}
log.debug(
`Creating api key for ${role} role with the following privileges ${JSON.stringify(
roleDescriptor
)}`
`Creating API key for ${role} with privileges: ${JSON.stringify(roleDescriptor)}`
);
roleDescriptors = {
[role]: roleDescriptor,
};
roleDescriptors = { [role]: roleDescriptor };
}

const payload = createApiKeyPayload(role, roleDescriptors);
const response = await supertestWithoutAuth
.post('/internal/security/api_key')
.set(INTERNAL_REQUEST_HEADERS)
.set(adminCookieHeader)
.send({
name: 'myTestApiKey',
metadata: {},
role_descriptors: roleDescriptors,
});
.send(payload);

if (response.status !== 200) {
throw new Error(
Expand All @@ -120,38 +155,59 @@ export function SamlAuthProvider({ getService }: FtrProviderContext) {
const apiKey = response.body;
const apiKeyHeader = { Authorization: 'ApiKey ' + apiKey.encoded };

log.debug(`Created api key for role: [${role}]`);
log.debug(`Created API key for role: [${role}]`);
return { apiKey, apiKeyHeader };
},

async invalidateM2mApiKeyWithRoleScope(roleCredentials: RoleCredentials) {
// Get admin credentials in order to invalidate the API key
const adminCookieHeader = await this.getM2MApiCookieCredentialsWithRoleScope('admin');

const requestBody = {
apiKeys: [
{
id: roleCredentials.apiKey.id,
name: roleCredentials.apiKey.name,
},
],
isAdmin: true,
};
const adminCookieHeader = await getAdminCredentials();

const { status } = await supertestWithoutAuth
.post('/internal/security/api_key/invalidate')
.set(INTERNAL_REQUEST_HEADERS)
.set(adminCookieHeader)
.send(requestBody);
.send({
apiKeys: [{ id: roleCredentials.apiKey.id, name: roleCredentials.apiKey.name }],
isAdmin: true,
});

expect(status).to.be(200);
},

async setCustomRole(descriptors: KibanaRoleDescriptors) {
if (!isCustomRoleEnabled) {
throw new Error(`Custom roles are not supported for the current deployment`);
}
log.debug(`Updating role ${CUSTOM_ROLE}`);
const adminCookieHeader = await getAdminCredentials();

const customRoleDescriptors = {
kibana: descriptors.kibana,
elasticsearch: descriptors.elasticsearch ?? [],
};

const { status } = await supertestWithoutAuth
.put(`/api/security/role/${CUSTOM_ROLE}`)
.set(INTERNAL_REQUEST_HEADERS)
.set(adminCookieHeader)
.send(customRoleDescriptors);

expect(status).to.be(204);

// Update descriptors for custome role, it will be used to create API key
supportedRoleDescriptors.set(CUSTOM_ROLE, customRoleDescriptors);
},

getCommonRequestHeader() {
return COMMON_REQUEST_HEADERS;
},

getInternalRequestHeader(): InternalRequestHeader {
return INTERNAL_REQUEST_HEADERS;
},

DEFAULT_ROLE,
CUSTOM_ROLE,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ const projectDefaultRoles = new Map<string, Role>([
['oblt', 'editor'],
]);

const projectTypesWithCustomRolesEnabled = ['es', 'security'];

const getDefaultServerlessRole = (projectType: string) => {
if (projectDefaultRoles.has(projectType)) {
return projectDefaultRoles.get(projectType)!;
Expand All @@ -50,18 +52,39 @@ export class ServerlessAuthProvider implements AuthProvider {
this.rolesDefinitionPath = resolve(SERVERLESS_ROLES_ROOT_PATH, this.projectType, 'roles.yml');
}

getSupportedRoleDescriptors(): Record<string, unknown> {
return readRolesDescriptorsFromResource(this.rolesDefinitionPath) as Record<string, unknown>;
getSupportedRoleDescriptors() {
const roleDescriptors = new Map<string, any>(
Object.entries(
readRolesDescriptorsFromResource(this.rolesDefinitionPath) as Record<string, unknown>
)
);
// Adding custom role to the map without privileges, so it can be later updated and used in the tests
if (this.isCustomRoleEnabled()) {
roleDescriptors.set(this.getCustomRole(), null);
}
return roleDescriptors;
}

getDefaultRole(): string {
return getDefaultServerlessRole(this.projectType);
}

isCustomRoleEnabled() {
return projectTypesWithCustomRolesEnabled.includes(this.projectType);
}

getCustomRole() {
return 'customRole';
}

getRolesDefinitionPath(): string {
return this.rolesDefinitionPath;
}

getCommonRequestHeader() {
return COMMON_REQUEST_HEADERS;
}

getInternalRequestHeader() {
return getServerlessInternalRequestHeaders();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,31 @@ import {

export class StatefulAuthProvider implements AuthProvider {
private readonly rolesDefinitionPath = resolve(REPO_ROOT, STATEFUL_ROLES_ROOT_PATH, 'roles.yml');
getSupportedRoleDescriptors(): Record<string, unknown> {
return readRolesDescriptorsFromResource(this.rolesDefinitionPath) as Record<string, unknown>;

getSupportedRoleDescriptors() {
const roleDescriptors = new Map<string, any>(
Object.entries(
readRolesDescriptorsFromResource(this.rolesDefinitionPath) as Record<string, unknown>
)
);
// no privileges set by default
roleDescriptors.set(this.getCustomRole(), null);

return roleDescriptors;
}

getDefaultRole() {
return 'editor';
}

isCustomRoleEnabled() {
return true;
}

getCustomRole() {
return 'customRole';
}

getRolesDefinitionPath() {
return this.rolesDefinitionPath;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ export function createServerlessTestConfig<T extends DeploymentAgnosticCommonSer
...svlSharedConfig.get('esTestCluster'),
serverArgs: [
...svlSharedConfig.get('esTestCluster.serverArgs'),
// custom native roles are enabled only for search and security projects
...(options.serverlessProject !== 'oblt'
? ['xpack.security.authc.native_roles.enabled=true']
: []),
...esServerArgsFromController[options.serverlessProject],
],
},
Expand All @@ -109,6 +113,10 @@ export function createServerlessTestConfig<T extends DeploymentAgnosticCommonSer
...svlSharedConfig.get('kbnTestServer.serverArgs'),
...kbnServerArgsFromController[options.serverlessProject],
`--serverless=${options.serverlessProject}`,
// custom native roles are enabled only for search and security projects
...(options.serverlessProject !== 'oblt'
? ['--xpack.security.roleManagementEnabled=true']
: []),
],
},
testFiles: options.testFiles,
Expand Down
53 changes: 53 additions & 0 deletions x-pack/test_serverless/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,59 @@ describe("my internal APIs test suite", async function() {
});
```

#### Testing with custom roles

With custom native roles now enabled for the Security and Search projects on MKI, the FTR supports
defining and authenticating with custom roles in both UI functional tests and API integration tests.

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.

Note: We are still working on a solution to run these tests against MKI. In the meantime, please tag the suite with `skipMKI`.

FTR UI test example:
```
// First, set privileges for the custom role
await samlAuth.setCustomRole({
elasticsearch: {
indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }],
},
kibana: [
{
feature: {
discover: ['read'],
},
spaces: ['*'],
},
],
});
// Then, log in via the browser as a user with the newly defined privileges
await pageObjects.svlCommonPage.loginWithCustomRole();
```

FTR api_integration test example:
```
// First, set privileges for the custom role
await samlAuth.setCustomRole({
elasticsearch: {
indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }],
},
kibana: [
{
feature: {
discover: ['read'],
},
spaces: ['*'],
},
],
});

// Then, generate an API key with the newly defined privileges
const roleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('customRole');

// Remember to invalidate the API key after use
await samlAuth.invalidateM2mApiKeyWithRoleScope(roleAuthc);
```

### Testing with feature flags

**tl;dr:** Tests specific to functionality behind a feature flag need special
Expand Down
Loading