diff --git a/load-test/Makefile b/load-test/Makefile index 4d2e9aa18..106b27281 100644 --- a/load-test/Makefile +++ b/load-test/Makefile @@ -12,6 +12,10 @@ human_password_login: bundle machine_pat_login: bundle k6 run 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} + .PHONY: user_info user_info: bundle k6 run dist/user_info.js --vus ${VUS} --duration ${DURATION} @@ -26,6 +30,10 @@ introspect: ensure_modules bundle cd ../../xk6-modules && xk6 build --with xk6-zitadel=. ./../../xk6-modules/k6 run dist/introspection.js --vus ${VUS} --duration ${DURATION} +.PHONY: add_session +add_session: bundle + k6 run dist/session.js --vus ${VUS} --duration ${DURATION} + .PHONY: lint lint: npm i diff --git a/load-test/README.md b/load-test/README.md index 6e1542da4..18705a897 100644 --- a/load-test/README.md +++ b/load-test/README.md @@ -36,6 +36,9 @@ Before you run the tests you need an initialized user. The tests don't implement * `make machine_pat_login` setup: creates machines and a pat for each machine test: calls user info endpoint with the given pats +* `make machine_client_credentials_login` + setup: creates machines and a client credential secret for each machine + test: calls token endpoint with the `client_credentials` grant type. * `make user_info` setup: creates human users and signs them in test: calls user info endpoint using the given humans @@ -43,4 +46,7 @@ Before you run the tests you need an initialized user. The tests don't implement test: creates a human, updates its profile, locks the user and then deletes it * `make introspect` setup: creates projects, one api per project, one key per api and generates the jwt from the given keys - test: calls introspection endpoint using the given JWTs \ No newline at end of file + test: calls introspection endpoint using the given JWTs +* `make add_session` + setup: creates human users + test: creates new sessions with user id check \ No newline at end of file diff --git a/load-test/src/oidc.ts b/load-test/src/oidc.ts index 0f09fba96..a9173bfce 100644 --- a/load-test/src/oidc.ts +++ b/load-test/src/oidc.ts @@ -72,3 +72,35 @@ export function introspect(jwt: string, token: string) { introspectTrend.add(res.timings.duration); } + +const clientCredentialsTrend = new Trend('oidc_client_credentials_duration', true); +export function clientCredentials(clientId: string, clientSecret: string): Promise { + return new Promise((resolve, reject) => { + const response = http.asyncRequest('POST', configuration().token_endpoint, + { + grant_type: "client_credentials", + scope: 'openid profile urn:zitadel:iam:org:project:id:zitadel:aud', + client_id: clientId, + client_secret: clientSecret, + }, + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }, + ); + response.then((res) => { + check(res, { + 'client credentials status ok': (r) => r.status === 200, + }) || reject(`client credentials request failed (client id: ${clientId}) status: ${res.status} body: ${res.body}`); + + clientCredentialsTrend.add(res.timings.duration); + const tokens = new Tokens(res.json() as JSONObject) + check(tokens, { + 'client credentials token ok': (t) => t.accessToken !== undefined, + }) || reject(`client credentials access token missing (client id: ${clientId}`); + + resolve(tokens) + }); + }); +} \ No newline at end of file diff --git a/load-test/src/session.ts b/load-test/src/session.ts new file mode 100644 index 000000000..28a994c97 --- /dev/null +++ b/load-test/src/session.ts @@ -0,0 +1,42 @@ +import { Trend } from 'k6/metrics'; +import { Org } from './org'; +import http from 'k6/http'; +import url from './url'; +import { check } from 'k6'; +import { User } from './user'; + +export type Session = { + id: string; +}; + +const addSessionTrend = new Trend('session_add_session_duration', true); +export function createSession(user: User, org: Org, accessToken: string): Promise { + return new Promise((resolve, reject) => { + let response = http.asyncRequest( + 'POST', + url('/v2beta/sessions'), + JSON.stringify({ + checks: { + user: { + userId: user.userId, + } + } + }), + { + headers: { + authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + 'x-zitadel-orgid': org.organizationId, + }, + }, + ); + response.then((res) => { + check(res, { + 'add Session status ok': (r) => r.status === 201, + }) || reject(`unable to add Session status: ${res.status} body: ${res.body}`); + + addSessionTrend.add(res.timings.duration); + resolve(res.json() as Session); + }); + }); +} diff --git a/load-test/src/use_cases/machine_client_credentials_login.ts b/load-test/src/use_cases/machine_client_credentials_login.ts new file mode 100644 index 000000000..a2cbcca8d --- /dev/null +++ b/load-test/src/use_cases/machine_client_credentials_login.ts @@ -0,0 +1,49 @@ +import { loginByUsernamePassword } from '../login_ui'; +import { createOrg, removeOrg } from '../org'; +import {createMachine, User, addMachineSecret} from '../user'; +import {clientCredentials, userinfo} from '../oidc'; +import { Config, MaxVUs } from '../config'; + +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 credentials = ( + await Promise.all( + machines.map((machine) => { + return addMachineSecret(machine.userId, org, tokens.accessToken!); + }), + ) + ).map((credentials, i) => { + return { userId: machines[i].userId, loginName: machines[i].loginName, password: credentials.clientSecret }; + }); + console.info(`setup: secrets added`); + + return { tokens, machines: credentials, org }; +} + +export default function (data: any) { + clientCredentials(data.machines[__VU - 1].loginName, data.machines[__VU - 1].password) + .then((token) => { + userinfo(token.accessToken!) + }) +} + +export function teardown(data: any) { + removeOrg(data.org, data.tokens.accessToken); + console.info('teardown: org removed'); +} diff --git a/load-test/src/use_cases/session.ts b/load-test/src/use_cases/session.ts new file mode 100644 index 000000000..d9cdfbd3b --- /dev/null +++ b/load-test/src/use_cases/session.ts @@ -0,0 +1,41 @@ +import { loginByUsernamePassword } from '../login_ui'; +import { createOrg, removeOrg } from '../org'; +import { User, createHuman } from '../user'; +import { Trend } from 'k6/metrics'; +import { Config, MaxVUs } from '../config'; +import { createSession } from '../session'; +import { check } from 'k6'; + +export async function setup() { + const tokens = loginByUsernamePassword(Config.admin as User); + console.log('setup: admin signed in'); + + const org = await createOrg(tokens.accessToken!); + console.log(`setup: org (${org.organizationId}) created`); + + const humanPromises = Array.from({ length: MaxVUs() }, (_, i) => { + return createHuman(`zitizen-${i}`, org, tokens.accessToken!); + }); + + const humans = (await Promise.all(humanPromises)).map((user) => { + return { userId: user.userId, loginName: user.loginNames[0], password: 'Password1!' }; + }); + console.log(`setup: ${humans.length} users created`); + return { tokens, users: humans, org }; +} + +const addSessionTrend = new Trend('add_session_duration', true); +export default async function (data: any) { + const start = new Date(); + const session = await createSession(data.users[__VU - 1], data.org, data.tokens.accessToken); + + check(session, { + 'add session is status ok': (s) => s.id !== "", + }); + + addSessionTrend.add(new Date().getTime() - start.getTime()); +} + +export function teardown(data: any) { + removeOrg(data.org, data.tokens.accessToken); +} diff --git a/load-test/src/user.ts b/load-test/src/user.ts index a4abd0932..21214b16a 100644 --- a/load-test/src/user.ts +++ b/load-test/src/user.ts @@ -171,6 +171,32 @@ export function addMachinePat(userId: string, org: Org, accessToken: string): Pr }); } +export type MachineSecret = { + clientId: string; + clientSecret: string; +}; + +const addMachineSecretTrend = new Trend('user_add_machine_secret_duration', true); +export function addMachineSecret(userId: string, org: Org, accessToken: string): Promise { + return new Promise((resolve, reject) => { + let response = http.asyncRequest('PUT', url(`/management/v1/users/${userId}/secret`), null, { + headers: { + authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + 'x-zitadel-orgid': org.organizationId, + }, + }); + response.then((res) => { + check(res, { + 'generate machine secret status ok': (r) => r.status === 200, + }) || reject(`unable to generate machine secret (user id: ${userId}) status: ${res.status} body: ${res.body}`); + + addMachineSecretTrend.add(res.timings.duration); + resolve(res.json()! as MachineSecret); + }); + }); +} + const lockUserTrend = new Trend('lock_user_duration', true); export function lockUser(userId: string, org: Org, accessToken: string): Promise> { return new Promise((resolve, reject) => {