chore: rehaul DevX (#10571)

# Which Problems Are Solved

Replaces Turbo by Nx and lays the foundation for the next CI
improvements. It enables using Nx Cloud to speed the up the pipelines
that affect any node package.
It streamlines the dev experience for frontend and backend developers by
providing the following commands:

| Task | Command | Notes |
|------|---------|--------|
| **Production** | `nx run PROJECT:prod` | Production server |
| **Develop** | `nx run PROJECT:dev` | Hot reloading development server
|
| **Test** | `nx run PROJECT:test` | Run all tests |
| **Lint** | `nx run PROJECT:lint` | Check code style |
| **Lint Fix** | `nx run PROJECT:lint-fix` | Auto-fix style issues |

The following values can be used for PROJECT:

- @zitadel/zitadel (root commands)
- @zitadel/api,
- @zitadel/login,
- @zitadel/console,
- @zitadel/docs,
- @zitadel/client
- @zitadel/proto

The project names and folders are streamlined:

| Old Folder | New Folder |
| --- | --- |
| ./e2e | ./tests/functional-ui |
| ./load-test | ./benchmark |
| ./build/zitadel | ./apps/api |
| ./console | ./apps/console (postponed so the PR is reviewable) |  

Also, all references to the TypeScript repo are removed so we can
archive it.

# How the Problems Are Solved

- Ran `npx nx@latest init`
- Replaced all turbo.json by project.json and fixed the target configs
- Removed Turbo dependency
- All JavaScript related code affected by a PRs changes is
quality-checked using the `nx affected` command
- We move PR checks that are runnable using Nx into the `check`
workflow. For workflows where we don't use Nx, yet, we restore
previously built dependency artifacts from Nx.
- We only use a single and easy to understand dev container
- The CONTRIBUTING.md is streamlined
- The setup with a generated client pat is orchestrated with Nx
- Everything related to the TypeScript repo is updated or removed. A
**Deploy with Vercel** button is added to the docs and the
CONTRIBUTING.md.

# Additional Changes

- NPM package names have a consistent pattern.
- Docker bake is removed. The login container is built and released like
the core container.
- The integration tests build the login container before running, so
they don't rely on the login container action anymore. This fixes
consistently failing checks on PRs from forks.
- The docs build in GitHub actions is removed, as we already build on
Vercel.

# Additional Context

- Internal discussion:
https://zitadel.slack.com/archives/C087ADF8LRX/p1756277884928169
- Workflow dispatch test:
https://github.com/zitadel/zitadel/actions/runs/17760122959

---------

Co-authored-by: Florian Forster <[email protected]>
Co-authored-by: Tim Möhlmann <[email protected]>
Co-authored-by: Copilot <[email protected]>
This commit is contained in:
Elio Bischof
2025-10-08 10:27:02 +02:00
committed by GitHub
co-authored by Florian Forster Tim Möhlmann Copilot
parent e47acaeef2
commit f69a6ed4f3
236 changed files with 5708 additions and 6549 deletions
+70
View File
@@ -0,0 +1,70 @@
import { Trend } from 'k6/metrics';
import { Org } from './org';
import http from 'k6/http';
import url from './url';
import { check } from 'k6';
export type API = {
appId: string;
};
const addAPITrend = new Trend('app_add_app_duration', true);
export function createAPI(name: string, projectId: string, org: Org, accessToken: string): Promise<API> {
return new Promise((resolve, reject) => {
let response = http.asyncRequest(
'POST',
url(`/management/v1/projects/${projectId}/apps/api`),
JSON.stringify({
name: name,
authMethodType: 'API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT',
}),
{
headers: {
authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'x-zitadel-orgid': org.organizationId,
},
},
);
response.then((res) => {
check(res, {
'add api status ok': (r) => r.status >= 200 && r.status < 300,
}) || reject(`unable to add api project: ${projectId} status: ${res.status} body: ${res.body}`);
resolve(res.json() as API);
addAPITrend.add(res.timings.duration);
});
});
}
export type AppKey = {
keyDetails: string;
};
const addAppKeyTrend = new Trend('app_add_app_key_duration', true);
export function createAppKey(appId: string, projectId: string, org: Org, accessToken: string): Promise<AppKey> {
return new Promise((resolve, reject) => {
let response = http.asyncRequest(
'POST',
url(`/management/v1/projects/${projectId}/apps/${appId}/keys`),
JSON.stringify({
type: 'KEY_TYPE_JSON',
}),
{
headers: {
authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'x-zitadel-orgid': org.organizationId,
},
},
);
response.then((res) => {
check(res, {
'add app key status ok': (r) => r.status >= 200 && r.status < 300,
}) || reject(`unable to add app key project: ${projectId} app: ${appId} status: ${res.status} body: ${res.body}`);
resolve(res.json() as AppKey);
addAppKeyTrend.add(res.timings.duration);
});
});
}
+67
View File
@@ -0,0 +1,67 @@
// @ts-ignore Import module
import { randomString } from 'https://jslib.k6.io/k6-utils/1.2.0/index.js';
import crypto from 'k6/crypto';
import http from 'k6/http';
import execution from 'k6/execution';
import { Stage } from 'k6/options';
import url from './url';
export const Config = {
host: __ENV.ZITADEL_HOST || 'http://localhost:8080',
orgId: '',
codeVerifier: __ENV.CODE_VERIFIER || randomString(10),
admin: {
loginName: __ENV.ADMIN_LOGIN_NAME || '[email protected]',
password: __ENV.ADMIN_PASSWORD || 'Password1!',
},
};
const client = {
response_type: 'code',
scope: 'openid email profile urn:zitadel:iam:org:project:id:zitadel:aud',
prompt: 'login',
code_challenge_method: 'S256',
code_challenge: crypto.sha256(Config.codeVerifier, 'base64rawurl'),
client_id: __ENV.CLIENT_ID || '',
redirect_uri: url('/ui/console/auth/callback'),
};
export function Client() {
if (client.client_id) {
return client;
}
const env = http.get(url('/ui/console/assets/environment.json'));
client.client_id = env.json('clientid') ? env.json('clientid')?.toString()! : '';
return client;
}
let maxVUs: number;
export function MaxVUs() {
if (maxVUs != undefined) {
return maxVUs;
}
let max: number = execution.test.options.stages
? execution.test.options.stages
.map((value: Stage): number => value.target)
.reduce((acc: number, value: number): number => {
return acc <= value ? acc : value;
})
: 1;
if (execution.test.options.scenarios) {
new Map(Object.entries(execution.test.options.scenarios)).forEach((value) => {
if ('vus' in value) {
max = value.vus && max < value.vus ? value.vus : max;
} else if ('maxVUs' in value) {
max = value.maxVUs && max < value.maxVUs ? value.maxVUs : max;
}
});
}
maxVUs = max;
return maxVUs;
}
+131
View File
@@ -0,0 +1,131 @@
import { JSONObject, check, fail } from 'k6';
import http, { Response } from 'k6/http';
// @ts-ignore Import module
import { URL } from 'https://jslib.k6.io/url/1.0.0/index.js';
import { Trend } from 'k6/metrics';
import { Config, Client } from './config';
import url from './url';
import { User } from './user';
import { Tokens } from './oidc';
export function loginByUsernamePassword(user: User) {
check(user, {
'user defined': (u) => u !== undefined || fail(`user is undefined`),
});
const loginUI = initLogin();
const loginNameResponse = enterLoginName(loginUI, user);
const passwordResponse = enterPassword(loginNameResponse, user);
return token(new URL(passwordResponse.url).searchParams.get('code'));
}
const initLoginTrend = new Trend('login_ui_init_login_duration', true);
export function initLogin(clientId?: string): Response {
let params = {};
let expectedStatus = 200;
if (clientId) {
params = {
headers: {
'x-zitadel-login-client': clientId,
},
redirects: 0,
};
expectedStatus = 302;
}
const response = http.get(
url('/oauth/v2/authorize', {
searchParams: Client(),
}),
params,
);
check(response, {
'authorize status ok': (r) => r.status == expectedStatus || fail(`init login failed: ${JSON.stringify(r)}`),
});
initLoginTrend.add(response.timings.duration);
return response;
}
const enterLoginNameTrend = new Trend('login_ui_enter_login_name_duration', true);
function enterLoginName(page: Response, user: User): Response {
const response = page.submitForm({
formSelector: 'form',
fields: {
loginName: user.loginName,
},
});
check(response, {
'login name status ok': (r) => (r && r.status >= 200 && r.status < 300) || fail('enter login name failed'),
'login shows password page': (r) => r && r.body !== null && r.body.toString().includes('password'),
// 'login has no error': (r) => r && r.body != null && r.body.toString().includes('error') || fail(`error in enter login name ${r.body}`)
});
enterLoginNameTrend.add(response.timings.duration);
return response;
}
const enterPasswordTrend = new Trend('login_ui_enter_password_duration', true);
function enterPassword(page: Response, user: User): Response {
let response = page.submitForm({
formSelector: 'form',
fields: {
password: user.password,
},
});
enterPasswordTrend.add(response.timings.duration);
// skip 2fa init
if (response.url.endsWith('/password')) {
response = response.submitForm({
formSelector: 'form',
submitSelector: '[name="skip"]',
});
}
check(response, {
'password status ok': (r) => (r.status >= 200 && r.status < 300) || fail('enter password failed'),
'password callback': (r) =>
r.url.startsWith(url('/ui/console/auth/callback?code=')) || fail(`wrong password callback: ${r.url}`),
});
return response;
}
const tokenTrend = new Trend('login_ui_token_duration', true);
function token(code = '') {
check(code, {
'code set': (c) => (c !== undefined && c !== null) || fail('code was not set'),
});
const response = http.post(
url('/oauth/v2/token'),
{
grant_type: 'authorization_code',
code: code,
redirect_uri: Client().redirect_uri,
code_verifier: Config.codeVerifier,
client_id: Client().client_id,
},
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
},
);
tokenTrend.add(response.timings.duration);
check(response, {
'token status ok': (r) =>
(r.status >= 200 && r.status < 300) || fail(`invalid token response status: ${r.status} body: ${r.body}`),
});
const token = new Tokens(response.json() as JSONObject);
check(token, {
'access token created': (t) => t.accessToken !== undefined,
'id token created': (t) => t.idToken !== undefined,
'info created': (t) => t.info !== undefined,
});
return token;
}
+26
View File
@@ -0,0 +1,26 @@
import http from 'k6/http';
import { Trend } from 'k6/metrics';
import url from './url';
import { check, fail } from 'k6';
const addIAMMemberTrend = new Trend('membership_iam_member', true);
export async function addIAMMember(userId: string, roles: string[], accessToken: string): Promise<void> {
const res = await http.post(
url('/admin/v1/members'),
JSON.stringify({
userId: userId,
roles: roles,
}),
{
headers: {
authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
},
);
check(res, {
'member added successful': (r) =>
(r.status >= 200 && r.status < 300) || fail(`unable add member: ${JSON.stringify(res)}`),
});
addIAMMemberTrend.add(res.timings.duration);
}
+4
View File
@@ -0,0 +1,4 @@
export type Metadata = {
key: string;
value: string;
};
+212
View File
@@ -0,0 +1,212 @@
import { JSONObject, check, fail } from 'k6';
import encoding from 'k6/encoding';
import http, { RequestBody, Response } from 'k6/http';
import { Trend } from 'k6/metrics';
import url from './url';
import { Client, Config } from './config';
// @ts-ignore Import module
import zitadel from 'k6/x/zitadel';
export class Tokens {
idToken?: string;
accessToken?: string;
info?: any;
constructor(res: JSONObject) {
this.idToken = res.id_token ? res.id_token!.toString() : undefined;
this.accessToken = res.access_token ? res.access_token!.toString() : undefined;
this.info = this.idToken
? JSON.parse(encoding.b64decode(this.idToken?.split('.')[1].toString(), 'rawstd', 's'))
: undefined;
}
}
let oidcConfig: any | undefined;
function configuration() {
if (oidcConfig !== undefined) {
return oidcConfig;
}
const res = http.get(url('/.well-known/openid-configuration'));
check(res, {
'openid configuration': (r) => (r.status >= 200 && r.status < 300) || fail('unable to load openid configuration'),
});
oidcConfig = res.json();
return oidcConfig;
}
const userinfoTrend = new Trend('oidc_user_info_duration', true);
export function userinfo(token: string) {
const userinfo = http.get(configuration().userinfo_endpoint, {
headers: {
authorization: 'Bearer ' + token,
'Content-Type': 'application/json',
},
});
check(userinfo, {
'userinfo status ok': (r) => r.status >= 200 && r.status < 300,
});
userinfoTrend.add(userinfo.timings.duration);
}
const introspectTrend = new Trend('oidc_introspect_duration', true);
export function introspect(jwt: string, token: string) {
const res = http.post(
configuration().introspection_endpoint,
{
client_assertion: jwt,
token: token,
client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
},
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
alg: 'RS256',
},
},
);
check(res, {
'introspect status ok': (r) => r.status >= 200 && r.status < 300,
});
introspectTrend.add(res.timings.duration);
}
const clientCredentialsTrend = new Trend('oidc_client_credentials_duration', true);
export function clientCredentials(clientId: string, clientSecret: string): Promise<Tokens> {
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 && r.status < 300,
}) || 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);
});
});
}
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 urn:zitadel:iam:org:project:id:zitadel:aud',
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 && r.status < 300,
'access token returned': (r) => r.json('access_token')! != undefined && r.json('access_token')! != '',
});
return new Tokens(res.json() as JSONObject);
});
}
const authRequestByIDTrend = new Trend('oidc_auth_request_by_id_duration', true);
export async function authRequestByID(id: string, tokens: any): Promise<Response> {
const response = http.get(url(`/v2/oidc/auth_requests/${id}`), {
headers: {
Authorization: `Bearer ${tokens.accessToken}`,
},
});
check(response, {
'authorize status ok': (r) =>
(r.status >= 200 && r.status < 300) || fail(`auth request by failed: ${JSON.stringify(r)}`),
});
authRequestByIDTrend.add(response.timings.duration);
return response;
}
const finalizeAuthRequestTrend = new Trend('oidc_auth_request_finalize', true);
export async function finalizeAuthRequest(id: string, session: any, tokens: any): Promise<Response> {
const res = await http.post(
url(`/v2/oidc/auth_requests/${id}`),
JSON.stringify({
session: {
sessionId: session.sessionId,
sessionToken: session.sessionToken,
},
}),
{
headers: {
Authorization: `Bearer ${tokens.accessToken}`,
'Content-Type': 'application/json',
// 'Accept': 'application/json',
'x-zitadel-login-client': tokens.info.client_id,
},
},
);
check(res, {
'finalize auth request status ok': (r) =>
(r.status >= 200 && r.status < 300) || fail(`finalize auth request failed: ${JSON.stringify(r)}`),
});
finalizeAuthRequestTrend.add(res.timings.duration);
return res;
}
+56
View File
@@ -0,0 +1,56 @@
import http from 'k6/http';
import { Trend } from 'k6/metrics';
import url from './url';
import { Config } from './config';
import { check } from 'k6';
export type Org = {
organizationId: string;
};
const createOrgTrend = new Trend('org_create_org_duration', true);
export function createOrg(accessToken: string): Promise<Org> {
return new Promise((resolve, reject) => {
let response = http.asyncRequest(
'POST',
url('/v2/organizations'),
JSON.stringify({
name: `load-test-${new Date(Date.now()).toISOString()}`,
}),
{
headers: {
authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'x-zitadel-orgid': Config.orgId,
},
},
);
response.then((res) => {
check(res, {
'org created': (r) => {
return r !== undefined && r.status >= 200 && r.status < 300;
},
}) || reject(`unable to create org status: ${res.status} || body: ${res.body}`);
createOrgTrend.add(res.timings.duration);
resolve(res.json() as Org);
});
});
}
export function removeOrg(org: Org, accessToken: string) {
const response = http.del(url('/management/v1/orgs/me'), null, {
headers: {
authorization: `Bearer ${accessToken}`,
'x-zitadel-orgid': org.organizationId,
},
});
check(response, {
'org removed': (r) => r.status >= 200 && r.status < 300,
}) || console.log(`status: ${response.status} || body: ${response.body}|| org: ${JSON.stringify(org)}`);
return response.json();
}
+37
View File
@@ -0,0 +1,37 @@
import { Trend } from 'k6/metrics';
import { Org } from './org';
import http from 'k6/http';
import url from './url';
import { check } from 'k6';
export type Project = {
id: string;
};
const addProjectTrend = new Trend('project_add_project_duration', true);
export function createProject(name: string, org: Org, accessToken: string): Promise<Project> {
return new Promise((resolve, reject) => {
let response = http.asyncRequest(
'POST',
url('/management/v1/projects'),
JSON.stringify({
name: name,
}),
{
headers: {
authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'x-zitadel-orgid': org.organizationId,
},
},
);
response.then((res) => {
check(res, {
'add project status ok': (r) => r.status >= 200 && r.status < 300,
}) || reject(`unable to add project status: ${res.status} body: ${res.body}`);
addProjectTrend.add(res.timings.duration);
resolve(res.json() as Project);
});
});
}
+58
View File
@@ -0,0 +1,58 @@
import { Trend } from 'k6/metrics';
import { Org } from './org';
import http from 'k6/http';
import url from './url';
import { check } from 'k6';
export type Session = {
challenges: any;
id: string;
token: string;
};
const addSessionTrend = new Trend('session_add_session_duration', true);
export function createSession(org: Org, accessToken: string, checks?: any): Promise<Session> {
return new Promise((resolve, reject) => {
let response = http.asyncRequest('POST', url('/v2/sessions'), checks ? JSON.stringify({ checks: checks }) : null, {
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 >= 200 && r.status < 300,
}) || reject(`unable to add Session status: ${res.status} body: ${res.body}`);
addSessionTrend.add(res.timings.duration);
resolve(res.json() as Session);
});
});
}
const setSessionTrend = new Trend('session_set_session_duration', true);
export function setSession(id: string, session: any, accessToken: string, challenges?: any, checks?: any): Promise<Session> {
const body = {
sessionToken: session.sessionToken,
checks: checks ? checks : null,
challenges: challenges ? challenges : null,
};
return new Promise((resolve, reject) => {
let response = http.asyncRequest('PATCH', url(`/v2/sessions/${id}`), JSON.stringify(body), {
headers: {
authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
// 'x-zitadel-orgid': org.organizationId,
},
});
response.then((res) => {
check(res, {
'set Session status ok': (r) => r.status >= 200 && r.status < 300,
}) || reject(`unable to set Session status: ${res.status} body: ${res.body}`);
setSessionTrend.add(res.timings.duration);
resolve(res.json() as Session);
});
});
}
+18
View File
@@ -0,0 +1,18 @@
import { options } from 'k6/http';
import { Config } from './config';
export type options = {
searchParams?: { [name: string]: string };
};
export default function url(path: string, options: options = {}) {
let url = new URL(Config.host + path);
if (options.searchParams) {
Object.entries(options.searchParams).forEach(([key, value]) => {
url.searchParams.append(key, value);
});
}
return url.toString();
}
@@ -0,0 +1,37 @@
import { loginByUsernamePassword } from '../login_ui';
import { createOrg, removeOrg } from '../org';
import { User, createHuman } from '../user';
import { userinfo } from '../oidc';
import { Trend } from 'k6/metrics';
import { Config, MaxVUs } from '../config';
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 humanPasswordLoginTrend = new Trend('human_password_login_duration', true);
export default function (data: any) {
const start = new Date();
const token = loginByUsernamePassword(data.users[__VU - 1]);
userinfo(token.accessToken!);
humanPasswordLoginTrend.add(new Date().getTime() - start.getTime());
}
export function teardown(data: any) {
removeOrg(data.org, data.tokens.accessToken);
}
+54
View File
@@ -0,0 +1,54 @@
import { loginByUsernamePassword } from '../login_ui';
import { createAPI, createAppKey } from '../app';
import { createProject } from '../project';
import { createOrg, removeOrg } from '../org';
import { introspect } from '../oidc';
import { Config, MaxVUs } from '../config';
import { b64decode } from 'k6/encoding';
// @ts-ignore Import module
import zitadel from 'k6/x/zitadel';
import { User } from '../user';
export async function setup() {
const adminTokens = loginByUsernamePassword(Config.admin as User);
console.info('setup: admin signed in');
const org = await createOrg(adminTokens.accessToken!);
console.info(`setup: org (${org.organizationId}) created`);
const projectPromises = Array.from({ length: MaxVUs() }, (_, i) => {
return createProject(`project-${i}`, org, adminTokens.accessToken!);
});
const projects = await Promise.all(projectPromises);
console.log(`setup: ${projects.length} projects created`);
const apis = await Promise.all(
projects.map((project, i) => {
return createAPI(`api-${i}`, project.id, org, adminTokens.accessToken!);
}),
);
console.info(`setup: ${apis.length} apis created`);
const keys = await Promise.all(
apis.map((api, i) => {
return createAppKey(api.appId, projects[i].id, org, adminTokens.accessToken!);
}),
);
console.info(`setup: ${keys.length} keys created`);
const tokens = keys.map((key) => {
return zitadel.jwtFromKey(b64decode(key.keyDetails, 'url', 's'), Config.host);
});
console.info(`setup: ${tokens.length} tokens generated`);
return { adminTokens, tokens, org };
}
export default function (data: any) {
introspect(data.tokens[__VU - 1], data.adminTokens.accessToken);
}
export function teardown(data: any) {
removeOrg(data.org, data.adminTokens.accessToken);
console.info('teardown: org removed');
}
@@ -0,0 +1,48 @@
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');
}
@@ -0,0 +1,49 @@
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));
}
export function teardown(data: any) {
removeOrg(data.org, data.tokens.accessToken);
console.info('teardown: org removed');
}
@@ -0,0 +1,32 @@
import { loginByUsernamePassword } from '../login_ui';
import { createOrg, removeOrg } from '../org';
import { createMachine, User, addMachineKey } from '../user';
import { JWTProfileRequest, token, userinfo } from '../oidc';
import { Config } 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`);
const machine = await createMachine(`zitachine`, org, tokens.accessToken!);
console.info(`setup: machine ${machine.userId} created`);
const key = await addMachineKey(machine.userId, org, tokens.accessToken!, publicKey);
console.info(`setup: key ${key.keyId} added`);
return { tokens, machine: { userId: machine.userId, keyId: key.keyId }, org };
}
export default function (data: any) {
token(new JWTProfileRequest(data.machine.userId, data.machine.keyId));
}
export function teardown(data: any) {
removeOrg(data.org, data.tokens.accessToken);
console.info('teardown: org removed');
}
@@ -0,0 +1,46 @@
import { loginByUsernamePassword } from '../login_ui';
import { createOrg, removeOrg } from '../org';
import { createMachine, addMachinePat, User } from '../user';
import { 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 pats = (
await Promise.all(
machines.map((machine) => {
return addMachinePat(machine.userId, org, tokens.accessToken!);
}),
)
).map((pat, i) => {
return { userId: machines[i].userId, loginName: machines[i].loginName, pat: pat.token };
});
console.info(`setup: Pats added`);
return { tokens, machines: pats, org };
}
export default function (data: any) {
userinfo(data.machines[__VU - 1].pat);
}
export function teardown(data: any) {
removeOrg(data.org, data.tokens.accessToken);
console.info('teardown: org removed');
}
@@ -0,0 +1,47 @@
import { loginByUsernamePassword } from '../login_ui';
import { createOrg, removeOrg } from '../org';
import { createHuman, updateHuman, lockUser, deleteUser, User } from '../user';
import { Config } from '../config';
import { check } from 'k6';
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`);
return { tokens, org };
}
export default async function (data: any) {
const human = await createHuman(`vu-${__VU}-${new Date(Date.now()).getTime()}`, data.org, data.tokens.accessToken);
const updateRes = await updateHuman(
{
profile: {
nickName: `${new Date(Date.now()).toISOString()}`,
},
},
human.userId,
data.org,
data.tokens.accessToken,
);
check(updateRes, {
'update user is status ok': (r) => r.status >= 200 && r.status < 300,
});
const lockRes = await lockUser(human.userId, data.org, data.tokens.accessToken);
check(lockRes, {
'lock user is status ok': (r) => r.status >= 200 && r.status < 300,
});
const deleteRes = await deleteUser(human.userId, data.org, data.tokens.accessToken);
check(deleteRes, {
'delete user is status ok': (r) => r.status >= 200 && r.status < 300,
});
}
export function teardown(data: any) {
removeOrg(data.org, data.tokens.accessToken);
console.info('teardown: org removed');
}
@@ -0,0 +1,45 @@
import { loginByUsernamePassword } from '../../login_ui';
import { createOrg, removeOrg } from '../../org';
import { User, createHuman, createMachine } 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.org, data.tokens.accessToken, {
user: {
userId: data.users[__VU - 1].userId,
},
});
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);
}
@@ -0,0 +1,53 @@
import { loginByUsernamePassword, initLogin } from '../../login_ui';
import { createOrg, removeOrg } from '../../org';
import { User, addMachineKey, createMachine } from '../../user';
import { Trend } from 'k6/metrics';
import { Config } from '../../config';
import { check } from 'k6';
import { finalizeAuthRequest, JWTProfileRequest, token } from '../../oidc';
import { createSession } from '../../session';
import encoding from 'k6/encoding';
import { addIAMMember } from '../../membership';
const publicKey = encoding.b64encode(open('../.keys/key.pem.pub'));
export async function setup() {
const adminTokens = loginByUsernamePassword(Config.admin as User);
console.log('setup: admin signed in');
const org = await createOrg(adminTokens.accessToken!);
console.log(`setup: org (${org.organizationId}) created`);
const loginUser = await createMachine('load-test', org, adminTokens.accessToken!);
const loginUserKey = await addMachineKey(loginUser.userId, org, adminTokens.accessToken!, publicKey);
await addIAMMember(loginUser.userId, ['IAM_LOGIN_CLIENT'], adminTokens.accessToken!);
const tokens = await token(new JWTProfileRequest(loginUser.userId, loginUserKey.keyId));
return { tokens, user: loginUser, key: loginUserKey, org, adminTokens };
}
// implements the flow described in
// https://zitadel.com/docs/guides/integrate/login-ui/oidc-standard
const addSessionTrend = new Trend('oidc_session_duration', true);
export default async function (data: any) {
const start = new Date();
const authorizeResponse = initLogin(data.tokens.info.client_id);
const authRequestId = new URLSearchParams(authorizeResponse.headers['Location']).values().next().value;
check(authRequestId, {
'auth request id returned': (s) => s !== '',
});
const session = await createSession(data.org, data.tokens.accessToken, {
user: {
userId: data.user.userId,
},
});
await finalizeAuthRequest(authRequestId!, session, data.tokens!);
addSessionTrend.add(new Date().getTime() - start.getTime());
}
export function teardown(data: any) {
removeOrg(data.org, data.adminTokens.accessToken);
}
@@ -0,0 +1,57 @@
import { loginByUsernamePassword } from '../../login_ui';
import { createOrg, removeOrg } from '../../org';
import { createHuman, setEmailOTPOnHuman, User } from '../../user';
import { Trend } from 'k6/metrics';
import { Config, MaxVUs } from '../../config';
import { createSession, setSession } from '../../session';
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) => {
setEmailOTPOnHuman(user, org, tokens.accessToken!);
return { userId: user.userId, loginName: user.loginNames[0], password: 'Password1!' };
});
console.log(`setup: ${humans.length} users created`);
return { tokens, org, users: humans };
}
// implements the flow described in
// https://zitadel.com/docs/guides/integrate/login-ui/oidc-standard
const otpSessionTrend = new Trend('otp_session_duration', true);
export default async function (data: any) {
const start = new Date();
let session = await createSession(data.org, data.tokens.accessToken, {
user: {
loginName: data.users[__VU - 1].loginName,
},
});
const sessionId = (session as any).sessionId;
session = await setSession(sessionId, session, data.tokens.accessToken, {
otpEmail: {
return_code: {},
},
});
session = await setSession(sessionId, session, data.tokens.accessToken, null, {
otpEmail: {
code: session.challenges.otpEmail,
},
});
otpSessionTrend.add(new Date().getTime() - start.getTime());
}
export function teardown(data: any) {
removeOrg(data.org, data.tokens.accessToken);
}
@@ -0,0 +1,49 @@
import { loginByUsernamePassword } from '../../login_ui';
import { createOrg, removeOrg } from '../../org';
import { createHuman, setEmailOTPOnHuman, User } from '../../user';
import { Trend } from 'k6/metrics';
import { Config, MaxVUs } from '../../config';
import { createSession, setSession } from '../../session';
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) => {
setEmailOTPOnHuman(user, org, tokens.accessToken!);
return { userId: user.userId, loginName: user.loginNames[0], password: 'Password1!' };
});
console.log(`setup: ${humans.length} users created`);
return { tokens, org, users: humans };
}
const passwordSessionTrend = new Trend('password_session_duration', true);
export default async function (data: any) {
const start = new Date();
let session = await createSession(data.org, data.tokens.accessToken, {
user: {
loginName: data.users[__VU - 1].loginName,
},
});
const sessionId = (session as any).sessionId;
session = await setSession(sessionId, session, data.tokens.accessToken, null, {
password: {
password: data.users[__VU - 1].password,
},
});
passwordSessionTrend.add(new Date().getTime() - start.getTime());
}
export function teardown(data: any) {
removeOrg(data.org, data.tokens.accessToken);
}
+27
View File
@@ -0,0 +1,27 @@
import { loginByUsernamePassword } from '../login_ui';
import { userinfo } from '../oidc';
import { Config } from '../config';
import { User, createHuman } from '../user';
import { createOrg, removeOrg } from '../org';
export async function setup() {
const adminTokens = loginByUsernamePassword(Config.admin as User);
console.info('setup: admin signed in');
const org = await createOrg(adminTokens.accessToken!);
console.info(`setup: org (${org.organizationId}) created`);
const user = await createHuman('gigi', org, adminTokens.accessToken!);
console.info(`setup: user (${user.userId}) created`);
return { org, tokens: loginByUsernamePassword({ loginName: user.loginNames[0], password: 'Password1!' } as User) };
}
export default function (data: any) {
userinfo(data.tokens.accessToken);
}
export function teardown(data: any) {
removeOrg(data.org, data.tokens.accessToken);
console.info('teardown: org removed');
}
@@ -0,0 +1,65 @@
import { loginByUsernamePassword } from '../login_ui';
import { createOrg, removeOrg } from '../org';
import { createHuman, User, createMachine, setUserMetadata, listUsers } from '../user';
import { Config } from '../config';
import { check } from 'k6';
import encoding from 'k6/encoding';
const userAmount = parseInt(__ENV.USER_AMOUNT) || 2500;
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`);
const users: User[] = [];
await Promise.all(
Array.from({ length: userAmount }, async (_, i) => {
let user: User;
let type: 'human' | 'machine';
if (i % 2 === 0) {
user = await createHuman(`zitizen-${i}`, org, tokens.accessToken!);
type = 'human';
} else {
user = await createMachine(`zitachine-${i}`, org, tokens.accessToken!);
type = 'machine';
}
users.push(user);
await setUserMetadata(
[
{ key: 'type', value: encoding.b64encode(type, 'rawurl') },
{ key: 'org', value: encoding.b64encode(org.organizationId, 'rawurl') },
{ key: 'id', value: encoding.b64encode(user.userId, 'rawurl') },
],
user.userId,
tokens.accessToken!,
);
if (i % 10 === 0) {
console.log(`setup: ${i} of ${userAmount} users setup`);
}
}),
);
console.info(`setup: ${users.length} users created`);
return { tokens, org, users };
}
export default async function (data: any) {
const result = await listUsers(
{
queries: [{ metadataKeyFilter: { key: 'org', method: 'TEXT_FILTER_METHOD_EQUALS' } }],
},
data.tokens.accessToken!,
);
check(result, {
'total result length': (res) => res.details.totalResult == userAmount,
}) || console.log(`unexpected amount of users. expected ${userAmount} but got ${result.details.totalResult}`);
}
export function teardown(data: any) {
removeOrg(data.org, data.tokens.accessToken);
console.info('teardown: org removed');
}
@@ -0,0 +1,67 @@
import { loginByUsernamePassword } from '../login_ui';
import { createOrg, removeOrg } from '../org';
import { createHuman, User, createMachine, setUserMetadata, listUsers } from '../user';
import { Config } from '../config';
import { check } from 'k6';
import encoding from 'k6/encoding';
const userAmount = parseInt(__ENV.USER_AMOUNT) || 2500;
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`);
const users: User[] = [];
await Promise.all(
Array.from({ length: userAmount }, async (_, i) => {
let user: User;
let type: 'human' | 'machine';
if (i % 2 === 0) {
user = await createHuman(`zitizen-${i}`, org, tokens.accessToken!);
type = 'human';
} else {
user = await createMachine(`zitachine-${i}`, org, tokens.accessToken!);
type = 'machine';
}
users.push(user);
await setUserMetadata(
[
{ key: 'type', value: encoding.b64encode(type, 'rawurl') },
{ key: 'org', value: encoding.b64encode(org.organizationId, 'rawurl') },
{ key: 'id', value: encoding.b64encode(user.userId, 'rawurl') },
],
user.userId,
tokens.accessToken!,
);
if (i % 10 === 0) {
console.log(`setup: ${i} of ${userAmount} users setup`);
}
}),
);
console.info(`setup: ${users.length} users created`);
return { tokens, org, users };
}
export default async function (data: any) {
const result = await listUsers(
{
queries: [
{ metadataValueFilter: { value: encoding.b64encode('human', 'rawurl'), method: 'BYTE_FILTER_METHOD_EQUALS' } },
],
},
data.tokens.accessToken!,
);
check(result, {
'total result length': (res) => res.details.totalResult == userAmount / 2,
}) || console.log(`unexpected amount of users. expected ${userAmount / 2} but got ${result.details.totalResult}`);
}
export function teardown(data: any) {
removeOrg(data.org, data.tokens.accessToken);
console.info('teardown: org removed');
}
@@ -0,0 +1,77 @@
import { loginByUsernamePassword } from '../login_ui';
import { createOrg } from '../org';
import { User, createMachine } from '../user';
import { Config, MaxVUs } from '../config';
import { createProject, Project } from '../project';
import { addUserGrant } from '../user_grant';
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`);
const projects = await Promise.all(
Array.from({ length: 50 }, (_, i) => {
return createProject(`project-${i}`, org, tokens.accessToken!);
}),
);
console.log(`setup: ${projects.length} projects created`);
let machines = (
await Promise.all(
Array.from({ length: MaxVUs() }, async (_, i) => {
return await createMachine(`zitachine-${i}`, org, tokens.accessToken!);
}),
)
).map((machine) => {
return { userId: machine.userId, loginName: machine.loginNames[0] };
});
console.log(`setup: ${machines.length} machines created`);
return { tokens, org, machines, projects };
}
export default async function (data: any) {
const machine = await createMachine(`zitachine-${__VU}-${__ITER}`, data.org, data.tokens.accessToken!);
let userGrants = await Promise.all(
data.projects.map((project: Project) => {
return addUserGrant(data.org, machine.userId, project, [], data.tokens.accessToken!);
}),
);
return { userGrants };
}
export function teardown(data: any) {
// removeOrg(data.org, data.tokens.accessToken);
console.info('teardown: org is not removed to verify correctness of projections, do not forget to remove the org afterwards');
}
/**
* To verify the correctness of the projections you can use the following statements:
*
* set the owner of the events:
*
* set my.owner = '<org id of the created org>';
*
* check if the amount of events is the same as amount of objects
*
* select * from (
select 'projections.user_grants5', count(*) from projections.user_grants5 where resource_owner = (select current_setting('my.owner'))
union all
select 'projections.users14', count(*) from projections.users14 where resource_owner = (select current_setting('my.owner'))
union all
select 'projections.sessions8', count(*) from projections.sessions8 where user_resource_owner = (select current_setting('my.owner'))
union all
select aggregate_type, count(*) from eventstore.events2
where
aggregate_type in ('user', 'usergrant', 'session')
and event_type in ('user.machine.added', 'user.human.added', 'user.grant.added', 'session.user.checked')
and (owner = (select current_setting('my.owner'))
OR payload->>'userResourceOwner' = (select current_setting('my.owner'))
)
group by aggregate_type
) order by 2;
*/
+368
View File
@@ -0,0 +1,368 @@
import { Trend } from 'k6/metrics';
import { Org } from './org';
import http, { RefinedResponse } from 'k6/http';
import url from './url';
import { check } from 'k6';
import { Metadata } from './metadata';
export type User = {
userId: string;
loginName: string;
password: string;
};
export interface Human extends User {
loginNames: string[];
}
const createHumanTrend = new Trend('user_create_human_duration', true);
export function createHuman(username: string, org: Org, accessToken: string): Promise<Human> {
return new Promise((resolve, reject) => {
let response = http.asyncRequest(
'POST',
url('/v2/users/human'),
JSON.stringify({
username: username,
organization: {
orgId: org.organizationId,
},
profile: {
givenName: 'Gigi',
familyName: 'Zitizen',
},
email: {
email: `${username}@zitadel.com`,
isVerified: true,
},
password: {
password: 'Password1!',
changeRequired: false,
},
}),
{
headers: {
authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'x-zitadel-orgid': org.organizationId,
},
},
);
response
.then((res) => {
check(res, {
'create user is status ok': (r) => r.status >= 200 && r.status < 300,
}) || reject(`unable to create user(username: ${username}) status: ${res.status} body: ${res.body}`);
createHumanTrend.add(res.timings.duration);
const user = http.get(url(`/v2/users/${res.json('userId')!}`), {
headers: {
authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'x-zitadel-orgid': org.organizationId,
},
});
resolve(user.json('user')! as unknown as Human);
})
.catch((reason) => {
reject(reason);
});
});
}
const setEmailOTPOnHumanTrend = new Trend('set_human_email_otp_duration', true);
export async function setEmailOTPOnHuman(user: User, org: Org, accessToken: string): Promise<void> {
const response = await http.asyncRequest('POST', url(`/v2/users/${user.userId}/otp_email`), null, {
headers: {
authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'x-zitadel-orgid': org.organizationId,
},
});
check(response, {
'set email otp status ok': (r) => r.status >= 200 && r.status < 300,
});
setEmailOTPOnHumanTrend.add(response.timings.duration);
return;
}
const updateHumanTrend = new Trend('update_human_duration', true);
export function updateHuman(
payload: any = {},
userId: string,
org: Org,
accessToken: string,
): Promise<RefinedResponse<any>> {
return new Promise((resolve, reject) => {
let response = http.asyncRequest('PUT', url(`/v2beta/users/${userId}`), JSON.stringify(payload), {
headers: {
authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'x-zitadel-orgid': org.organizationId,
},
});
response
.then((res) => {
check(res, {
'update user is status ok': (r) => r.status === 201,
});
updateHumanTrend.add(res.timings.duration);
resolve(res);
})
.catch((reason) => {
reject(reason);
});
});
}
export interface Machine extends User {
loginNames: string[];
}
const createMachineTrend = new Trend('user_create_machine_duration', true);
export function createMachine(username: string, org: Org, accessToken: string): Promise<Machine> {
return new Promise((resolve, reject) => {
let response = http.asyncRequest(
'POST',
url('/management/v1/users/machine'),
JSON.stringify({
userName: username,
name: username,
// bearer
access_token_type: 0,
}),
{
headers: {
authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'x-zitadel-orgid': org.organizationId,
},
},
);
response
.then((res) => {
check(res, {
'create user is status ok': (r) => r.status >= 200 && r.status < 300,
}) || reject(`unable to create user(username: ${username}) status: ${res.status} body: ${res.body}`);
createMachineTrend.add(res.timings.duration);
const user = http.get(url(`/v2beta/users/${res.json('userId')!}`), {
headers: {
authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'x-zitadel-orgid': org.organizationId,
},
});
resolve(user.json('user')! as unknown as Machine);
})
.catch((reason) => {
reject(reason);
});
});
}
export type MachinePat = {
token: string;
};
const addMachinePatTrend = new Trend('user_add_machine_pat_duration', true);
export function addMachinePat(userId: string, org: Org, accessToken: string): Promise<MachinePat> {
return new Promise((resolve, reject) => {
let response = http.asyncRequest('POST', url(`/management/v1/users/${userId}/pats`), null, {
headers: {
authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'x-zitadel-orgid': org.organizationId,
},
});
response.then((res) => {
check(res, {
'add pat status ok': (r) => r.status >= 200 && r.status < 300,
}) || reject(`unable to add pat (user id: ${userId}) status: ${res.status} body: ${res.body}`);
addMachinePatTrend.add(res.timings.duration);
resolve(res.json()! as MachinePat);
});
});
}
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<MachineSecret> {
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 && r.status < 300,
}) || 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);
});
});
}
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 && r.status < 300,
}) || 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) => {
let response = http.asyncRequest('POST', url(`/v2beta/users/${userId}/lock`), null, {
headers: {
authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'x-zitadel-orgid': org.organizationId,
},
});
response
.then((res) => {
check(res, {
'update user is status ok': (r) => r.status >= 200 && r.status < 300,
});
lockUserTrend.add(res.timings.duration);
resolve(res);
})
.catch((reason) => {
reject(reason);
});
});
}
const deleteUserTrend = new Trend('delete_user_duration', true);
export function deleteUser(userId: string, org: Org, accessToken: string): Promise<RefinedResponse<any>> {
return new Promise((resolve, reject) => {
let response = http.asyncRequest('DELETE', url(`/v2beta/users/${userId}`), null, {
headers: {
authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'x-zitadel-orgid': org.organizationId,
},
});
response
.then((res) => {
check(res, {
'update user is status ok': (r) => r.status === 201,
});
deleteUserTrend.add(res.timings.duration);
resolve(res);
})
.catch((reason) => {
reject(reason);
});
});
}
const setUserMetadataTrend = new Trend('set_user_metadata_duration', true);
export function setUserMetadata(metadata: Metadata[], userId: string, accessToken: string): Promise<RefinedResponse<any>> {
return new Promise((resolve, reject) => {
let response = http.asyncRequest('POST', url(`/v2/users/${userId}/metadata`), JSON.stringify({ metadata: metadata }), {
headers: {
authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
});
response
.then((res) => {
check(res, {
'set user metadata is status ok': (r) => r.status >= 200 && r.status < 300,
}) || console.log(`unable to set user metadata (user id: ${userId}) status: ${res.status} body: ${res.body}`);
setUserMetadataTrend.add(res.timings.duration);
resolve(res);
})
.catch((reason) => {
reject(reason);
});
});
}
export type ListUsersRequest = {
queries: {
metadataKeyFilter?: {
key: string;
method: 'TEXT_FILTER_METHOD_EQUALS' | 'TEXT_FILTER_METHOD_CONTAINS' | 'TEXT_FILTER_METHOD_CONTAINS_IGNORE_CASE';
};
metadataValueFilter?: {
value: string;
method: 'BYTE_FILTER_METHOD_EQUALS' | 'BYTE_FILTER_METHOD_NOT_EQUALS';
};
}[];
};
export type ListUsersResult = {
details: {
totalResult: number;
};
};
const listUsersTrend = new Trend('list_users_duration', true);
export function listUsers(body: ListUsersRequest, accessToken: string): Promise<ListUsersResult> {
return new Promise((resolve, reject) => {
let response = http.asyncRequest('POST', url(`/v2/users`), JSON.stringify(body), {
headers: {
authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
});
response
.then((res) => {
check(res, {
'list users is status ok': (r) => r.status >= 200 && r.status < 300,
}) || console.log(`unable to list users status: ${res.status} body: ${res.body}`);
listUsersTrend.add(res.timings.duration);
resolve(res.json()! as ListUsersResult);
})
.catch((reason) => {
reject(reason);
});
});
}
+46
View File
@@ -0,0 +1,46 @@
import http from 'k6/http';
import { Trend } from 'k6/metrics';
import url from './url';
import { check, fail } from 'k6';
import { Project } from './project';
import { Org } from './org';
export type UserGrant = {
userGrantId: string;
};
const addUserGrantTrend = new Trend('user_grant_add', true);
export async function addUserGrant(
org: Org,
userId: string,
project: Project,
roles: string[],
accessToken: string,
): Promise<UserGrant> {
return new Promise((resolve, reject) => {
let response = http.asyncRequest(
'POST',
url(`/management/v1/users/${userId}/grants`),
JSON.stringify({
projectId: project.id,
roleKeys: roles,
}),
{
headers: {
authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'x-zitadel-orgid': org.organizationId,
},
},
);
response.then((res) => {
check(res, {
'add User Grant status ok': (r) => r.status >= 200 && r.status < 300,
}) || reject(`unable to add User Grant status: ${res.status} body: ${res.body}`);
addUserGrantTrend.add(res.timings.duration);
resolve(res.json() as UserGrant);
});
});
}