mirror of
https://github.com/zitadel/zitadel
synced 2024-11-21 16:30:53 +00:00
test(load): machine jwt profile grant (#8482)
# Which Problems Are Solved Currently there was no load test present for machine jwt profile grant. This test is now added # How the Problems Are Solved K6 test implemented. # Additional Context - part of https://github.com/zitadel/zitadel/issues/8352
This commit is contained in:
parent
cbbd44c303
commit
1ce9a4322e
1
.gitignore
vendored
1
.gitignore
vendored
@ -31,6 +31,7 @@ sandbox.go
|
||||
google-credentials
|
||||
key.json
|
||||
.keys/*
|
||||
load-test/.keys
|
||||
|
||||
# dumps
|
||||
.backups
|
||||
|
@ -6,33 +6,40 @@ ADMIN_PASSWORD ?=
|
||||
|
||||
.PHONY: human_password_login
|
||||
human_password_login: bundle
|
||||
k6 run dist/human_password_login.js --vus ${VUS} --duration ${DURATION}
|
||||
k6 run --summary-trend-stats "min,avg,max,p(50),p(95),p(99)" dist/human_password_login.js --vus ${VUS} --duration ${DURATION}
|
||||
|
||||
.PHONY: machine_pat_login
|
||||
machine_pat_login: bundle
|
||||
k6 run dist/machine_pat_login.js --vus ${VUS} --duration ${DURATION}
|
||||
k6 run --summary-trend-stats "min,avg,max,p(50),p(95),p(99)" dist/machine_pat_login.js --vus ${VUS} --duration ${DURATION}
|
||||
|
||||
.PHONY: machine_client_credentials_login
|
||||
machine_client_credentials_login: bundle
|
||||
k6 run dist/machine_client_credentials_login.js --vus ${VUS} --duration ${DURATION}
|
||||
k6 run --summary-trend-stats "min,avg,max,p(50),p(95),p(99)" dist/machine_client_credentials_login.js --vus ${VUS} --duration ${DURATION}
|
||||
|
||||
.PHONY: user_info
|
||||
user_info: bundle
|
||||
k6 run dist/user_info.js --vus ${VUS} --duration ${DURATION}
|
||||
k6 run --summary-trend-stats "min,avg,max,p(50),p(95),p(99)" dist/user_info.js --vus ${VUS} --duration ${DURATION}
|
||||
|
||||
.PHONY: manipulate_user
|
||||
manipulate_user: bundle
|
||||
k6 run dist/manipulate_user.js --vus ${VUS} --duration ${DURATION}
|
||||
k6 run --summary-trend-stats "min,avg,max,p(50),p(95),p(99)" dist/manipulate_user.js --vus ${VUS} --duration ${DURATION}
|
||||
|
||||
.PHONY: introspect
|
||||
introspect: ensure_modules bundle
|
||||
go install go.k6.io/xk6/cmd/xk6@latest
|
||||
cd ../../xk6-modules && xk6 build --with xk6-zitadel=.
|
||||
./../../xk6-modules/k6 run dist/introspection.js --vus ${VUS} --duration ${DURATION}
|
||||
./../../xk6-modules/k6 run --summary-trend-stats "min,avg,max,p(50),p(95),p(99)" dist/introspection.js --vus ${VUS} --duration ${DURATION}
|
||||
|
||||
.PHONY: add_session
|
||||
add_session: bundle
|
||||
k6 run dist/session.js --vus ${VUS} --duration ${DURATION}
|
||||
k6 run --summary-trend-stats "min,avg,max,p(50),p(95),p(99)" dist/session.js --vus ${VUS} --duration ${DURATION}
|
||||
|
||||
.PHONY: machine_jwt_profile_grant
|
||||
machine_jwt_profile_grant: ensure_modules ensure_key_pair bundle
|
||||
go install go.k6.io/xk6/cmd/xk6@latest
|
||||
cd ../../xk6-modules && xk6 build --with xk6-zitadel=.
|
||||
./../../xk6-modules/k6 run --summary-trend-stats "min,avg,max,p(50),p(95),p(99)" dist/machine_jwt_profile_grant.js --iterations 1
|
||||
# --vus ${VUS} --duration ${DURATION}
|
||||
|
||||
.PHONY: lint
|
||||
lint:
|
||||
@ -50,4 +57,16 @@ endif
|
||||
.PHONY: bundle
|
||||
bundle:
|
||||
npm i
|
||||
npm run bundle
|
||||
npm run bundle
|
||||
|
||||
.PHONY: ensure_key_pair
|
||||
ensure_key_pair:
|
||||
ifeq (,$(wildcard $(PWD)/.keys))
|
||||
mkdir .keys
|
||||
endif
|
||||
ifeq (,$(wildcard $(PWD)/.keys/key.pem))
|
||||
openssl genrsa -out .keys/key.pem 2048
|
||||
endif
|
||||
ifeq (,$(wildcard $(PWD)/.keys/key.pem.pub))
|
||||
openssl rsa -in .keys/key.pem -outform PEM -pubout -out .keys/key.pem.pub
|
||||
endif
|
@ -49,4 +49,7 @@ Before you run the tests you need an initialized user. The tests don't implement
|
||||
test: calls introspection endpoint using the given JWTs
|
||||
* `make add_session`
|
||||
setup: creates human users
|
||||
test: creates new sessions with user id check
|
||||
test: creates new sessions with user id check
|
||||
* `make machine_jwt_profile_grant`
|
||||
setup: generates private/public key, creates machine users, adds a key
|
||||
test: creates a token and calls user info
|
44
load-test/package-lock.json
generated
44
load-test/package-lock.json
generated
@ -19,7 +19,7 @@
|
||||
"babel-loader": "9.1.3",
|
||||
"clean-webpack-plugin": "4.0.0",
|
||||
"copy-webpack-plugin": "^12.0.2",
|
||||
"prettier": "^3.1.1",
|
||||
"prettier": "^3.3.3",
|
||||
"prettier-plugin-organize-imports": "^3.2.4",
|
||||
"typescript": "5.4.5",
|
||||
"webpack": "5.89.0",
|
||||
@ -27,7 +27,7 @@
|
||||
"webpack-glob-entries": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "16 || 18 || 20"
|
||||
"node": "18 || 20"
|
||||
}
|
||||
},
|
||||
"node_modules/@ampproject/remapping": {
|
||||
@ -2389,12 +2389,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"fill-range": "^7.0.1"
|
||||
"fill-range": "^7.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@ -2841,9 +2841,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
@ -2965,9 +2965,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/globby": {
|
||||
"version": "14.0.1",
|
||||
"resolved": "https://registry.npmjs.org/globby/-/globby-14.0.1.tgz",
|
||||
"integrity": "sha512-jOMLD2Z7MAhyG8aJpNOpmziMOP4rPLcc95oQPKXBazW82z+CEgPFBQvEpRUa1KeIMUJo4Wsm+q6uzO/Q/4BksQ==",
|
||||
"version": "14.0.2",
|
||||
"resolved": "https://registry.npmjs.org/globby/-/globby-14.0.2.tgz",
|
||||
"integrity": "sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@sindresorhus/merge-streams": "^2.1.0",
|
||||
@ -3012,9 +3012,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ignore": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",
|
||||
"integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==",
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
@ -3383,12 +3383,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/micromatch": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
|
||||
"integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
|
||||
"version": "4.0.7",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz",
|
||||
"integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"braces": "^3.0.2",
|
||||
"braces": "^3.0.3",
|
||||
"picomatch": "^2.3.1"
|
||||
},
|
||||
"engines": {
|
||||
@ -3636,9 +3636,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.2.5",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz",
|
||||
"integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==",
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz",
|
||||
"integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
|
@ -4,30 +4,30 @@
|
||||
"repository": "ssh://git@github.com/zitadel/zitadel.git",
|
||||
"author": "ZITADEL Authors <hi@zitadel.com>",
|
||||
"engines": {
|
||||
"node": "16 || 18 || 20"
|
||||
"node": "18 || 20"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.23.7",
|
||||
"@babel/plugin-proposal-class-properties": "7.13.0",
|
||||
"@babel/plugin-proposal-object-rest-spread": "7.13.8",
|
||||
"@babel/preset-env": "7.23.8",
|
||||
"@babel/preset-typescript": "7.23.3",
|
||||
"@types/k6": ">=0.50.0",
|
||||
"@types/webpack": "5.28.5",
|
||||
"babel-loader": "9.1.3",
|
||||
"clean-webpack-plugin": "4.0.0",
|
||||
"copy-webpack-plugin": "^12.0.2",
|
||||
"typescript": "5.4.5",
|
||||
"webpack": "5.89.0",
|
||||
"webpack-cli": "5.1.4",
|
||||
"webpack-glob-entries": "^1.0.1",
|
||||
"prettier": "^3.1.1",
|
||||
"prettier-plugin-organize-imports": "^3.2.4"
|
||||
"@babel/core": "7.23.7",
|
||||
"@babel/plugin-proposal-class-properties": "7.13.0",
|
||||
"@babel/plugin-proposal-object-rest-spread": "7.13.8",
|
||||
"@babel/preset-env": "7.23.8",
|
||||
"@babel/preset-typescript": "7.23.3",
|
||||
"@types/k6": ">=0.50.0",
|
||||
"@types/webpack": "5.28.5",
|
||||
"babel-loader": "9.1.3",
|
||||
"clean-webpack-plugin": "4.0.0",
|
||||
"copy-webpack-plugin": "^12.0.2",
|
||||
"prettier": "^3.3.3",
|
||||
"prettier-plugin-organize-imports": "^3.2.4",
|
||||
"typescript": "5.4.5",
|
||||
"webpack": "5.89.0",
|
||||
"webpack-cli": "5.1.4",
|
||||
"webpack-glob-entries": "^1.0.1"
|
||||
},
|
||||
"scripts": {
|
||||
"bundle": "webpack",
|
||||
"lint": "prettier --check src",
|
||||
"lint:fix": "prettier --write src"
|
||||
"bundle": "webpack",
|
||||
"lint": "prettier --check src",
|
||||
"lint:fix": "prettier --write src"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,11 @@
|
||||
import { JSONObject, check, fail } from 'k6';
|
||||
import encoding from 'k6/encoding';
|
||||
import http from 'k6/http';
|
||||
import http, { RequestBody } from 'k6/http';
|
||||
import { Trend } from 'k6/metrics';
|
||||
import url from './url';
|
||||
import { Config } from './config';
|
||||
// @ts-ignore Import module
|
||||
import zitadel from 'k6/x/zitadel';
|
||||
|
||||
export class Tokens {
|
||||
idToken?: string;
|
||||
@ -103,4 +106,66 @@ export function clientCredentials(clientId: string, clientSecret: string): Promi
|
||||
resolve(tokens)
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export interface TokenRequest {
|
||||
payload(): RequestBody;
|
||||
headers(): { [name: string]: string; };
|
||||
}
|
||||
|
||||
const privateKey = open('../.keys/key.pem');
|
||||
|
||||
export class JWTProfileRequest implements TokenRequest {
|
||||
keyPayload!: {
|
||||
userId: string;
|
||||
expiration: number;
|
||||
keyId: string;
|
||||
};
|
||||
|
||||
constructor(userId: string, keyId: string) {
|
||||
this.keyPayload = {
|
||||
userId: userId,
|
||||
// 1 minute
|
||||
expiration: 60*1_000_000_000,
|
||||
keyId: keyId,
|
||||
};
|
||||
}
|
||||
|
||||
payload(): RequestBody{
|
||||
const assertion = zitadel.signJWTProfileAssertion(
|
||||
this.keyPayload.userId,
|
||||
this.keyPayload.keyId,
|
||||
{
|
||||
audience: [Config.host],
|
||||
expiration: this.keyPayload.expiration,
|
||||
key: privateKey
|
||||
});
|
||||
return {
|
||||
'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer',
|
||||
scope: 'openid',
|
||||
assertion: `${assertion}`
|
||||
};
|
||||
};
|
||||
public headers(): { [name: string]: string; } {
|
||||
return {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const tokenDurationTrend = new Trend('oidc_token_duration', true);
|
||||
export async function token(request: TokenRequest): Promise<Tokens> {
|
||||
return http.asyncRequest('POST', configuration().token_endpoint,
|
||||
request.payload(),
|
||||
{
|
||||
headers: request.headers(),
|
||||
},
|
||||
).then((res) => {
|
||||
tokenDurationTrend.add(res.timings.duration);
|
||||
check(res, {
|
||||
'token status ok': (r) => r.status === 200,
|
||||
'access token returned': (r) => r.json('access_token')! != undefined && r.json('access_token')! != '',
|
||||
});
|
||||
return new Tokens(res.json() as JSONObject);
|
||||
});
|
||||
};
|
57
load-test/src/use_cases/machine_jwt_profile_grant.ts
Normal file
57
load-test/src/use_cases/machine_jwt_profile_grant.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import { loginByUsernamePassword } from '../login_ui';
|
||||
import { createOrg, removeOrg } from '../org';
|
||||
import {createMachine, User, addMachineKey} from '../user';
|
||||
import {JWTProfileRequest, token, userinfo} from '../oidc';
|
||||
import { Config, MaxVUs } from '../config';
|
||||
import encoding from 'k6/encoding';
|
||||
|
||||
const publicKey = encoding.b64encode(open('../.keys/key.pem.pub'));
|
||||
|
||||
export async function setup() {
|
||||
const tokens = loginByUsernamePassword(Config.admin as User);
|
||||
console.info('setup: admin signed in');
|
||||
|
||||
const org = await createOrg(tokens.accessToken!);
|
||||
console.info(`setup: org (${org.organizationId}) created`);
|
||||
|
||||
let machines = (
|
||||
await Promise.all(
|
||||
Array.from({ length: MaxVUs() }, (_, i) => {
|
||||
return createMachine(`zitachine-${i}`, org, tokens.accessToken!);
|
||||
}),
|
||||
)
|
||||
).map((machine) => {
|
||||
return { userId: machine.userId, loginName: machine.loginNames[0] };
|
||||
});
|
||||
console.info(`setup: ${machines.length} machines created`);
|
||||
|
||||
let keys = (
|
||||
await Promise.all(
|
||||
machines.map((machine) => {
|
||||
return addMachineKey(
|
||||
machine.userId,
|
||||
org,
|
||||
tokens.accessToken!,
|
||||
publicKey,
|
||||
);
|
||||
}),
|
||||
)
|
||||
).map((key, i) => {
|
||||
return { userId: machines[i].userId, keyId: key.keyId };
|
||||
});
|
||||
console.info(`setup: ${keys.length} keys added`);
|
||||
|
||||
return { tokens, machines: keys, org };
|
||||
}
|
||||
|
||||
export default function (data: any) {
|
||||
token(new JWTProfileRequest(data.machines[__VU - 1].userId, data.machines[__VU - 1].keyId))
|
||||
.then((token) => {
|
||||
userinfo(token.accessToken!)
|
||||
})
|
||||
}
|
||||
|
||||
export function teardown(data: any) {
|
||||
removeOrg(data.org, data.tokens.accessToken);
|
||||
console.info('teardown: org removed');
|
||||
}
|
@ -197,6 +197,38 @@ export function addMachineSecret(userId: string, org: Org, accessToken: string):
|
||||
});
|
||||
}
|
||||
|
||||
export type MachineKey = {
|
||||
keyId: string;
|
||||
};
|
||||
|
||||
const addMachineKeyTrend = new Trend('user_add_machine_key_duration', true);
|
||||
export function addMachineKey(userId: string, org: Org, accessToken: string, publicKey?: string): Promise<MachineKey> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let response = http.asyncRequest('POST', url(`/management/v1/users/${userId}/keys`),
|
||||
JSON.stringify({
|
||||
type: 'KEY_TYPE_JSON',
|
||||
userId: userId,
|
||||
// base64 encoded public key
|
||||
publicKey: publicKey
|
||||
}),
|
||||
{
|
||||
headers: {
|
||||
authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
'x-zitadel-orgid': org.organizationId,
|
||||
},
|
||||
});
|
||||
response.then((res) => {
|
||||
check(res, {
|
||||
'generate machine key status ok': (r) => r.status === 200,
|
||||
}) || reject(`unable to generate machine Key (user id: ${userId}) status: ${res.status} body: ${res.body}`);
|
||||
|
||||
addMachineKeyTrend.add(res.timings.duration);
|
||||
resolve(res.json()! as MachineKey);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const lockUserTrend = new Trend('lock_user_duration', true);
|
||||
export function lockUser(userId: string, org: Org, accessToken: string): Promise<RefinedResponse<any>> {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
@ -32,14 +32,6 @@ module.exports = {
|
||||
},
|
||||
plugins: [
|
||||
new CleanWebpackPlugin(),
|
||||
// Copy assets to the destination folder
|
||||
// see `src/post-file-test.ts` for an test example using an asset
|
||||
new CopyPlugin({
|
||||
patterns: [{
|
||||
from: path.resolve(__dirname, 'assets'),
|
||||
noErrorOnMissing: true
|
||||
}],
|
||||
}),
|
||||
],
|
||||
optimization: {
|
||||
// Don't minimize, as it's not used in the browser
|
||||
|
Loading…
Reference in New Issue
Block a user