From bb72beae91f09f1bde6c08b1259323ef2dcc73db Mon Sep 17 00:00:00 2001 From: Nathan Lie Date: Wed, 28 Aug 2024 10:05:00 +0300 Subject: [PATCH 1/5] feat(backend): create operator kratos identity on backend startup --- localenv/admin-auth/docker-compose.yml | 14 +++++ package.json | 9 ++- packages/backend/src/app.ts | 60 +++++++++++++++++++ packages/backend/src/config/app.ts | 4 +- packages/backend/src/index.ts | 3 + .../frontend/kratos/scripts/userInvitation.ts | 7 +++ 6 files changed, 91 insertions(+), 6 deletions(-) diff --git a/localenv/admin-auth/docker-compose.yml b/localenv/admin-auth/docker-compose.yml index 192a812a41..ed0668f076 100644 --- a/localenv/admin-auth/docker-compose.yml +++ b/localenv/admin-auth/docker-compose.yml @@ -1,14 +1,28 @@ services: + cloud-nine-backend: + environment: + KRATOS_ADMIN_URL: http://cloud-nine-kratos:4434/admin + KRATOS_ADMIN_EMAIL: admin@mail.com + depends_on: + - cloud-nine-kratos cloud-nine-admin: environment: AUTH_ENABLED: true KRATOS_CONTAINER_PUBLIC_URL: 'http://cloud-nine-kratos:4433' KRATOS_BROWSER_PUBLIC_URL: 'http://localhost:4433' KRATOS_ADMIN_URL: 'http://cloud-nine-kratos:4434/admin' + KRATOS_ADMIN_EMAIL: 'admin@mail.com' depends_on: - cloud-nine-backend - cloud-nine-kratos + happy-life-backend: + environment: + KRATOS_ADMIN_URL: 'http://happy-life-kratos:4434/admin' + KRATOS_ADMIN_EMAIL: 'admin@mail.com' + depends_on: + - happy-life-kratos + happy-life-admin: environment: AUTH_ENABLED: true diff --git a/package.json b/package.json index 323e41cb4f..845f7cca8e 100644 --- a/package.json +++ b/package.json @@ -18,11 +18,10 @@ "check:prettier": "prettier --check .", "clean": "find . -name 'node_modules' -type d -prune -exec rm -rf '{}' +", "build": "tsc --build", - "localenv:compose:psql": "docker compose -f ./localenv/cloud-nine-wallet/docker-compose.yml -f ./localenv/happy-life-bank/docker-compose.yml -f ./localenv/merged/docker-compose.yml", - "localenv:compose": "docker compose -f ./localenv/cloud-nine-wallet/docker-compose.yml -f ./localenv/happy-life-bank/docker-compose.yml -f ./localenv/merged/docker-compose.yml -f ./localenv/tigerbeetle/docker-compose.yml --env-file ./localenv/tigerbeetle/.env.tigerbeetle", - "localenv:compose:psql:telemetry": "docker compose -f ./localenv/cloud-nine-wallet/docker-compose.yml -f ./localenv/happy-life-bank/docker-compose.yml -f ./localenv/merged/docker-compose.yml -f ./localenv/telemetry/docker-compose.yml", - "localenv:compose:telemetry": "docker compose -f ./localenv/cloud-nine-wallet/docker-compose.yml -f ./localenv/happy-life-bank/docker-compose.yml -f ./localenv/merged/docker-compose.yml -f ./localenv/tigerbeetle/docker-compose.yml -f ./localenv/telemetry/docker-compose.yml --env-file ./localenv/tigerbeetle/.env.tigerbeetle", - "localenv:compose:adminauth": "docker compose -f ./localenv/cloud-nine-wallet/docker-compose.yml -f ./localenv/happy-life-bank/docker-compose.yml -f ./localenv/merged/docker-compose.yml -f ./localenv/admin-auth/docker-compose.yml", + "localenv:compose:psql": "docker compose -f ./localenv/cloud-nine-wallet/docker-compose.yml -f ./localenv/happy-life-bank/docker-compose.yml -f ./localenv/merged/docker-compose.yml -f ./localenv/admin-auth/docker-compose.yml", + "localenv:compose": "docker compose -f ./localenv/cloud-nine-wallet/docker-compose.yml -f ./localenv/happy-life-bank/docker-compose.yml -f ./localenv/merged/docker-compose.yml -f ./localenv/admin-auth/docker-compose.yml -f ./localenv/tigerbeetle/docker-compose.yml --env-file ./localenv/tigerbeetle/.env.tigerbeetle", + "localenv:compose:psql:telemetry": "docker compose -f ./localenv/cloud-nine-wallet/docker-compose.yml -f ./localenv/happy-life-bank/docker-compose.yml -f ./localenv/merged/docker-compose.yml -f ./localenv/admin-auth/docker-compose.yml -f ./localenv/telemetry/docker-compose.yml", + "localenv:compose:telemetry": "docker compose -f ./localenv/cloud-nine-wallet/docker-compose.yml -f ./localenv/happy-life-bank/docker-compose.yml -f ./localenv/merged/docker-compose.yml -f ./localenv/admin-auth/docker-compose.yml -f ./localenv/tigerbeetle/docker-compose.yml -f ./localenv/telemetry/docker-compose.yml --env-file ./localenv/tigerbeetle/.env.tigerbeetle", "localenv:seed:auth": "pnpm -C ./packages/auth knex seed:run --env=development && pnpm -C ./packages/auth knex seed:run --env=peerdevelopment", "sanity": "pnpm -r build && pnpm -r test", "localenv:compose:autopeer": "run-p tunnel:start wait-tunnel:localenv:compose", diff --git a/packages/backend/src/app.ts b/packages/backend/src/app.ts index c56544f38f..ad34d74a1f 100644 --- a/packages/backend/src/app.ts +++ b/packages/backend/src/app.ts @@ -1,6 +1,7 @@ import { join } from 'path' import http, { Server } from 'http' import { ParsedUrlQuery } from 'querystring' +import axios from 'axios' import { Client as TigerbeetleClient } from 'tigerbeetle-node' import { IocContract } from '@adonisjs/fold' @@ -297,6 +298,65 @@ export class App { } } + public async createOperatorIdentity(): Promise { + const { kratosAdminEmail, kratosAdminUrl } = + await this.container.use('config') + const logger = await this.container.use('logger') + // TODO: error out since kratos is essentially required + if (!kratosAdminUrl || !kratosAdminEmail) return + try { + const identityQueryResponse = await axios.get( + `${kratosAdminUrl}/identities?credentials_identifier=${kratosAdminEmail}` + ) + if ( + identityQueryResponse.data.length > 0 && + identityQueryResponse.data[0].id + ) { + logger.debug( + `User with email ${kratosAdminEmail} exists on the system with the ID: ${identityQueryResponse.data[0].id}` + ) + return + } + logger.debug( + `No user with email ${kratosAdminEmail} exists on the system` + ) + + const createIdentityResponse = await axios.post( + `${kratosAdminUrl}/identities`, + { + schema_id: 'default', + traits: { + email: kratosAdminEmail + }, + metadata_admin: { + operator: true + } + }, + { + headers: { + 'Content-Type': 'application/json' + } + } + ) + logger.debug( + `Successfully created user ${kratosAdminEmail} with ID ${createIdentityResponse.data.id}` + ) + } catch (error) { + if (axios.isAxiosError(error)) { + logger.error( + `Error retrieving identity ${kratosAdminEmail}:`, + error.response?.status, + error.response?.data + ) + } else { + logger.error( + `An unexpected error occurred while trying to retrieve the identity for ${kratosAdminEmail}:`, + error + ) + } + } + } + public async startAdminServer(port: number): Promise { const koa = await this.createKoaServer() const httpServer = http.createServer(koa.callback()) diff --git a/packages/backend/src/config/app.ts b/packages/backend/src/config/app.ts index 29a3c7070a..45c28e632b 100644 --- a/packages/backend/src/config/app.ts +++ b/packages/backend/src/config/app.ts @@ -187,7 +187,9 @@ export const Config = { 'INCOMING_PAYMENT_EXPIRY_MAX_MS', 2592000000 ), // 30 days - enableSpspPaymentPointers: envBool('ENABLE_SPSP_PAYMENT_POINTERS', true) + enableSpspPaymentPointers: envBool('ENABLE_SPSP_PAYMENT_POINTERS', true), + kratosAdminUrl: envString('KRATOS_ADMIN_URL'), + kratosAdminEmail: envString('KRATOS_ADMIN_EMAIL') } function parseRedisTlsConfig( diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index a34d784ef7..c91dfa8fdc 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -640,6 +640,9 @@ export const start = async ( `Auto-peering server listening on ${config.autoPeeringServerPort}` ) } + + await app.createOperatorIdentity() + logger.info('Operator identity created on Kratos') } // If this script is run directly, start the server diff --git a/packages/frontend/kratos/scripts/userInvitation.ts b/packages/frontend/kratos/scripts/userInvitation.ts index 79a7e3dfa5..347cdb7b2f 100644 --- a/packages/frontend/kratos/scripts/userInvitation.ts +++ b/packages/frontend/kratos/scripts/userInvitation.ts @@ -4,6 +4,10 @@ import { logger } from '../../app/utils/logger.server' // Use process.argv to accept an email argument from the command line const USER_EMAIL = process.argv[2] + +// Use process.argv to accept privilege argument from the command line +const ROLE = process.argv[3] + if (!USER_EMAIL) { logger.error('No email argument provided.') process.exit(1) @@ -53,6 +57,9 @@ const createIdentity = async () => { schema_id: 'default', traits: { email: USER_EMAIL + }, + metadata_admin: { + [ROLE]: true } }, { From ee3454aa0380eed2c88e29271a5afb3c3907a263 Mon Sep 17 00:00:00 2001 From: Nathan Lie Date: Wed, 28 Aug 2024 10:25:55 +0300 Subject: [PATCH 2/5] feat: generate recovery link --- packages/backend/src/app.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/backend/src/app.ts b/packages/backend/src/app.ts index ad34d74a1f..db4c2740d0 100644 --- a/packages/backend/src/app.ts +++ b/packages/backend/src/app.ts @@ -341,6 +341,14 @@ export class App { logger.debug( `Successfully created user ${kratosAdminEmail} with ID ${createIdentityResponse.data.id}` ) + + const recoveryCodeResponse = await axios.post( + `${kratosAdminUrl}/recovery/link`, + { + identity_id: createIdentityResponse.data.id + } + ) + logger.info(`Recovery link for ${kratosAdminEmail} at ${recoveryCodeResponse.data.recovery_link}`) } catch (error) { if (axios.isAxiosError(error)) { logger.error( From 995c54de05af4f6b9d3738ffe5b0de3ae7e49c29 Mon Sep 17 00:00:00 2001 From: Nathan Lie Date: Wed, 28 Aug 2024 10:42:00 +0300 Subject: [PATCH 3/5] chore: formatting --- localenv/admin-auth/docker-compose.yml | 1 - packages/backend/src/app.ts | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/localenv/admin-auth/docker-compose.yml b/localenv/admin-auth/docker-compose.yml index ed0668f076..40a35bf4fd 100644 --- a/localenv/admin-auth/docker-compose.yml +++ b/localenv/admin-auth/docker-compose.yml @@ -11,7 +11,6 @@ services: KRATOS_CONTAINER_PUBLIC_URL: 'http://cloud-nine-kratos:4433' KRATOS_BROWSER_PUBLIC_URL: 'http://localhost:4433' KRATOS_ADMIN_URL: 'http://cloud-nine-kratos:4434/admin' - KRATOS_ADMIN_EMAIL: 'admin@mail.com' depends_on: - cloud-nine-backend - cloud-nine-kratos diff --git a/packages/backend/src/app.ts b/packages/backend/src/app.ts index db4c2740d0..53731c3496 100644 --- a/packages/backend/src/app.ts +++ b/packages/backend/src/app.ts @@ -348,7 +348,9 @@ export class App { identity_id: createIdentityResponse.data.id } ) - logger.info(`Recovery link for ${kratosAdminEmail} at ${recoveryCodeResponse.data.recovery_link}`) + logger.info( + `Recovery link for ${kratosAdminEmail} at ${recoveryCodeResponse.data.recovery_link}` + ) } catch (error) { if (axios.isAxiosError(error)) { logger.error( From f0aca0aa0267b4e6eb391fa41b004a16ae36daea Mon Sep 17 00:00:00 2001 From: Nathan Lie Date: Wed, 28 Aug 2024 11:39:59 +0300 Subject: [PATCH 4/5] fix: handle new cases --- packages/backend/src/app.ts | 75 ++++++++++++++++++++++------------- packages/backend/src/index.ts | 1 - 2 files changed, 48 insertions(+), 28 deletions(-) diff --git a/packages/backend/src/app.ts b/packages/backend/src/app.ts index 53731c3496..b15d3a878d 100644 --- a/packages/backend/src/app.ts +++ b/packages/backend/src/app.ts @@ -303,49 +303,70 @@ export class App { await this.container.use('config') const logger = await this.container.use('logger') // TODO: error out since kratos is essentially required - if (!kratosAdminUrl || !kratosAdminEmail) return + if (!kratosAdminUrl || !kratosAdminEmail) { + throw new Error('Missing admin configuration') + } try { const identityQueryResponse = await axios.get( `${kratosAdminUrl}/identities?credentials_identifier=${kratosAdminEmail}` ) - if ( + const isExistingIdentity = identityQueryResponse.data.length > 0 && identityQueryResponse.data[0].id - ) { + const operatorRole = + identityQueryResponse.data[0]?.metadata_admin.operator + let identityResponse + if (isExistingIdentity && operatorRole) { + // Identity already exists with operator role logger.debug( - `User with email ${kratosAdminEmail} exists on the system with the ID: ${identityQueryResponse.data[0].id}` + `Identity with email ${kratosAdminEmail} exists on the system with the ID: ${identityQueryResponse.data[0].id}` ) return - } - logger.debug( - `No user with email ${kratosAdminEmail} exists on the system` - ) + } else if (isExistingIdentity && !operatorRole) { + // Identity already exists but does not have operator role + identityResponse = await axios.put( + `${kratosAdminUrl}/admin/identities/${identityQueryResponse.data[0].id}`, + { + metadata_admin: { + operator: true + } + } + ) + logger.debug( + `Successfully created user ${kratosAdminEmail} with ID ${identityResponse.data.id}` + ) + } else { + // Identity does not exist + logger.debug( + `No identity with email ${kratosAdminEmail} exists on the system` + ) - const createIdentityResponse = await axios.post( - `${kratosAdminUrl}/identities`, - { - schema_id: 'default', - traits: { - email: kratosAdminEmail + identityResponse = await axios.post( + `${kratosAdminUrl}/identities`, + { + schema_id: 'default', + traits: { + email: kratosAdminEmail + }, + metadata_admin: { + operator: true + } }, - metadata_admin: { - operator: true + { + headers: { + 'Content-Type': 'application/json' + } } - }, - { - headers: { - 'Content-Type': 'application/json' - } - } - ) - logger.debug( - `Successfully created user ${kratosAdminEmail} with ID ${createIdentityResponse.data.id}` - ) + ) + logger.debug( + `Successfully created user ${kratosAdminEmail} with ID ${identityResponse.data.id}` + ) + } const recoveryCodeResponse = await axios.post( `${kratosAdminUrl}/recovery/link`, { - identity_id: createIdentityResponse.data.id + identity_id: identityResponse.data.id } ) logger.info( diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index c91dfa8fdc..c6b9445f59 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -642,7 +642,6 @@ export const start = async ( } await app.createOperatorIdentity() - logger.info('Operator identity created on Kratos') } // If this script is run directly, start the server From 42fd6d2920f175ba410f5797a31d6c89d534968a Mon Sep 17 00:00:00 2001 From: Nathan Lie Date: Wed, 28 Aug 2024 12:11:06 +0300 Subject: [PATCH 5/5] chore: fix test environments --- packages/backend/jest.config.js | 2 ++ test/integration/testenv/cloud-nine-wallet/docker-compose.yml | 2 ++ test/integration/testenv/happy-life-bank/docker-compose.yml | 2 ++ 3 files changed, 6 insertions(+) diff --git a/packages/backend/jest.config.js b/packages/backend/jest.config.js index 492a6e5e30..e6527265b5 100644 --- a/packages/backend/jest.config.js +++ b/packages/backend/jest.config.js @@ -16,6 +16,8 @@ process.env.WEBHOOK_URL = 'http://127.0.0.1:4001/webhook' process.env.STREAM_SECRET = '2/PxuRFV9PAp0yJlnAifJ+1OxujjjI16lN+DBnLNRLA=' process.env.USE_TIGERBEETLE = false process.env.ENABLE_TELEMETRY = false +process.env.KRATOS_ADMIN_URL = 'http://127.0.0.1:4434/admin' +process.env.KRATOS_ADMIN_EMAIL = 'admin@mail.com' module.exports = { ...baseConfig, diff --git a/test/integration/testenv/cloud-nine-wallet/docker-compose.yml b/test/integration/testenv/cloud-nine-wallet/docker-compose.yml index e0cf08b12b..9bfed78239 100644 --- a/test/integration/testenv/cloud-nine-wallet/docker-compose.yml +++ b/test/integration/testenv/cloud-nine-wallet/docker-compose.yml @@ -38,6 +38,8 @@ services: EXCHANGE_RATES_URL: http://host.docker.internal:8888/rates REDIS_URL: redis://shared-redis:6379/0 USE_TIGERBEETLE: false + KRATOS_ADMIN_URL: http://cloud-nine-kratos:4434/admin + KRATOS_ADMIN_EMAIL: admin@mail.com volumes: - ../private-key.pem:/workspace/private-key.pem depends_on: diff --git a/test/integration/testenv/happy-life-bank/docker-compose.yml b/test/integration/testenv/happy-life-bank/docker-compose.yml index 9cba1bc0c7..30b6f3fe94 100644 --- a/test/integration/testenv/happy-life-bank/docker-compose.yml +++ b/test/integration/testenv/happy-life-bank/docker-compose.yml @@ -36,6 +36,8 @@ services: EXCHANGE_RATES_URL: http://host.docker.internal:8889/rates REDIS_URL: redis://shared-redis:6379/2 USE_TIGERBEETLE: false + KRATOS_ADMIN_URL: 'http://happy-life-kratos:4434/admin' + KRATOS_ADMIN_EMAIL: 'admin@mail.com' volumes: - ../private-key.pem:/workspace/private-key.pem depends_on: