From 599b2a6c29895b71b393146d0d01dcaaea9a852b Mon Sep 17 00:00:00 2001 From: Florian Schade Date: Mon, 30 Nov 2020 02:33:48 +0100 Subject: [PATCH] add auth provider factory add oidc cache and respect token lifetime simplify account retrieval --- tests/k6/src/lib/api/api.ts | 2 +- tests/k6/src/lib/api/dav.ts | 6 +- tests/k6/src/lib/api/users.ts | 2 +- tests/k6/src/lib/auth.ts | 208 +++++++++++++------ tests/k6/src/lib/defaults.ts | 38 +++- tests/k6/src/lib/playbook/dav.ts | 6 +- tests/k6/src/lib/types.ts | 8 +- tests/k6/src/lib/utils.ts | 14 -- tests/k6/src/test/benchmark/file-download.ts | 29 +-- tests/k6/src/test/benchmark/file-upload.ts | 26 +-- tests/k6/src/test/issue/github/ocis/162.ts | 1 - 11 files changed, 200 insertions(+), 140 deletions(-) diff --git a/tests/k6/src/lib/api/api.ts b/tests/k6/src/lib/api/api.ts index 8b9f3e344..b6bf237b6 100644 --- a/tests/k6/src/lib/api/api.ts +++ b/tests/k6/src/lib/api/api.ts @@ -1,7 +1,7 @@ import encoding from 'k6/encoding'; import * as types from "../types"; -export const headersDefault = ({credential}: { credential: types.Account | types.Token }): { [key: string]: string } => { +export const headersDefault = ({credential}: { credential: types.Credential }): { [key: string]: string } => { const isOIDCGuard = (credential as types.Token).tokenType !== undefined; const authOIDC = credential as types.Token; const authBasic = credential as types.Account; diff --git a/tests/k6/src/lib/api/dav.ts b/tests/k6/src/lib/api/dav.ts index 1d0b58b52..2d7407cff 100644 --- a/tests/k6/src/lib/api/dav.ts +++ b/tests/k6/src/lib/api/dav.ts @@ -4,7 +4,7 @@ import * as defaults from "../defaults"; import * as types from "../types"; export const fileUpload = ( - {credential, userName, asset}: { credential: types.Account | types.Token; userName: string; asset: types.Asset } + {credential, userName, asset}: { credential: types.Credential; userName: string; asset: types.Asset } ): RefinedResponse => { return http.put( `${defaults.OC_HOST}/remote.php/dav/files/${userName}/${asset.fileName}`, @@ -18,7 +18,7 @@ export const fileUpload = ( } export const fileDownload = ( - {credential, userName, fileName}: { credential: types.Account | types.Token; userName: string; fileName: string } + {credential, userName, fileName}: { credential: types.Credential; userName: string; fileName: string } ): RefinedResponse => { return http.get( `${defaults.OC_HOST}/remote.php/dav/files/${userName}/${fileName}`, @@ -31,7 +31,7 @@ export const fileDownload = ( } export const fileDelete = ( - {credential, userName, fileName}: { credential: types.Account | types.Token; userName: string; fileName: string } + {credential, userName, fileName}: { credential: types.Credential; userName: string; fileName: string } ): RefinedResponse => { return http.del( `${defaults.OC_HOST}/remote.php/dav/files/${userName}/${fileName}`, diff --git a/tests/k6/src/lib/api/users.ts b/tests/k6/src/lib/api/users.ts index b6dd38df0..84f3a7e8e 100644 --- a/tests/k6/src/lib/api/users.ts +++ b/tests/k6/src/lib/api/users.ts @@ -4,7 +4,7 @@ import * as defaults from "../defaults"; import * as types from "../types"; export const userInfo = ( - {credential, userName}: { credential: types.Account | types.Token; userName: string; } + {credential, userName}: { credential: types.Credential; userName: string; } ): RefinedResponse => { return http.get( `${defaults.OC_HOST}/ocs/v1.php/cloud/users/${userName}`, diff --git a/tests/k6/src/lib/auth.ts b/tests/k6/src/lib/auth.ts index 73e784bf7..13dc3602f 100644 --- a/tests/k6/src/lib/auth.ts +++ b/tests/k6/src/lib/auth.ts @@ -5,83 +5,159 @@ import * as types from "./types"; import {fail} from 'k6'; import {get} from 'lodash' -export const oidc = (account: types.Account): types.Token => { - const redirectUri = `${defaults.OC_OIDC_HOST}/oidc-callback.html`; - const logonUri = `${defaults.OC_OIDC_HOST}/signin/v1/identifier/_/logon`; - const logonResponse = http.post( - logonUri, - JSON.stringify( - { - params: [account.login, account.password, '1'], - hello: { - scope: 'openid profile email', - client_id: 'phoenix', - redirect_uri: redirectUri, - flow: 'oidc' - }, - 'state': 'vp42cf' - }, - ), - { - headers: { - 'Kopano-Konnect-XSRF': '1', - Referer: defaults.OC_OIDC_HOST, - 'Content-Type': 'application/json', - }, - }, - ); - const authorizeURI = get(logonResponse.json(), 'hello.continue_uri'); +export default class Factory { + private provider!: types.AuthProvider; + public account!: types.Account; - if (logonResponse.status != 200 || !authorizeURI) { - fail(logonUri); + constructor(account: types.Account) { + this.account = account; + + if (defaults.OC_OIDC) { + this.provider = new OIDCProvider(account); + } + + if (!defaults.OC_OIDC) { + this.provider = new AccountProvider(account); + } } - const authorizeUri = `${authorizeURI}?${ - queryString.stringify( + public get credential(): types.Credential { + return this.provider.credential + } +} + +class AccountProvider implements types.AuthProvider { + private account: types.Account; + + constructor(account: types.Account) { + this.account = account; + } + + public get credential(): types.Account { + return this.account; + } +} + +class OIDCProvider implements types.AuthProvider { + private account: types.Account; + private redirectUri = `${defaults.OC_OIDC_HOST}/oidc-callback.html`; + private logonUri = `${defaults.OC_OIDC_HOST}/signin/v1/identifier/_/logon`; + private tokenUrl = `${defaults.OC_OIDC_HOST}/konnect/v1/token`; + private cache!: { + validTo: Date; + token: types.Token; + } + + constructor(account: types.Account) { + this.account = account; + } + + public get credential(): types.Token { + if (!this.cache || this.cache.validTo <= new Date()) { + const continueURI = this.getContinueURI(); + const code = this.getCode(continueURI); + const token = this.getToken(code); + + this.cache = { + validTo: ((): Date => { + const offset = 5; + const d = new Date(); + + d.setSeconds(d.getSeconds() + token.expiresIn - offset) + + return d + })(), + token, + } + } + + return this.cache.token; + } + + private getContinueURI(): string { + const logonResponse = http.post( + this.logonUri, + JSON.stringify( + { + params: [this.account.login, this.account.password, '1'], + hello: { + scope: 'openid profile email', + client_id: 'phoenix', + redirect_uri: this.redirectUri, + flow: 'oidc' + }, + 'state': 'vp42cf' + }, + ), { - client_id: 'phoenix', - prompt: 'none', - redirect_uri: redirectUri, - response_mode: 'query', - response_type: 'code', - scope: 'openid profile email', + headers: { + 'Kopano-Konnect-XSRF': '1', + Referer: defaults.OC_OIDC_HOST, + 'Content-Type': 'application/json', + }, + }, + ); + const continueURI = get(logonResponse.json(), 'hello.continue_uri'); + + if (logonResponse.status != 200 || !continueURI) { + fail(this.logonUri); + } + + return continueURI; + } + + private getCode(continueURI: string): string { + const authorizeUri = `${continueURI}?${ + queryString.stringify( + { + client_id: 'phoenix', + prompt: 'none', + redirect_uri: this.redirectUri, + response_mode: 'query', + response_type: 'code', + scope: 'openid profile email', + }, + ) + }`; + const authorizeResponse = http.get( + authorizeUri, + { + redirects: 0, }, ) - }`; - const authorizeResponse = http.get( - authorizeUri, - { - redirects: 0, - }, - ) - const authCode = get(queryString.parseUrl(authorizeResponse.headers.Location), 'query.code') - if (authorizeResponse.status != 302 || !authCode) { - fail(authorizeURI); - } + const code = get(queryString.parseUrl(authorizeResponse.headers.Location), 'query.code') - const tokenUrl = `${defaults.OC_OIDC_HOST}/konnect/v1/token`; - const tokenResponse = http.post( - tokenUrl, - { - client_id: 'phoenix', - code: authCode, - redirect_uri: redirectUri, - grant_type: 'authorization_code' + if (authorizeResponse.status != 302 || !code) { + fail(continueURI); } - ) - const token = { - accessToken: get(tokenResponse.json(), 'access_token'), - tokenType: get(tokenResponse.json(), 'token_type'), - idToken: get(tokenResponse.json(), 'id_token'), - expiresIn: get(tokenResponse.json(), 'expires_in'), + return code } - if (tokenResponse.status != 200 || !token.accessToken || !token.tokenType || !token.idToken || !token.expiresIn) { - fail(authorizeURI); - } + private getToken(code: string): types.Token { + const tokenResponse = http.post( + this.tokenUrl, + { + client_id: 'phoenix', + code, + redirect_uri: this.redirectUri, + grant_type: 'authorization_code' + } + ) - return token -} \ No newline at end of file + const token = { + accessToken: get(tokenResponse.json(), 'access_token'), + tokenType: get(tokenResponse.json(), 'token_type'), + idToken: get(tokenResponse.json(), 'id_token'), + expiresIn: get(tokenResponse.json(), 'expires_in'), + } + + if (tokenResponse.status != 200 || !token.accessToken || !token.tokenType || !token.idToken || !token.expiresIn) { + fail(this.tokenUrl); + } + + return token; + } +} diff --git a/tests/k6/src/lib/defaults.ts b/tests/k6/src/lib/defaults.ts index 094456a43..3798b1dcf 100644 --- a/tests/k6/src/lib/defaults.ts +++ b/tests/k6/src/lib/defaults.ts @@ -11,16 +11,32 @@ export const OC_TEST_FILE = { fileName: ocTestFile, bytes: open(ocTestFile, 'b'), } -export const k6OptionsDefault: Options = { +export const K6_OPTION_DEFAULTS: Options = { insecureSkipTLSVerify: true, }; -export const knownAccounts: { [key: string]: types.Account; } = { - einstein: { - login: 'einstein', - password: 'relativity', - }, - richard: { - login: 'richard', - password: 'superfluidity', - }, -} \ No newline at end of file + +export class ACCOUNTS { + public static readonly EINSTEIN = 'einstein'; + public static readonly RICHARD = 'richard'; + private static readonly list: { [key: string]: types.Account; } = { + einstein: { + login: 'einstein', + password: 'relativity', + }, + richard: { + login: 'richard', + password: 'superfluidity', + }, + } + + public static for(key: string): types.Account { + if (OC_LOGIN && OC_PASSWORD) { + return { + login: OC_LOGIN, + password: OC_PASSWORD, + } + } + + return this.list[key]; + } +} diff --git a/tests/k6/src/lib/playbook/dav.ts b/tests/k6/src/lib/playbook/dav.ts index 089710847..36c677541 100644 --- a/tests/k6/src/lib/playbook/dav.ts +++ b/tests/k6/src/lib/playbook/dav.ts @@ -8,7 +8,7 @@ export const fileUpload = () => { const fileUploadTrend = new Trend('occ_file_upload_trend', true); const fileUploadErrorRate = new Gauge('occ_file_upload_error_rate'); - return ({credential, userName, asset}: { credential: types.Account | types.Token; userName: string; asset: types.Asset }): string => { + return ({credential, userName, asset}: { credential: types.Credential; userName: string; asset: types.Asset }): string => { const fileName = `upload-${userName}-${__VU}-${__ITER}.${utils.extension(asset.fileName)}`; const uploadResponse = api.dav.fileUpload({ credential: credential as any, @@ -33,7 +33,7 @@ export const fileDelete = () => { const fileDeleteTrend = new Trend('occ_file_delete_trend', true); const fileDeleteErrorRate = new Gauge('occ_file_delete_error_rate'); - return ({credential, userName, fileName}: { credential: types.Account | types.Token, userName: string; fileName: string }) => { + return ({credential, userName, fileName}: { credential: types.Credential, userName: string; fileName: string }) => { const deleteResponse = api.dav.fileDelete({ credential: credential as any, fileName, @@ -52,7 +52,7 @@ export const fileDownload = () => { const fileDownloadTrend = new Trend('occ_file_download_trend', true); const fileDownloadErrorRate = new Gauge('occ_file_download_error_rate'); - return ({credential, userName, fileName}: { credential: types.Account | types.Token, userName: string; fileName: string }): bytes => { + return ({credential, userName, fileName}: { credential: types.Credential, userName: string; fileName: string }): bytes => { const downloadResponse = api.dav.fileDownload({ credential: credential as any, fileName, diff --git a/tests/k6/src/lib/types.ts b/tests/k6/src/lib/types.ts index 76567b290..159be1087 100644 --- a/tests/k6/src/lib/types.ts +++ b/tests/k6/src/lib/types.ts @@ -9,10 +9,16 @@ export interface Token { accessToken: string; tokenType: string; idToken: string; - expiresIn: string; + expiresIn: number; } export interface Account { login: string password: string +} + +export type Credential = Token | Account + +export interface AuthProvider { + credential: Credential } \ No newline at end of file diff --git a/tests/k6/src/lib/utils.ts b/tests/k6/src/lib/utils.ts index 36da0281d..54f9dcb35 100644 --- a/tests/k6/src/lib/utils.ts +++ b/tests/k6/src/lib/utils.ts @@ -1,6 +1,3 @@ -import * as types from "./types"; -import * as defaults from "./defaults"; - export const randomString = (): string => { return Math.random().toString(36).slice(2) } @@ -9,14 +6,3 @@ export const extension = (p: string): string | undefined => { return (p.split('/').pop())!.split('.').pop() } -export const getAccount = (key: string): types.Account => { - if (defaults.OC_LOGIN && defaults.OC_PASSWORD) { - return { - login: defaults.OC_LOGIN, - password: defaults.OC_PASSWORD, - } - } - - return defaults.knownAccounts[key]; -} - diff --git a/tests/k6/src/test/benchmark/file-download.ts b/tests/k6/src/test/benchmark/file-download.ts index 21ef37de7..0e8ff5f23 100644 --- a/tests/k6/src/test/benchmark/file-download.ts +++ b/tests/k6/src/test/benchmark/file-download.ts @@ -1,35 +1,24 @@ import {defaults, playbook} from '../../lib' import {Options} from 'k6/options'; import {sleep} from "k6"; -import * as auth from "../../lib/auth"; -import * as types from "../../lib/types"; -import * as utils from "../../lib/utils"; - -interface dataI { - credential: types.Account | types.Token; -} +import auth from "../../lib/auth"; export const options: Options = { - ...defaults.k6OptionsDefault, + ...defaults.K6_OPTION_DEFAULTS, iterations: 1, vus: 1, }; -const account = utils.getAccount('einstein'); +const authFactory = new auth(defaults.ACCOUNTS.for(defaults.ACCOUNTS.EINSTEIN)); const playbooks = { fileUpload: playbook.dav.fileUpload(), fileDownload: playbook.dav.fileDownload(), fileDelete: playbook.dav.fileDelete(), } -export const setup = (): dataI => { - return { - credential: defaults.OC_OIDC ? auth.oidc(account) : account, - } -} -export default (data: dataI) => { - const credential = data.credential; - const userName = account.login; + +export default () => { + const {login: userName} = authFactory.account; const fileName = playbooks.fileUpload({ - credential, + credential: authFactory.credential, userName, asset: defaults.OC_TEST_FILE }); @@ -37,7 +26,7 @@ export default (data: dataI) => { sleep(1) playbooks.fileDownload({ - credential, + credential: authFactory.credential, userName, fileName, }); @@ -45,7 +34,7 @@ export default (data: dataI) => { sleep(1) playbooks.fileDelete({ - credential, + credential: authFactory.credential, userName, fileName, }); diff --git a/tests/k6/src/test/benchmark/file-upload.ts b/tests/k6/src/test/benchmark/file-upload.ts index 062c14c95..0d068e951 100644 --- a/tests/k6/src/test/benchmark/file-upload.ts +++ b/tests/k6/src/test/benchmark/file-upload.ts @@ -1,34 +1,22 @@ import {defaults, playbook} from '../../lib' import {Options} from 'k6/options'; import {sleep} from "k6"; -import * as auth from "../../lib/auth"; -import * as types from "../../lib/types"; -import * as utils from "../../lib/utils"; - -interface dataI { - credential: types.Account | types.Token; -} +import auth from "../../lib/auth"; export const options: Options = { - ...defaults.k6OptionsDefault, + ...defaults.K6_OPTION_DEFAULTS, iterations: 1, vus: 1, }; -const account = utils.getAccount('einstein'); +const authFactory = new auth(defaults.ACCOUNTS.for(defaults.ACCOUNTS.EINSTEIN)); const playbooks = { fileUpload: playbook.dav.fileUpload(), fileDelete: playbook.dav.fileDelete(), } -export const setup = (): dataI => { - return { - credential: defaults.OC_OIDC ? auth.oidc(account) : account, - } -} -export default (data: dataI) => { - const credential = data.credential; - const userName = account.login; +export default () => { + const {login: userName} = authFactory.account; const fileName = playbooks.fileUpload({ - credential, + credential: authFactory.credential, userName, asset: defaults.OC_TEST_FILE }); @@ -36,7 +24,7 @@ export default (data: dataI) => { sleep(1) playbooks.fileDelete({ - credential, + credential: authFactory.credential, userName, fileName, }); diff --git a/tests/k6/src/test/issue/github/ocis/162.ts b/tests/k6/src/test/issue/github/ocis/162.ts index ec574f947..144535de8 100644 --- a/tests/k6/src/test/issue/github/ocis/162.ts +++ b/tests/k6/src/test/issue/github/ocis/162.ts @@ -6,5 +6,4 @@ export const options: Options = { iterations: 200, vus: 50, }; -export const {setup} = uploadFilesBenchmark; export default uploadFilesBenchmark.default; \ No newline at end of file