diff --git a/packages/kbn-ftr-common-functional-services/index.ts b/packages/kbn-ftr-common-functional-services/index.ts index ac5b33edce1a1..21c9fcbdd0807 100644 --- a/packages/kbn-ftr-common-functional-services/index.ts +++ b/packages/kbn-ftr-common-functional-services/index.ts @@ -30,6 +30,7 @@ export type { InternalRequestHeader, RoleCredentials, CookieCredentials, + KibanaRoleDescriptors, } from './services/saml_auth'; import { SamlAuthProvider } from './services/saml_auth/saml_auth_provider'; diff --git a/packages/kbn-ftr-common-functional-services/services/saml_auth/get_auth_provider.ts b/packages/kbn-ftr-common-functional-services/services/saml_auth/get_auth_provider.ts index f924e5b40d72a..eb7ababe7d2c3 100644 --- a/packages/kbn-ftr-common-functional-services/services/saml_auth/get_auth_provider.ts +++ b/packages/kbn-ftr-common-functional-services/services/saml_auth/get_auth_provider.ts @@ -12,8 +12,10 @@ import { ServerlessAuthProvider } from './serverless/auth_provider'; import { StatefulAuthProvider } from './stateful/auth_provider'; export interface AuthProvider { - getSupportedRoleDescriptors(): Record; + getSupportedRoleDescriptors(): Map; getDefaultRole(): string; + isCustomRoleEnabled(): boolean; + getCustomRole(): string; getRolesDefinitionPath(): string; getCommonRequestHeader(): { [key: string]: string }; getInternalRequestHeader(): { [key: string]: string }; diff --git a/packages/kbn-ftr-common-functional-services/services/saml_auth/index.ts b/packages/kbn-ftr-common-functional-services/services/saml_auth/index.ts index f379a3dc761ed..6caf70183998e 100644 --- a/packages/kbn-ftr-common-functional-services/services/saml_auth/index.ts +++ b/packages/kbn-ftr-common-functional-services/services/saml_auth/index.ts @@ -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'; diff --git a/packages/kbn-ftr-common-functional-services/services/saml_auth/saml_auth_provider.ts b/packages/kbn-ftr-common-functional-services/services/saml_auth/saml_auth_provider.ts index 1ee239ac5448e..5723dca7b339b 100644 --- a/packages/kbn-ftr-common-functional-services/services/saml_auth/saml_auth_provider.ts +++ b/packages/kbn-ftr-common-functional-services/services/saml_auth/saml_auth_provider.ts @@ -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) => { + 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'); @@ -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. @@ -61,14 +73,36 @@ 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 }), + }; + }; 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 { + // 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); }, @@ -76,40 +110,41 @@ export function SamlAuthProvider({ getService }: FtrProviderContext) { 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 { // 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( @@ -120,31 +155,50 @@ 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; }, @@ -152,6 +206,8 @@ export function SamlAuthProvider({ getService }: FtrProviderContext) { getInternalRequestHeader(): InternalRequestHeader { return INTERNAL_REQUEST_HEADERS; }, + DEFAULT_ROLE, + CUSTOM_ROLE, }; } diff --git a/packages/kbn-ftr-common-functional-services/services/saml_auth/serverless/auth_provider.ts b/packages/kbn-ftr-common-functional-services/services/saml_auth/serverless/auth_provider.ts index 148d14464bd74..25038a3cfa17b 100644 --- a/packages/kbn-ftr-common-functional-services/services/saml_auth/serverless/auth_provider.ts +++ b/packages/kbn-ftr-common-functional-services/services/saml_auth/serverless/auth_provider.ts @@ -24,6 +24,8 @@ const projectDefaultRoles = new Map([ ['oblt', 'editor'], ]); +const projectTypesWithCustomRolesEnabled = ['es', 'security']; + const getDefaultServerlessRole = (projectType: string) => { if (projectDefaultRoles.has(projectType)) { return projectDefaultRoles.get(projectType)!; @@ -50,18 +52,39 @@ export class ServerlessAuthProvider implements AuthProvider { this.rolesDefinitionPath = resolve(SERVERLESS_ROLES_ROOT_PATH, this.projectType, 'roles.yml'); } - getSupportedRoleDescriptors(): Record { - return readRolesDescriptorsFromResource(this.rolesDefinitionPath); + getSupportedRoleDescriptors() { + const roleDescriptors = new Map( + Object.entries( + readRolesDescriptorsFromResource(this.rolesDefinitionPath) as Record + ) + ); + // 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(); } diff --git a/packages/kbn-ftr-common-functional-services/services/saml_auth/stateful/auth_provider.ts b/packages/kbn-ftr-common-functional-services/services/saml_auth/stateful/auth_provider.ts index 10981afd5fac1..f4a8b5a8abff1 100644 --- a/packages/kbn-ftr-common-functional-services/services/saml_auth/stateful/auth_provider.ts +++ b/packages/kbn-ftr-common-functional-services/services/saml_auth/stateful/auth_provider.ts @@ -18,12 +18,31 @@ import { export class StatefulAuthProvider implements AuthProvider { private readonly rolesDefinitionPath = resolve(REPO_ROOT, STATEFUL_ROLES_ROOT_PATH, 'roles.yml'); - getSupportedRoleDescriptors(): Record { - return readRolesDescriptorsFromResource(this.rolesDefinitionPath); + + getSupportedRoleDescriptors() { + const roleDescriptors = new Map( + Object.entries( + readRolesDescriptorsFromResource(this.rolesDefinitionPath) as Record + ) + ); + // 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; } diff --git a/x-pack/test/api_integration/deployment_agnostic/default_configs/serverless.config.base.ts b/x-pack/test/api_integration/deployment_agnostic/default_configs/serverless.config.base.ts index 1c49433d742af..f73af3a6d4bf7 100644 --- a/x-pack/test/api_integration/deployment_agnostic/default_configs/serverless.config.base.ts +++ b/x-pack/test/api_integration/deployment_agnostic/default_configs/serverless.config.base.ts @@ -100,6 +100,10 @@ export function createServerlessTestConfig { + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); + await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover'); + await kibanaServer.uiSettings.update({ + defaultIndex: 'logstash-*', + }); + await samlAuth.setCustomRole({ + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + discover: ['read'], + }, + spaces: ['*'], + }, + ], + }); + // login with custom role + await pageObjects.svlCommonPage.loginWithCustomRole(); + await pageObjects.svlCommonPage.assertUserAvatarExists(); + }); + + after(async () => { + await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional'); + await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover'); + await kibanaServer.uiSettings.replace({}); + await kibanaServer.savedObjects.cleanStandardList(); + if (roleAuthc) { + await samlAuth.invalidateM2mApiKeyWithRoleScope(roleAuthc); + } + }); + + it('should have limited navigation menu', async () => { + await pageObjects.svlCommonPage.assertUserAvatarExists(); + // discover navigation link is present + await testSubjects.existOrFail('~nav-item-search_project_nav.kibana.discover'); + // dashboard and index_management navigation links are hidden + await testSubjects.missingOrFail('~nav-item-search_project_nav.kibana.dashboard'); + await testSubjects.missingOrFail( + 'nav-item-search_project_nav.content.management:index_management' + ); + }); + + it('should access Discover app', async () => { + await pageObjects.common.navigateToApp('discover'); + await pageObjects.timePicker.setDefaultAbsoluteRange(); + await pageObjects.header.waitUntilLoadingHasFinished(); + expect(await testSubjects.exists('unifiedHistogramChart')).to.be(true); + expect(await testSubjects.exists('discoverQueryHits')).to.be(true); + }); + + it('should access console with API key', async () => { + roleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('customRole'); + const { body } = await supertestWithoutAuth + .get('/api/console/api_server') + .set(roleAuthc.apiKeyHeader) + .set(samlAuth.getInternalRequestHeader()) + .set({ 'kbn-xsrf': 'true' }) + .expect(200); + expect(body.es).to.be.ok(); + }); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/search/index.ts b/x-pack/test_serverless/functional/test_suites/search/index.ts index 250f99d13a3a1..b2be587e51ea5 100644 --- a/x-pack/test_serverless/functional/test_suites/search/index.ts +++ b/x-pack/test_serverless/functional/test_suites/search/index.ts @@ -23,7 +23,7 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./rules/rule_details')); loadTestFile(require.resolve('./console_notebooks')); loadTestFile(require.resolve('./search_playground/playground_overview')); - loadTestFile(require.resolve('./ml')); + loadTestFile(require.resolve('./custom_role_access')); }); }