Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ftr] move SAML auth to kbn-test #172678

Merged
merged 21 commits into from
Dec 8, 2023
Merged
Changes from 16 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
bf8c7f9
[ftr] move SAML auth to kbn-test
dmlemeshko Dec 6, 2023
3ecba70
fetch kibana version only for cloud saml session
dmlemeshko Dec 6, 2023
cdbf959
[CI] Auto-commit changed files from 'node scripts/lint_ts_projects --…
kibanamachine Dec 6, 2023
30a9fa1
revert local prettier change
dmlemeshko Dec 6, 2023
b931c6c
Merge remote-tracking branch 'upstream/main' into ftr/move-saml-auth-…
dmlemeshko Dec 6, 2023
09f7893
Merge branch 'ftr/move-saml-auth-to-kbn-test' of github.com:dmlemeshk…
dmlemeshko Dec 6, 2023
d217a4c
[CI] Auto-commit changed files from 'node scripts/eslint --no-cache -…
kibanamachine Dec 6, 2023
82f750b
change interface to not rely on FTR config
dmlemeshko Dec 7, 2023
eaf24d6
Merge branch 'ftr/move-saml-auth-to-kbn-test' of github.com:dmlemeshk…
dmlemeshko Dec 7, 2023
740ab77
Merge branch 'main' of github.com:elastic/kibana into ftr/move-saml-a…
dmlemeshko Dec 7, 2023
3f46466
[CI] Auto-commit changed files from 'node scripts/eslint --no-cache -…
kibanamachine Dec 7, 2023
fac981e
clean code
dmlemeshko Dec 7, 2023
7ef742e
add unit tests
dmlemeshko Dec 7, 2023
692b618
Merge branch 'ftr/move-saml-auth-to-kbn-test' of github.com:dmlemeshk…
dmlemeshko Dec 7, 2023
27afd47
Merge remote-tracking branch 'upstream/main' into ftr/move-saml-auth-…
dmlemeshko Dec 7, 2023
63537f9
Update packages/kbn-test/src/auth/session_manager.ts
dmlemeshko Dec 7, 2023
568d02a
update tests
dmlemeshko Dec 7, 2023
2ece8e7
Merge branch 'ftr/move-saml-auth-to-kbn-test' of github.com:dmlemeshk…
dmlemeshko Dec 7, 2023
93664d5
update api integration test example
dmlemeshko Dec 7, 2023
405dde8
Merge remote-tracking branch 'upstream/main' into ftr/move-saml-auth-…
dmlemeshko Dec 7, 2023
c4ead20
[CI] Auto-commit changed files from 'node scripts/eslint --no-cache -…
kibanamachine Dec 7, 2023
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
2 changes: 1 addition & 1 deletion packages/kbn-test/index.ts
Original file line number Diff line number Diff line change
@@ -13,7 +13,7 @@ export { startServersCli, startServers } from './src/functional_tests/start_serv

// @internal
export { runTestsCli, runTests } from './src/functional_tests/run_tests';

export { SamlSessionManager, type SamlSessionManagerOptions, type HostOptions } from './src/auth';
export { runElasticsearch, runKibanaServer } from './src/functional_tests/lib';
export { getKibanaCliArg, getKibanaCliLoggers } from './src/functional_tests/lib/kibana_cli_args';

22 changes: 22 additions & 0 deletions packages/kbn-test/src/auth/helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import * as fs from 'fs';
import { Role, User } from './types';

export const readCloudUsersFromFile = (filePath: string): Array<[Role, User]> => {
if (!fs.existsSync(filePath)) {
throw new Error(`Please define user roles with email/password in ${filePath}`);
}
const data = fs.readFileSync(filePath, 'utf8');
if (data.length === 0) {
throw new Error(`'${filePath}' is empty: no roles are defined`);
}

return Object.entries(JSON.parse(data)) as Array<[Role, User]>;
};
13 changes: 13 additions & 0 deletions packages/kbn-test/src/auth/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

export {
SamlSessionManager,
type SamlSessionManagerOptions,
type HostOptions,
} from './session_manager';
Original file line number Diff line number Diff line change
@@ -1,40 +1,32 @@
/*
* 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.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { createSAMLResponse as createMockedSAMLResponse } from '@kbn/mock-idp-plugin/common';
import { ToolingLog } from '@kbn/tooling-log';
import axios, { AxiosResponse } from 'axios';
import * as cheerio from 'cheerio';
import { parse as parseCookie } from 'tough-cookie';
import { Cookie, parse as parseCookie } from 'tough-cookie';
import Url from 'url';
import { Session } from './svl_user_manager';

export interface CloudSamlSessionParams {
email: string;
password: string;
kbnHost: string;
kbnVersion: string;
log: ToolingLog;
}

export interface LocalSamlSessionParams {
username: string;
email: string;
fullname: string;
role: string;
kbnHost: string;
log: ToolingLog;
}
import { CloudSamlSessionParams, CreateSamlSessionParams, LocalSamlSessionParams } from './types';

export class Session {
readonly cookie;
readonly email;
readonly fullname;
constructor(cookie: Cookie, email: string, fullname: string) {
this.cookie = cookie;
this.email = email;
this.fullname = fullname;
}

export interface CreateSamlSessionParams {
hostname: string;
email: string;
password: string;
log: ToolingLog;
getCookieValue() {
return this.cookie.value;
}
}

const cleanException = (url: string, ex: any) => {
135 changes: 135 additions & 0 deletions packages/kbn-test/src/auth/session_manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { REPO_ROOT } from '@kbn/repo-info';
import { ToolingLog } from '@kbn/tooling-log';
import { resolve } from 'path';
import Url from 'url';
import { KbnClient } from '../kbn_client';
import { readCloudUsersFromFile } from './helper';
import { createCloudSAMLSession, createLocalSAMLSession, Session } from './saml_auth';
import { Role, User } from './types';

export interface HostOptions {
protocol: 'http' | 'https';
hostname: string;
port?: number;
username: string;
password: string;
}

export interface SamlSessionManagerOptions {
hostOptions: HostOptions;
isCloud: boolean;
log: ToolingLog;
}

/**
* Manages cookies associated with user roles
*/
export class SamlSessionManager {
private readonly isCloud: boolean;
private readonly kbnHost: string;
private readonly kbnClient: KbnClient;
private readonly log: ToolingLog;
private readonly roleToUserMap: Map<Role, User>;
private readonly sessionCache: Map<Role, Session>;
private readonly userRoleFilePath = resolve(REPO_ROOT, '.ftr', 'role_users.json');

constructor(options: SamlSessionManagerOptions) {
this.isCloud = options.isCloud;
this.log = options.log;
const hostOptionsWithoutAuth = {
protocol: options.hostOptions.protocol,
hostname: options.hostOptions.hostname,
port: options.hostOptions.port,
};
this.kbnHost = Url.format(hostOptionsWithoutAuth);
this.kbnClient = new KbnClient({
log: this.log,
url: Url.format({
...hostOptionsWithoutAuth,
auth: `${options.hostOptions.username}:${options.hostOptions.password}`,
}),
});
this.sessionCache = new Map<Role, Session>();
this.roleToUserMap = new Map<Role, User>();
}

/**
* Loads cloud users from '.ftr/role_users.json'
* QAF prepares the file for CI pipelines, make sure to add it manually for local run
*/
private getCloudUsers = () => {
if (this.roleToUserMap.size === 0) {
const data = readCloudUsersFromFile(this.userRoleFilePath);
for (const [roleName, user] of data) {
this.roleToUserMap.set(roleName, user);
}
}

return this.roleToUserMap;
};

private getCloudUserByRole = (role: string) => {
if (this.getCloudUsers().has(role)) {
return this.getCloudUsers().get(role)!;
} else {
throw new Error(`User with '${role}' role is not defined`);
}
};

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

let session: Session;

if (this.isCloud) {
this.log.debug(`new cloud SAML authentication with '${role}' role`);
const kbnVersion = await this.kbnClient.version.get();
const { email, password } = this.getCloudUserByRole(role);
session = await createCloudSAMLSession({
email,
password,
kbnHost: this.kbnHost,
kbnVersion,
log: this.log,
});
} else {
this.log.debug(`new fake SAML authentication with '${role}' role`);
session = await createLocalSAMLSession({
username: `elastic_${role}`,
email: `elastic_${role}@elastic.co`,
fullname: `test ${role}`,
role,
kbnHost: this.kbnHost,
log: this.log,
});
}

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

async getApiCredentialsForRole(role: string) {
const session = await this.getSessionByRole(role);
return { Cookie: `sid=${session.getCookieValue()}` };
}

async getSessionCookieForRole(role: string) {
const session = await this.getSessionByRole(role);
return session.getCookieValue();
}

async getUserData(role: string) {
const { email, fullname } = await this.getSessionByRole(role);
return { email, fullname };
}
}
156 changes: 156 additions & 0 deletions packages/kbn-test/src/auth/sesson_manager.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { ToolingLog } from '@kbn/tooling-log';
import { Cookie } from 'tough-cookie';
import { Session } from './saml_auth';
import { SamlSessionManager } from './session_manager';
import * as samlAuth from './saml_auth';
import * as helper from './helper';
import { Role, User } from './types';

const log = new ToolingLog();

const cookieInstance = Cookie.parse('sid=kbn_cookie_value; Path=/; Expires=Wed, 01 Oct 2023 07:00:00 GMT')!;
cookieInstance.cookieString;
const email = '[email protected]';
const fullname = 'Test User';

const cloudCookieInstance = Cookie.parse('sid=cloud_cookie_value; Path=/; Expires=Wed, 01 Oct 2023 07:00:00 GMT')!;
const cloudEmail = '[email protected]';
const cloudFullname = 'Test Viewer';

const cloudUsers = new Array<[Role, User]>();
cloudUsers.push(['viewer', {email: "[email protected]", password: "p1234"}])
cloudUsers.push(['admin', {email: "[email protected]", password: "p1234"}])

const createLocalSAMLSessionMock = jest.spyOn(samlAuth, 'createLocalSAMLSession');
const createCloudSAMLSessionMock = jest.spyOn(samlAuth, 'createCloudSAMLSession');
const readCloudUsersFromFileMock = jest.spyOn(helper, 'readCloudUsersFromFile');

jest.mock('../kbn_client/kbn_client', () => {
return {
KbnClient : jest.fn(),
};
})
const get = jest.fn();

beforeEach(() => {
jest.resetAllMocks();

jest.requireMock('../kbn_client/kbn_client').KbnClient.mockImplementation(() => ({ version: { get } }));
get.mockImplementationOnce(() => Promise.resolve('8.12.0'));

createLocalSAMLSessionMock.mockResolvedValue(new Session(cookieInstance, email, fullname));
createCloudSAMLSessionMock.mockResolvedValue(new Session(cloudCookieInstance, cloudEmail, cloudFullname));
readCloudUsersFromFileMock.mockReturnValue(cloudUsers);

});

describe('SamlSessionManager', () => {
describe('for local session', () => {
const hostOptions = {
protocol: 'http' as "http" | "https",
hostname: 'localhost',
port: 5620,
username: 'elastic',
password: 'changeme',
}
const isCloud = false;
test('should create an instance of SamlSessionManager', () => {
const samlSessionManager = new SamlSessionManager({hostOptions, log, isCloud});
expect(samlSessionManager).toBeInstanceOf(SamlSessionManager);
});

test(`'getSessionCookieForRole' should return the actual cookie value`, async () => {
const samlSessionManager = new SamlSessionManager({hostOptions, log, isCloud});
const cookie = await samlSessionManager.getSessionCookieForRole('tester');
expect(cookie).toBe(cookieInstance.value);
});

test(`'getApiCredentialsForRole' should return {Cookie: <cookieString>}`, async () => {
const samlSessionManager = new SamlSessionManager({hostOptions, log, isCloud});
const credentials = await samlSessionManager.getApiCredentialsForRole('tester')
expect(credentials).toEqual({Cookie: `${cookieInstance.cookieString()}`});
});

test(`'getSessionCookieForRole' should call 'createLocalSAMLSession' only once for the same role`, async () => {
const samlSessionManager = new SamlSessionManager({hostOptions, log, isCloud});
await samlSessionManager.getSessionCookieForRole('tester');
await samlSessionManager.getSessionCookieForRole('admin');
await samlSessionManager.getSessionCookieForRole('tester');
expect(createLocalSAMLSessionMock.mock.calls).toHaveLength(2);
expect(createCloudSAMLSessionMock.mock.calls).toHaveLength(0);

});

test(`'getUserData' should return the correct email & fullname`, async () => {
const samlSessionManager = new SamlSessionManager({hostOptions, log, isCloud});
const data = await samlSessionManager.getUserData('tester');
expect(data).toEqual({email, fullname})
});
})

describe('for cloud session', () => {
const hostOptions = {
protocol: 'https' as "http" | "https",
hostname: 'cloud',
username: 'elastic',
password: 'changeme',
}
const isCloud = true;
test('should create an instance of SamlSessionManager', () => {
const samlSessionManager = new SamlSessionManager({hostOptions, log, isCloud});
expect(samlSessionManager).toBeInstanceOf(SamlSessionManager);
});

test(`'getSessionCookieForRole' should return the actual cookie value`, async () => {
const samlSessionManager = new SamlSessionManager({hostOptions, log, isCloud});
createCloudSAMLSessionMock.mockResolvedValue(new Session(cloudCookieInstance, cloudEmail, cloudFullname));
const cookie = await samlSessionManager.getSessionCookieForRole('viewer');
expect(cookie).toBe(cloudCookieInstance.value);
});

test(`'getApiCredentialsForRole' should return {Cookie: <cookieString>}`, async () => {
const samlSessionManager = new SamlSessionManager({hostOptions, log, isCloud});
const credentials = await samlSessionManager.getApiCredentialsForRole('viewer')
expect(credentials).toEqual({Cookie: `${cloudCookieInstance.cookieString()}`});
});

test(`'getSessionCookieForRole' should call 'createLocalSAMLSession' only once for the same role`, async () => {
const samlSessionManager = new SamlSessionManager({hostOptions, log, isCloud});
await samlSessionManager.getSessionCookieForRole('viewer');
await samlSessionManager.getSessionCookieForRole('admin');
await samlSessionManager.getSessionCookieForRole('viewer');
expect(createLocalSAMLSessionMock.mock.calls).toHaveLength(0);
expect(createCloudSAMLSessionMock.mock.calls).toHaveLength(2);
});

test(`'getUserData' should return the correct email & fullname`, async () => {
const samlSessionManager = new SamlSessionManager({hostOptions, log, isCloud});
const data = await samlSessionManager.getUserData('viewer');
expect(data).toEqual({email: cloudEmail, fullname: cloudFullname})
});

test(`'getSessionCookieForRole' throws error when roles does not exist`, async () => {
const nonExistingRole = 'tester';
const samlSessionManager = new SamlSessionManager({hostOptions, log, isCloud});
await expect(samlSessionManager.getSessionCookieForRole(nonExistingRole)).rejects.toThrow(`User with '${nonExistingRole}' role is not defined`);
});

test(`throws error when roles does not exist`, async () => {
const nonExistingRole = 'tester';
const samlSessionManager = new SamlSessionManager({hostOptions, log, isCloud});
await expect(samlSessionManager.getSessionCookieForRole(nonExistingRole)).rejects.toThrow(`User with '${nonExistingRole}' role is not defined`);
await expect(samlSessionManager.getApiCredentialsForRole(nonExistingRole)).rejects.toThrow(`User with '${nonExistingRole}' role is not defined`);
await expect(samlSessionManager.getUserData(nonExistingRole)).rejects.toThrow(`User with '${nonExistingRole}' role is not defined`);
expect(createCloudSAMLSessionMock.mock.calls).toHaveLength(0);
});
})
});

Loading