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: auth service-to-service api #3148

Draft
wants to merge 10 commits into
base: 2893/multi-tenancy-v1
Choose a base branch
from
53 changes: 53 additions & 0 deletions packages/auth/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ export class App {
private interactionServer!: Server
private introspectionServer!: Server
private adminServer!: Server
private serviceAPIServer!: Server
private logger!: Logger
private config!: IAppConfig
private databaseCleanupRules!: {
Expand Down Expand Up @@ -455,6 +456,51 @@ export class App {
this.interactionServer = koa.listen(port)
}

public async startServiceAPIServer(port: number | string): Promise<void> {
const koa = await this.createKoaServer()

const router = new Router<DefaultState, AppContext>()
router.use(bodyParser())

const errorHandler = async (ctx: Koa.Context, next: Koa.Next) => {
try {
await next()
} catch (err) {
const logger = await ctx.container.use('logger')
logger.info(
{
method: ctx.method,
route: ctx.path,
headers: ctx.headers,
params: ctx.params,
requestBody: ctx.request.body,
err
},
'Service API Error'
)
}
}

koa.use(errorHandler)

router.get('/healthz', (ctx: AppContext): void => {
ctx.status = 200
})

const tenantRoutes = await this.container.use('tenantRoutes')

router.get('/tenant/:id', tenantRoutes.get)
router.post('/tenant', tenantRoutes.create)
router.patch('/tenant/:id', tenantRoutes.update)
router.delete('/tenant/:id', tenantRoutes.delete)

koa.use(cors())
koa.use(router.middleware())
koa.use(router.routes())

this.serviceAPIServer = koa.listen(port)
}

private async createKoaServer(): Promise<Koa<Koa.DefaultState, AppContext>> {
const koa = new Koa<DefaultState, AppContext>({
proxy: this.config.trustProxy
Expand Down Expand Up @@ -500,6 +546,9 @@ export class App {
if (this.introspectionServer) {
await this.stopServer(this.introspectionServer)
}
if (this.serviceAPIServer) {
await this.stopServer(this.serviceAPIServer)
}
}

private async stopServer(server: Server): Promise<void> {
Expand Down Expand Up @@ -530,6 +579,10 @@ export class App {
return this.getPort(this.introspectionServer)
}

public getServiceAPIPort(): number {
return this.getPort(this.serviceAPIServer)
}

private getPort(server: Server): number {
const address = server?.address()
if (address && !(typeof address == 'string')) {
Expand Down
1 change: 1 addition & 0 deletions packages/auth/src/config/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export const Config = {
authPort: envInt('AUTH_PORT', 3006),
interactionPort: envInt('INTERACTION_PORT', 3009),
introspectionPort: envInt('INTROSPECTION_PORT', 3007),
serviceAPIPort: envInt('SERVICE_API_PORT', 3010),
env: envString('NODE_ENV', 'development'),
trustProxy: envBool('TRUST_PROXY', false),
enableManualMigrations: envBool('ENABLE_MANUAL_MIGRATIONS', false),
Expand Down
14 changes: 14 additions & 0 deletions packages/auth/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { createInteractionService } from './interaction/service'
import { getTokenIntrospectionOpenAPI } from 'token-introspection'
import { Redis } from 'ioredis'
import { createTenantService } from './tenant/service'
import { createTenantRoutes } from './tenant/routes'

const container = initIocContainer(Config)
const app = new App(container)
Expand Down Expand Up @@ -163,6 +164,16 @@ export function initIocContainer(
}
)

container.singleton(
'tenantRoutes',
async (deps: IocContract<AppServices>) => {
return createTenantRoutes({
tenantService: await deps.use('tenantService'),
logger: await deps.use('logger')
})
}
)

container.singleton('openApi', async () => {
const authServerSpec = await getAuthServerOpenAPI()
const idpSpec = await createOpenAPI(
Expand Down Expand Up @@ -315,6 +326,9 @@ export const start = async (

await app.startIntrospectionServer(config.introspectionPort)
logger.info(`Introspection server listening on ${app.getIntrospectionPort()}`)

await app.startServiceAPIServer(config.serviceAPIPort)
logger.info(`Service API server listening on ${app.getServiceAPIPort()}`)
}

// If this script is run directly, start the server
Expand Down
225 changes: 225 additions & 0 deletions packages/auth/src/tenant/routes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
import { IocContract } from '@adonisjs/fold'
import { v4 } from 'uuid'

import { createContext } from '../tests/context'
import { createTestApp, TestContainer } from '../tests/app'
import { Config } from '../config/app'
import { initIocContainer } from '..'
import { AppServices } from '../app'
import { truncateTables } from '../tests/tableManager'
import {
CreateContext,
UpdateContext,
DeleteContext,
TenantRoutes,
createTenantRoutes,
GetContext
} from './routes'
import { TenantService } from './service'
import { Tenant } from './model'

describe('Tenant Routes', (): void => {
let deps: IocContract<AppServices>
let appContainer: TestContainer
let tenantRoutes: TenantRoutes
let tenantService: TenantService

beforeAll(async (): Promise<void> => {
deps = initIocContainer(Config)
appContainer = await createTestApp(deps)
tenantService = await deps.use('tenantService')
const logger = await deps.use('logger')

tenantRoutes = createTenantRoutes({
tenantService,
logger
})
})

afterEach(async (): Promise<void> => {
await truncateTables(appContainer.knex)
})

afterAll(async (): Promise<void> => {
await appContainer.shutdown()
})

describe('get', (): void => {
test('Gets a tenant', async (): Promise<void> => {
const tenant = await Tenant.query().insert({
id: v4(),
idpConsentUrl: 'https://example.com/consent',
idpSecret: 'secret123'
})

const ctx = createContext<GetContext>(
{
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
}
},
{
id: tenant.id
}
)

await expect(tenantRoutes.get(ctx)).resolves.toBeUndefined()
expect(ctx.status).toBe(200)
expect(ctx.body).toEqual({
id: tenant.id,
idpConsentUrl: tenant.idpConsentUrl,
idpSecret: tenant.idpSecret
})
})

test('Returns 404 when getting non-existent tenant', async (): Promise<void> => {
const ctx = createContext<GetContext>(
{
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
}
},
{
id: v4()
}
)

await expect(tenantRoutes.get(ctx)).resolves.toBeUndefined()
expect(ctx.status).toBe(404)
expect(ctx.body).toBeUndefined()
})
})

describe('create', (): void => {
test('Creates a tenant', async (): Promise<void> => {
const tenantData = {
id: v4(),
idpConsentUrl: 'https://example.com/consent',
idpSecret: 'secret123'
}

const ctx = createContext<CreateContext>(
{
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
}
},
{}
)
ctx.request.body = tenantData

await expect(tenantRoutes.create(ctx)).resolves.toBeUndefined()
expect(ctx.status).toBe(204)
expect(ctx.body).toBe(undefined)

const tenant = await Tenant.query().findById(tenantData.id)
expect(tenant).toBeDefined()
expect(tenant?.idpConsentUrl).toBe(tenantData.idpConsentUrl)
expect(tenant?.idpSecret).toBe(tenantData.idpSecret)
})
})

describe('update', (): void => {
test('Updates a tenant', async (): Promise<void> => {
const tenant = await Tenant.query().insert({
id: v4(),
idpConsentUrl: 'https://example.com/consent',
idpSecret: 'secret123'
})

const updateData = {
idpConsentUrl: 'https://example.com/new-consent',
idpSecret: 'newSecret123'
}

const ctx = createContext<UpdateContext>(
{
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
}
},
{
id: tenant.id
}
)
ctx.request.body = updateData

await expect(tenantRoutes.update(ctx)).resolves.toBeUndefined()
expect(ctx.status).toBe(204)
expect(ctx.body).toBe(undefined)

const updatedTenant = await Tenant.query().findById(tenant.id)
expect(updatedTenant?.idpConsentUrl).toBe(updateData.idpConsentUrl)
expect(updatedTenant?.idpSecret).toBe(updateData.idpSecret)
})

test('Returns 404 when updating non-existent tenant', async (): Promise<void> => {
const ctx = createContext<UpdateContext>(
{
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
}
},
{
id: v4()
}
)
ctx.request.body = {
idpConsentUrl: 'https://example.com/new-consent'
}

await expect(tenantRoutes.update(ctx)).resolves.toBeUndefined()
expect(ctx.status).toBe(404)
})
})

describe('delete', (): void => {
test('Deletes a tenant', async (): Promise<void> => {
const tenant = await Tenant.query().insert({
id: v4(),
idpConsentUrl: 'https://example.com/consent',
idpSecret: 'secret123'
})

const ctx = createContext<DeleteContext>(
{
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
}
},
{
id: tenant.id
}
)

await expect(tenantRoutes.delete(ctx)).resolves.toBeUndefined()
expect(ctx.status).toBe(204)

const deletedTenant = await Tenant.query().findById(tenant.id)
expect(deletedTenant?.deletedAt).not.toBeNull()
})

test('Returns 404 when deleting non-existent tenant', async (): Promise<void> => {
const ctx = createContext<DeleteContext>(
{
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
}
},
{
id: v4()
}
)

await expect(tenantRoutes.delete(ctx)).resolves.toBeUndefined()
expect(ctx.status).toBe(404)
})
})
})
Loading
Loading