Skip to content

Commit

Permalink
feat(backend,frontend): create kratos identity when tenants are creat…
Browse files Browse the repository at this point in the history
…ed, permissioned admin UI pages
  • Loading branch information
njlie committed Aug 30, 2024
1 parent 81d87a2 commit 1483683
Show file tree
Hide file tree
Showing 24 changed files with 291 additions and 201 deletions.
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
9 changes: 7 additions & 2 deletions 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.

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
35 changes: 29 additions & 6 deletions packages/backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,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/model'
export interface AppContextData {
logger: Logger
container: AppContainer
Expand Down Expand Up @@ -308,14 +309,15 @@ 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')
}
let identityId
try {
const identityQueryResponse = await axios.get(
`${kratosAdminUrl}/identities?credentials_identifier=${kratosAdminEmail}`
Expand All @@ -331,6 +333,7 @@ export class App {
logger.debug(
`Identity with email ${kratosAdminEmail} exists on the system with the ID: ${identityQueryResponse.data[0].id}`
)
identityId = identityQueryResponse.data.id
return
} else if (isExistingIdentity && !operatorRole) {
// Identity already exists but does not have operator role
Expand All @@ -345,6 +348,7 @@ export class App {
logger.debug(
`Successfully created user ${kratosAdminEmail} with ID ${identityResponse.data.id}`
)
identityId = identityResponse.data.id
} else {
// Identity does not exist
logger.debug(
Expand All @@ -371,17 +375,35 @@ export class App {
logger.debug(
`Successfully created user ${kratosAdminEmail} with ID ${identityResponse.data.id}`
)
identityId = identityResponse.data.id
}

const recoveryCodeResponse = await axios.post(
`${kratosAdminUrl}/recovery/link`,
{
identity_id: identityResponse.data.id
identity_id: identityId
}
)
logger.info(
`Recovery link for ${kratosAdminEmail} at ${recoveryCodeResponse.data.recovery_link}`
)

// 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.getByIdentity(identityId)
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
})
}
} catch (error) {
if (axios.isAxiosError(error)) {
logger.error(
Expand Down Expand Up @@ -492,10 +514,11 @@ export class App {
}

// Determine Kratos Identity
koa.use(async (ctx: TenantedAppContext, next: Koa.Next): Promise<void> => {
await getTenantIdFromRequestHeaders(ctx, this.config)
return next()
})
// 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, {
Expand Down
4 changes: 3 additions & 1 deletion packages/backend/src/config/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,9 @@ export const Config = {
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
36 changes: 34 additions & 2 deletions 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.

9 changes: 7 additions & 2 deletions packages/backend/src/graphql/generated/graphql.ts

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

12 changes: 7 additions & 5 deletions packages/backend/src/graphql/resolvers/tenant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const getTenants: QueryResolvers<ApolloContext>['tenants'] = async (
const order = sortOrder === 'ASC' ? SortOrder.Asc : SortOrder.Desc
const tenants = await tenantService.getPage(pagination, order)
const pageInfo = await getPageInfo({
getPage: (pagination: Pagination, sortOrder?: SortOrder) =>
getPage: (pagination: Pagination, sortOrder?: SortOrder) =>
tenantService.getPage(pagination, sortOrder),
page: tenants,
sortOrder: order
Expand Down Expand Up @@ -76,14 +76,15 @@ export const createTenant: MutationResolvers<ApolloContext>['createTenant'] =
const tenantService = await ctx.container.use('tenantService')

const tenantOrError = await tenantService.create({
idpConsentEndpoint: args.input.idpConsentEndpoint,
idpConsentEndpoint: args.input.idpConsentUrl,
idpSecret: args.input.idpSecret,
endpoints: args.input.endpoints.map((endpoint) => {
return {
value: endpoint.value,
type: mapTenantEndpointTypeToModelEndpointType[endpoint.type]
}
})
}),
email: args.input.email
})

if (isTenantError(tenantOrError)) {
Expand All @@ -102,8 +103,9 @@ export const createTenant: MutationResolvers<ApolloContext>['createTenant'] =
export function tenantToGraphql(tenant: Tenant): SchemaTenant {
return {
id: tenant.id,
email: tenant.email,
kratosIdentityId: tenant.kratosIdentityId,
createdAt: new Date(tenant.createdAt).toISOString(),
updatedAt: new Date(tenant.updatedAt).toISOString()
createdAt: tenant.createdAt.toISOString(),
updatedAt: tenant.updatedAt.toISOString()
}
}
9 changes: 7 additions & 2 deletions packages/backend/src/graphql/schema.graphql
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
type Query {
"Fetch a tenant"
tenant(id: ID!): Tenant
tenant(id: String!): Tenant

"Fetch a page of tenants"
tenants(
Expand Down Expand Up @@ -1148,11 +1148,14 @@ input CreateTenantEndpointsInput {
}

input CreateTenantInput {
"Email of the tenant"
email: String!

"List of endpoints types for the tenant"
endpoints: [CreateTenantEndpointsInput!]!

"IDP Endpoint"
idpConsentEndpoint: String!
idpConsentUrl: String!

"IDP Secret"
idpSecret: String!
Expand Down Expand Up @@ -1374,6 +1377,8 @@ type CreateTenantMutationResponse {
type Tenant {
"Tenant ID that is used in subsequent resources"
id: ID!
"Tenant Email for Kratos identity & recovery"
email: String!
"Kratos identity ID"
kratosIdentityId: String!
"Date-time of creation"
Expand Down
3 changes: 1 addition & 2 deletions packages/backend/src/shared/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,6 @@ export async function getTenantIdFromRequestHeaders(
ctx: TenantedAppContext,
config: IAppConfig
): Promise<void> {
const logger = await ctx.container.use('logger')
const cookie = ctx.request.headers['cookie']
const session = await axios.get(`${config.kratosPublicUrl}/sessions/whoami`, {
headers: {
Expand All @@ -222,7 +221,7 @@ export async function getTenantIdFromRequestHeaders(

const identityId = session.data?.identity.id
const tenantService = await ctx.container.use('tenantService')
const tenant = await tenantService.getByKratosId(identityId)
const tenant = await tenantService.getByIdentity(identityId)
if (!tenant) {
ctx.throw(401, 'Unauthorized')
}
Expand Down
1 change: 1 addition & 0 deletions packages/backend/src/tenant/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export class Tenant extends BaseModel {
return 'tenants'
}

public email!: string
public kratosIdentityId!: string
public deletedAt?: Date
}
Expand Down
Loading

0 comments on commit 1483683

Please sign in to comment.