Skip to content

Commit

Permalink
[ftr] move SAML auth to kbn-test (#172678)
Browse files Browse the repository at this point in the history
## Summary

This PR moves SAML session creation from FTR service to `@kbn/test`. It
should simplify its adoption in non-FTR context, e.g. Cypress tests or
jest integration tests:

```
import { SamlSessionManager } from '@kbn/test';

// create instance in your setup file
const sessionManager = new SamlSessionManager({
  hostOptions: {
    protocol,
    hostname,
    port,
    username,
    password,
  },
  log,
  isCloud
});
```

use it in your tests
```
sessionManager.getSessionCookieForRole('viewer');
```

---------

Co-authored-by: kibanamachine <[email protected]>
Co-authored-by: Aleh Zasypkin <[email protected]>
  • Loading branch information
3 people authored Dec 8, 2023
1 parent cbb16b8 commit a4a0ad7
Show file tree
Hide file tree
Showing 13 changed files with 436 additions and 207 deletions.
2 changes: 1 addition & 1 deletion packages/kbn-test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
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) => {
Expand Down
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 };
}
}
Loading

0 comments on commit a4a0ad7

Please sign in to comment.