Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(frontend): tenant management UI #2934

Merged
merged 25 commits into from
Sep 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
46e2a0c
feat(backend): create operator kratos identity on backend startup
njlie Aug 28, 2024
ac57762
feat: generate recovery link
njlie Aug 28, 2024
1ee274e
chore: formatting
njlie Aug 28, 2024
a55d564
fix: handle new cases
njlie Aug 28, 2024
1130b3e
feat(frontend,backend): tenant management api
njlie Aug 29, 2024
63d0d8f
Add migration files
sanducb Aug 27, 2024
87c5e7e
Update migration files
sanducb Aug 27, 2024
b9e9843
feat(tenant): basic tenant admin api schema and service
golobitch Aug 27, 2024
d871a63
Fix file name for migrations
sanducb Aug 27, 2024
2c416f3
feat(auth): create basic tenant service and model plus graphql schema
golobitch Aug 27, 2024
58caaf0
feat(graphql): add create tenant resolver and call service and update…
golobitch Aug 28, 2024
01f8446
feat(auth): add basic tenant schema and appropriate resources like mo…
golobitch Aug 28, 2024
e5c719a
Add tenant model in backend
sanducb Aug 28, 2024
d9b5ca2
feat(backend): add apollo client do dependencies
golobitch Aug 28, 2024
31f8d6c
feat(auth): delete tenant
golobitch Aug 28, 2024
b52867c
chore(auth): format
golobitch Aug 28, 2024
fa9166d
feat(backend): create tenant service implementation
golobitch Aug 28, 2024
3c99dca
feat(packages): make multi tenant work wip
golobitch Aug 28, 2024
d60a490
feat(backend): update resolvers with tenant id and finish the tenant …
golobitch Aug 29, 2024
1c25b9d
feat(backend): small changes to schema + mapping of tenant to graphql…
golobitch Aug 29, 2024
41b2e92
feat(backend,frontend): create kratos identity when tenants are creat…
njlie Aug 30, 2024
d451c3e
fix: tenant service functions
njlie Sep 4, 2024
03e246d
chore: fix test env
njlie Sep 4, 2024
f6b1fde
feat: better kratos check conditionals
njlie Sep 6, 2024
6550118
chore: remove unused package
njlie Sep 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions localenv/admin-auth/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
services:
cloud-nine-backend:
environment:
KRATOS_PUBLIC_URL: 'http://cloud-nine-kratos:4433'
KRATOS_ADMIN_URL: http://cloud-nine-kratos:4434/admin
KRATOS_ADMIN_EMAIL: [email protected]
depends_on:
Expand All @@ -17,6 +18,7 @@ services:

happy-life-backend:
environment:
KRATOS_PUBLIC_URL: 'http://happy-life-kratos:4433'
KRATOS_ADMIN_URL: 'http://happy-life-kratos:4434/admin'
KRATOS_ADMIN_EMAIL: '[email protected]'
depends_on:
Expand Down
2 changes: 2 additions & 0 deletions localenv/cloud-nine-wallet/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ services:
ENABLE_TELEMETRY: true
KEY_ID: 7097F83B-CB84-469E-96C6-2141C72E22C0
AUTH_ADMIN_API_SECRET: rPoZpe9tVyBNCigm05QDco7WLcYa0xMao7lO5KG1XG4=
OPERATOR_IDP_CONSENT_URL: http://localhost:3030/mock-idp/
OPERATOR_IDP_SECRET: 2pEcn2kkCclbOHQiGNEwhJ0rucATZhrA807HTm2rNXE=
depends_on:
- shared-database
- shared-redis
Expand Down
2 changes: 1 addition & 1 deletion localenv/cloud-nine-wallet/seed.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
tenants:
- name: PrimaryTenant
- name: c9PrimaryTenant
idpConsentUrl: https://interledger.org/consent
idpSecret: myVerySecureSecret
endpoints: [
Expand Down
5 changes: 4 additions & 1 deletion localenv/happy-life-bank/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,10 @@ services:
WALLET_ADDRESS_URL: ${HAPPY_LIFE_BANK_WALLET_ADDRESS_URL:-https://happy-life-bank-backend/.well-known/pay}
ENABLE_TELEMETRY: true
KEY_ID: 53f2d913-e98a-40b9-b270-372d0547f23d
AUTH_ADMIN_URL: http://happy-life-bank-auth:4003
AUTH_ADMIN_URL: http://happy-life-bank-auth:3003/graphql
AUTH_ADMIN_API_SECRET: rPoZpe9tVyBNCigm05QDco7WLcYa0xMao7lO5KG1XG4=
OPERATOR_IDP_CONSENT_URL: http://localhost:3031/mock-idp/
OPERATOR_IDP_SECRET: 2pEcn2kkCclbOHQiGNEwhJ0rucATZhrA807HTm2rNXE=
depends_on:
- cloud-nine-backend
happy-life-auth:
Expand Down
14 changes: 14 additions & 0 deletions localenv/happy-life-bank/seed.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
tenants:
- name: hlbPrimaryTenant
idpConsentUrl: https://interledger.org/consent
idpSecret: myVerySecureSecret
endpoints: [
{
"type": "RatesUrl",
"value": "https://interledger.org/rates"
},
{
"type": "WebhookBaseUrl",
"value": "https://interledger.org/webhooks"
}
]
assets:
- code: USD
scale: 2
Expand Down
7 changes: 6 additions & 1 deletion localenv/mock-account-servicing-entity/generated/graphql.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion packages/auth/src/graphql/resolvers/tenant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ export const deleteTenant: MutationResolvers<ApolloContext>['deleteTenant'] =
ctx
): Promise<ResolversTypes['DeleteTenantMutationResponse']> => {
const tenantService = await ctx.container.use('tenantService')

const tenant = await tenantService.delete(args.input.tenantId)
if (!tenant) {
throw new GraphQLError(errorToMessage[TenantError.UnknownTenant], {
Expand Down
12 changes: 12 additions & 0 deletions packages/auth/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,18 @@ export function initIocContainer(
}
)

container.singleton(
'tenantService',
async (deps: IocContract<AppServices>) => {
const [logger, knex] = await Promise.all([
deps.use('logger'),
deps.use('knex')
])

return createTenantService({ logger, knex })
}
)

container.singleton(
'accessService',
async (deps: IocContract<AppServices>) => {
Expand Down
6 changes: 6 additions & 0 deletions packages/backend/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ 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 = '[email protected]'
process.env.OPERATOR_IDP_SECRET = 'testsecret'
process.env.OPERATOR_IDP_CONSENT_URL = 'http://127.0.0.1:3030/mock-idp/'
process.env.AUTH_ADMIN_URL = 'http://127.0.0.1:3003/graphql'
process.env.AUTH_ADMIN_API_SECRET =
'rPoZpe9tVyBNCigm05QDco7WLcYa0xMao7lO5KG1XG4='
process.env.KRATOS_PUBLIC_URL = 'http://127.0.0.1:4433/graphql'

module.exports = {
...baseConfig,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ exports.up = function (knex) {
return knex.schema.createTable('tenants', function (table) {
table.uuid('id').primary()
table.string('kratosIdentityId').notNullable()
table.string('email').notNullable()
table.timestamp('createdAt').defaultTo(knex.fn.now())
table.timestamp('updatedAt').defaultTo(knex.fn.now())
table.timestamp('deletedAt').nullable().defaultTo(null)
Expand Down
118 changes: 34 additions & 84 deletions packages/backend/src/app.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
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'
Expand Down Expand Up @@ -86,7 +85,10 @@ import { IlpPaymentService } from './payment-method/ilp/service'
import { TelemetryService } from './telemetry/service'
import { ApolloArmor } from '@escape.tech/graphql-armor'
import { openPaymentsServerErrorMiddleware } from './open_payments/route-errors'
import { verifyApiSignature } from './shared/utils'
import {
// getTenantIdFromRequestHeaders,
verifyApiSignature
} from './shared/utils'
import { WalletAddress } from './open_payments/wallet_address/model'
import {
getWalletAddressUrlFromIncomingPayment,
Expand All @@ -102,6 +104,7 @@ import { LoggingPlugin } from './graphql/plugin'
import { GrantService } from './open_payments/grant/service'
import { AuthServerService } from './open_payments/authServer/service'
import { TenantService } from './tenant/service'
import { EndpointType } from './tenant/endpoints/model'
export interface AppContextData {
logger: Logger
container: AppContainer
Expand All @@ -122,7 +125,12 @@ export type AppRequest<ParamsT extends string = string> = Omit<
params: Record<ParamsT, string>
}

export interface WalletAddressUrlContext extends AppContext {
export interface TenantedAppContext extends AppContext {
tenantId: string
isOperator: boolean
}

export interface WalletAddressUrlContext extends TenantedAppContext {
walletAddressUrl: string
grant?: Grant
client?: string
Expand Down Expand Up @@ -300,93 +308,28 @@ export class App {
}
}

// TODO: Move into Kratos service
public async createOperatorIdentity(): Promise<void> {
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) {
throw new Error('Missing admin configuration')
}
try {
const identityQueryResponse = await axios.get(
`${kratosAdminUrl}/identities?credentials_identifier=${kratosAdminEmail}`
)
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(
`Identity with email ${kratosAdminEmail} exists on the system with the ID: ${identityQueryResponse.data[0].id}`
)
return
} 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`
)

identityResponse = 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 ${identityResponse.data.id}`
)
}

const recoveryCodeResponse = await axios.post(
`${kratosAdminUrl}/recovery/link`,
{
identity_id: identityResponse.data.id
}
)
logger.info(
`Recovery link for ${kratosAdminEmail} at ${recoveryCodeResponse.data.recovery_link}`
)
} 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
)
}
// Create tenant if it does not exist
// TODO: check if tenant exists by querying psql for tenant email, then querying kratos for stored kratosId if found
const tenantService = await this.container.use('tenantService')
const operatorTenant = await tenantService.getByEmail(kratosAdminEmail)
if (!operatorTenant) {
const config = await this.container.use('config')
await tenantService.create({
idpSecret: config.operatorIdpSecret,
idpConsentEndpoint: config.operatorIdpConsentUrl,
endpoints: [
{ value: config.webhookUrl, type: EndpointType.WebhookBaseUrl }
],
email: config.kratosAdminEmail,
isOperator: true
})
}
}

Expand Down Expand Up @@ -483,6 +426,13 @@ export class App {
})
}

// Determine Kratos Identity
// TODO: Comment out for now until seed script has a good way to acquire a kratos session
// koa.use(async (ctx: TenantedAppContext, next: Koa.Next): Promise<void> => {
// await getTenantIdFromRequestHeaders(ctx, this.config)
// return next()
// })

koa.use(
koaMiddleware(this.apolloServer, {
context: async (): Promise<ApolloContext> => {
Expand Down
5 changes: 4 additions & 1 deletion packages/backend/src/config/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,8 +191,11 @@ export const Config = {
2592000000
), // 30 days
enableSpspPaymentPointers: envBool('ENABLE_SPSP_PAYMENT_POINTERS', true),
kratosPublicUrl: envString('KRATOS_PUBLIC_URL'),
kratosAdminUrl: envString('KRATOS_ADMIN_URL'),
kratosAdminEmail: envString('KRATOS_ADMIN_EMAIL')
kratosAdminEmail: envString('KRATOS_ADMIN_EMAIL'),
operatorIdpSecret: envString('OPERATOR_IDP_SECRET'),
operatorIdpConsentUrl: envString('OPERATOR_IDP_CONSENT_URL')
}

function parseRedisTlsConfig(
Expand Down
34 changes: 33 additions & 1 deletion packages/backend/src/graphql/generated/graphql.schema.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading