Skip to content

Commit

Permalink
add saml auth and fetch cookie for Kibana
Browse files Browse the repository at this point in the history
  • Loading branch information
dmlemeshko committed Nov 8, 2023
1 parent b135763 commit 64f5beb
Show file tree
Hide file tree
Showing 5 changed files with 239 additions and 76 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,27 +15,27 @@ export function SvlCommonPageProvider({ getService, getPageObjects }: FtrProvide
const deployment = getService('deployment');
const log = getService('log');
const browser = getService('browser');
const svlUserManager = getService('svlUserManager');

const delay = (ms: number) =>
new Promise((resolve) => {
setTimeout(resolve, ms);
});

return {
async loginWithRole(roleName: string) {
log.debug(`Logging with cookie for '${roleName}' role`);
// get the Cookie using svlRoleManager
// await svlRoleManager.getCookieByRole(roleName);
await browser.setCookie('sid', '');
async loginWithRole(role: string) {
log.debug(`Logging with cookie for '${role}' role`);
const session = await svlUserManager.getSessionByRole(role);
await browser.get(deployment.getHostPort() + '/bootstrap.js');
await browser.setCookie('sid', session.cookie);
await browser.get(deployment.getHostPort());
// ensure welcome screen won't be shown. This is relevant for environments which don't allow
// to use the yml setting, e.g. cloud
await browser.setLocalStorageItem('home:welcome:show', 'false');
if (await testSubjects.exists('userMenuButton', { timeout: 10_000 })) {
log.debug('userMenuButton is found, logged in passed');
return true;
} else {
throw new Error(`Failed to login with cookie for '${roleName}' role`);
throw new Error(`Failed to login with cookie for '${role}' role`);
}
},

Expand Down
2 changes: 1 addition & 1 deletion x-pack/test_serverless/shared/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import { SvlReportingServiceProvider } from './svl_reporting';
import { SupertestProvider, SupertestWithoutAuthProvider } from './supertest';
import { SvlCommonApiServiceProvider } from './svl_common_api';
import { SvlUserManagerProvider } from './svl_user_manager';
import { SvlUserManagerProvider } from './user_manager/svl_user_manager';

export const services = {
supertest: SupertestProvider,
Expand Down
68 changes: 0 additions & 68 deletions x-pack/test_serverless/shared/services/svl_user_manager.ts

This file was deleted.

122 changes: 122 additions & 0 deletions x-pack/test_serverless/shared/services/user_manager/saml_auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import axios, { AxiosResponse } from 'axios';
import Url from 'url';
import * as cheerio from 'cheerio';

export interface SessionParams {
username: string;
password: string;
cloudEnv: CloudEnv;
kbnHost: string;
kbnVersion: string;
}

export type CloudEnv = 'qa' | 'staging' | 'production';

const envHosts: { [key: string]: string } = {
qa: 'console.qa.cld.elstc.co',
staging: 'staging.found.no',
production: 'api.elastic-cloud.com',
};

const getSidCookie = (cookies: string[] | undefined) => {
return cookies?.[0].toString().split(';')[0].split('sid=')[1] ?? '';
};

const getHostUrl = (env: CloudEnv, pathname?: string) => {
return Url.format({
protocol: 'https',
hostname: envHosts[env],
pathname,
});
};

const createCloudSession = async (env: CloudEnv, email: string, password: string) => {
const cloudLoginUrl = getHostUrl(env, '/api/v1/users/_login');
const sessionResponse: AxiosResponse = await axios.request({
url: cloudLoginUrl,
method: 'post',
data: {
email,
password,
},
headers: {
accept: 'application/json',
'content-type': 'application/json',
},
validateStatus: () => true,
maxRedirects: 0,
});
return sessionResponse.data.token;
};

const createSAMLRequest = async (kbnUrl: string, kbnVersion: string) => {
const samlResponse: AxiosResponse = await axios.request({
url: kbnUrl + '/internal/security/login',
method: 'post',
data: {
providerType: 'saml',
providerName: 'cloud-saml-kibana',
currentURL: kbnUrl + '/login?next=%2F"',
},
headers: {
'kbn-version': kbnVersion,
'x-elastic-internal-origin': 'Kibana',
'content-type': 'application/json',
},
validateStatus: () => true,
maxRedirects: 0,
});
const sid = getSidCookie(samlResponse.headers['set-cookie']);
return { location: samlResponse.data.location, sid };
};

const createSAMLResponse = async (url: string, ecSession: string) => {
const samlResponse = await axios.get(url, {
headers: {
Cookie: `ec_session=${ecSession}`,
},
});
const $ = cheerio.load(samlResponse.data);
const value = $('input').attr('value') ?? '';
if (value.length === 0) {
throw new Error('Failed to parse SAML response value');
}
return value;
};

const finishSAMLHandshake = async (
kbnHost: string,
samlResponse: string,
cloudSessionSid: string
) => {
const encodedResponse = encodeURIComponent(samlResponse);
const authResponse: AxiosResponse = await axios.request({
url: kbnHost + '/api/security/saml/callback',
method: 'post',
data: `SAMLResponse=${encodedResponse}`,
headers: {
Cookie: `sid=${cloudSessionSid}`,
'content-type': 'application/x-www-form-urlencoded',
},
validateStatus: () => true,
maxRedirects: 0,
});

return getSidCookie(authResponse.headers['set-cookie']);
};

export const createNewSAMLSession = async (params: SessionParams) => {
const { username, password, cloudEnv, kbnHost, kbnVersion } = params;
const ecSession = await createCloudSession(cloudEnv, username, password);
const { location, sid } = await createSAMLRequest(kbnHost, kbnVersion);
const samlResponse = await createSAMLResponse(location, ecSession);
const cookie = await finishSAMLHandshake(kbnHost, samlResponse, sid);
return { username, cookie };
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { REPO_ROOT } from '@kbn/repo-info';
import { resolve } from 'path';
import * as fs from 'fs';
import Url from 'url';
import { createNewSAMLSession } from './saml_auth';
import { FtrProviderContext } from '../../../functional/ftr_provider_context';

export interface User {
readonly username: string;
readonly password: string;
}

export type Role = string;
export interface Session {
cookie: string;
username: string;
}

export function SvlUserManagerProvider({ getService }: FtrProviderContext) {
const kibanaServer = getService('kibanaServer');
const config = getService('config');
const log = getService('log');
const isServerless = config.get('serverless');
const isCloud = !!process.env.TEST_CLOUD;
// const cloudEnv = process.env.TEST_CLOUD_ENV ?? 'qa';
const cloudRoleUsersFilePath = resolve(REPO_ROOT, '.ftr', 'role_users.json');
// const roles = Object.keys(
// loadYaml(fs.readFileSync('packages/kbn-es/src/serverless_resources/roles.yml', 'utf8'))
// );
let users: { [key: string]: User };

if (!isServerless) {
throw new Error(`'svlUserManager' service can't be used in non-serverless FTR context`);
}

if (!isCloud) {
log.warning(
`Roles testing is only available on Cloud at the moment.
We are working to enable it for the local development`
);
} else {
// QAF should prepare the file on MKI pipelines
if (!fs.existsSync(cloudRoleUsersFilePath)) {
throw new Error(
`svlUserManager service requires user roles to be defined in ${cloudRoleUsersFilePath}`
);
}

const data = fs.readFileSync(cloudRoleUsersFilePath, 'utf8');
if (data.length === 0) {
throw new Error(`'${cloudRoleUsersFilePath}' is empty: no roles are defined`);
}
users = JSON.parse(data);
}

// to be re-used within FTr config run
const sessionCache = new Map<Role, Session>();

return {
getApiKeyByRole() {
// Get API key from cookie for API integration tests
},

async getSessionByRole(role: string) {
if (sessionCache.has(role)) {
return sessionCache.get(role)!;
}

log.debug(`new SAML authentication with ${role} role`);
const { username, password } = this.getUserByRole(role);
const kbnVersion = await kibanaServer.version.get();
const kbnHost = Url.format({
protocol: config.get('servers.kibana.protocol'),
hostname: config.get('servers.kibana.hostname'),
});

const session = await createNewSAMLSession({
username,
password,
cloudEnv: 'qa',
kbnHost,
kbnVersion,
});

sessionCache.set(role, session);
return session;
},

getUserByRole(role: string) {
// WIP
if (!isCloud) {
throw new Error('Roles are not defined for local run');
}
const user = users[role];
if (user) {
return user;
} else {
throw new Error(`'${role}' role is not defined in '${cloudRoleUsersFilePath}'`);
}
},
};
}

0 comments on commit 64f5beb

Please sign in to comment.