-
Notifications
You must be signed in to change notification settings - Fork 8.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add saml auth and fetch cookie for Kibana
- Loading branch information
1 parent
b135763
commit 64f5beb
Showing
5 changed files
with
239 additions
and
76 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
68 changes: 0 additions & 68 deletions
68
x-pack/test_serverless/shared/services/svl_user_manager.ts
This file was deleted.
Oops, something went wrong.
122 changes: 122 additions & 0 deletions
122
x-pack/test_serverless/shared/services/user_manager/saml_auth.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; | ||
}; |
109 changes: 109 additions & 0 deletions
109
x-pack/test_serverless/shared/services/user_manager/svl_user_manager.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}'`); | ||
} | ||
}, | ||
}; | ||
} |