Skip to content

Commit

Permalink
feat(auth-admin): Webhook for general mandate delegation (#16257)
Browse files Browse the repository at this point in the history
* gm-delegation-webhook

* chore: charts update dirty files

* pr comments fixes

* pr comments fixes

* fix broken test

* Pr comments

* Pr comments

* chore: nx format:write update dirty files

* fix translation string

* Add comment to signing secret dev value.
Reuse validatePersonsNationalIds in createDeleagtionByZendeskId

* Update test labels

---------

Co-authored-by: andes-it <[email protected]>
Co-authored-by: Sævar Már Atlason <[email protected]>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
  • Loading branch information
4 people authored Oct 8, 2024
1 parent 0a169cb commit 6ce1b55
Show file tree
Hide file tree
Showing 26 changed files with 399 additions and 59 deletions.
2 changes: 2 additions & 0 deletions apps/services/auth/admin-api/infra/auth-admin-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ export const serviceSetup = (): ServiceBuilder<'services-auth-admin-api'> => {
.secrets({
ZENDESK_CONTACT_FORM_EMAIL: '/k8s/api/ZENDESK_CONTACT_FORM_EMAIL',
ZENDESK_CONTACT_FORM_TOKEN: '/k8s/api/ZENDESK_CONTACT_FORM_TOKEN',
ZENDESK_WEBHOOK_SECRET_GENERAL_MANDATE:
'/k8s/services-auth/ZENDESK_WEBHOOK_SECRET_GENERAL_MANDATE',
CLIENT_SECRET_ENCRYPTION_KEY:
'/k8s/services-auth/admin-api/CLIENT_SECRET_ENCRYPTION_KEY',
IDENTITY_SERVER_CLIENT_SECRET:
Expand Down
1 change: 0 additions & 1 deletion apps/services/auth/admin-api/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { ConfigModule } from '@nestjs/config'
import { SequelizeModule } from '@nestjs/sequelize'

import {
DelegationApiUserSystemNotificationConfig,
DelegationConfig,
SequelizeConfigService,
} from '@island.is/auth-api-lib'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,34 @@ import {
UseGuards,
} from '@nestjs/common'
import { ApiTags } from '@nestjs/swagger'
import flatMap from 'lodash/flatMap'

import {
BypassAuth,
CurrentUser,
IdsUserGuard,
Scopes,
ScopesGuard,
User,
ZendeskAuthGuard,
} from '@island.is/auth-nest-tools'
import {
CreatePaperDelegationDto,
DelegationAdminCustomDto,
DelegationAdminCustomService,
DelegationDTO,
ZendeskWebhookInputDto,
} from '@island.is/auth-api-lib'
import { Documentation } from '@island.is/nest/swagger'
import { Audit, AuditService } from '@island.is/nest/audit'
import { DelegationAdminScopes } from '@island.is/auth/scopes'
import flatMap from 'lodash/flatMap'
import { isDefined } from '@island.is/shared/utils'

import env from '../../../environments/environment'

const namespace = '@island.is/auth/delegation-admin'

@UseGuards(IdsUserGuard, ScopesGuard)
@Scopes(DelegationAdminScopes.read)
@ApiTags('delegation-admin')
@Controller('delegation-admin')
@Audit({ namespace })
Expand All @@ -43,6 +47,7 @@ export class DelegationAdminController {
) {}

@Get()
@Scopes(DelegationAdminScopes.read)
@Documentation({
response: { status: 200, type: DelegationAdminCustomDto },
request: {
Expand Down Expand Up @@ -91,6 +96,18 @@ export class DelegationAdminController {
)
}

@BypassAuth()
@UseGuards(new ZendeskAuthGuard(env.zendeskGeneralMandateWebhookSecret))
@Post('/zendesk')
@Documentation({
response: { status: 200 },
})
async createByZendeskId(
@Body() { id }: ZendeskWebhookInputDto,
): Promise<void> {
await this.delegationAdminService.createDelegationByZendeskId(id)
}

@Delete(':delegationId')
@Scopes(DelegationAdminScopes.admin)
@Documentation({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import request from 'supertest'
import bodyParser from 'body-parser'

import {
getRequestMethod,
Expand All @@ -11,9 +12,11 @@ import { User } from '@island.is/auth-nest-tools'
import { FixtureFactory } from '@island.is/services/auth/testing'
import { createCurrentUser } from '@island.is/testing/fixtures'
import { DelegationAdminScopes } from '@island.is/auth/scopes'
import { SequelizeConfigService } from '@island.is/auth-api-lib'
import { DelegationDTO, SequelizeConfigService } from '@island.is/auth-api-lib'
import { DelegationAdminCustomService } from '@island.is/auth-api-lib'

import { AppModule } from '../../../app.module'
import { includeRawBodyMiddleware } from '@island.is/infra-nest-server'

describe('withoutAuth and permissions', () => {
async function formatUrl(app: TestApp, endpoint: string, user?: User) {
Expand Down Expand Up @@ -132,4 +135,79 @@ describe('withoutAuth and permissions', () => {
app.cleanUp()
},
)

describe('POST /delegation-admin/zendesk', () => {
let app: TestApp
let server: request.SuperTest<request.Test>
let delegationAdminService: DelegationAdminCustomService

beforeEach(async () => {
app = await setupAppWithoutAuth({
AppModule,
SequelizeConfigService,
dbType: 'postgres',
beforeServerStart: async (app) => {
await new Promise((resolve) =>
resolve(app.use(includeRawBodyMiddleware())),
)
},
})

server = request(app.getHttpServer())

delegationAdminService = app.get(DelegationAdminCustomService)

jest
.spyOn(delegationAdminService, 'createDelegationByZendeskId')
.mockImplementation(() => Promise.resolve())
})

afterEach(() => {
app.cleanUp()
})

it('POST /delegation-admin/zendesk should return 403 Forbidden when request signature is invalid.', async () => {
// Act
const res = await getRequestMethod(
server,
'POST',
)('/delegation-admin/zendesk')
.send({
id: 'Incorrect body',
})
.set(
'x-zendesk-webhook-signature',
'6sUtGV8C8OdoGgCdsV2xRm3XeskZ33Bc5124RiAK4Q4=',
)
.set('x-zendesk-webhook-signature-timestamp', '2024-10-02T14:21:04Z')

// Assert
expect(res.status).toEqual(403)
expect(res.body).toMatchObject({
status: 403,
type: 'https://httpstatuses.org/403',
title: 'Forbidden',
detail: 'Forbidden resource',
})
})

it('POST /delegation-admin/zendesk should return 200 when signature is valid', async () => {
// Act
const res = await getRequestMethod(
server,
'POST',
)('/delegation-admin/zendesk')
.send({
id: 'test',
})
.set(
'x-zendesk-webhook-signature',
'ntgS06VGgd4z73lHjIpC2sk9azhRNi4u1xkXF/KPKTs=',
)
.set('x-zendesk-webhook-signature-timestamp', '2024-10-02T14:21:04Z')

// Assert
expect(res.status).toEqual(200)
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -224,15 +224,25 @@ describe('DelegationAdmin - With authentication', () => {
const mockZendeskService = (
toNationalId: string,
fromNationalId: string,
info?: {
tags?: string[]
status?: TicketStatus
},
) => {
const { tags, status } = {
tags: [DELEGATION_TAG],
status: TicketStatus.Solved,
...info,
}

zendeskServiceApiSpy = jest
.spyOn(zendeskService, 'getTicket')
.mockImplementation((ticketId: string) => {
return new Promise((resolve) =>
resolve({
id: ticketId,
tags: [DELEGATION_TAG],
status: TicketStatus.Solved,
tags: tags,
status: status,
custom_fields: [
{
id: ZENDESK_CUSTOM_FIELDS.DelegationToReferenceId,
Expand Down Expand Up @@ -328,5 +338,26 @@ describe('DelegationAdmin - With authentication', () => {
// Assert
expect(res.status).toEqual(400)
})

it('POST /delegation-admin should not create delegation with incorrect zendesk ticket status', async () => {
// Arrange
mockZendeskService(toNationalId, fromNationalId, {
status: TicketStatus.Open,
})

const delegation: CreatePaperDelegationDto = {
toNationalId,
fromNationalId,
referenceId: 'ref1',
}

// Act
const res = await getRequestMethod(
server,
'POST',
)('/delegation-admin').send(delegation)

expect(res.status).toEqual(400)
})
})
})
7 changes: 7 additions & 0 deletions apps/services/auth/admin-api/src/environments/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ const devConfig = {
port: 6333,
clientSecretEncryptionKey:
process.env.CLIENT_SECRET_ENCRYPTION_KEY ?? 'secret',
zendeskGeneralMandateWebhookSecret:
process.env.ZENDESK_WEBHOOK_SECRET_GENERAL_MANDATE ??
//The static test signing secret from Zendesk as described in their docs
// https://developer.zendesk.com/documentation/webhooks/verifying/#signing-secrets-on-new-webhooks
'dGhpc19zZWNyZXRfaXNfZm9yX3Rlc3Rpbmdfb25seQ==',
}

const prodConfig = {
Expand All @@ -27,6 +32,8 @@ const prodConfig = {
},
port: 3333,
clientSecretEncryptionKey: process.env.CLIENT_SECRET_ENCRYPTION_KEY,
zendeskGeneralMandateWebhookSecret:
process.env.ZENDESK_WEBHOOK_SECRET_GENERAL_MANDATE,
}

export default process.env.NODE_ENV === 'production' ? prodConfig : devConfig
8 changes: 7 additions & 1 deletion apps/services/auth/admin-api/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { bootstrap } from '@island.is/infra-nest-server'
import {
bootstrap,
includeRawBodyMiddleware,
} from '@island.is/infra-nest-server'

import { AppModule } from './app/app.module'
import { environment as env } from './environments'
Expand All @@ -14,4 +17,7 @@ bootstrap({
healthCheck: {
database: true,
},
beforeServerStart: (app) => {
app.use(includeRawBodyMiddleware())
},
})
1 change: 1 addition & 0 deletions charts/identity-server/values.dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ services-auth-admin-api:
SYSLUMENN_USERNAME: '/k8s/services-auth/SYSLUMENN_USERNAME'
ZENDESK_CONTACT_FORM_EMAIL: '/k8s/api/ZENDESK_CONTACT_FORM_EMAIL'
ZENDESK_CONTACT_FORM_TOKEN: '/k8s/api/ZENDESK_CONTACT_FORM_TOKEN'
ZENDESK_WEBHOOK_SECRET_GENERAL_MANDATE: '/k8s/services-auth/ZENDESK_WEBHOOK_SECRET_GENERAL_MANDATE'
securityContext:
allowPrivilegeEscalation: false
privileged: false
Expand Down
1 change: 1 addition & 0 deletions charts/identity-server/values.prod.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,7 @@ services-auth-admin-api:
SYSLUMENN_USERNAME: '/k8s/services-auth/SYSLUMENN_USERNAME'
ZENDESK_CONTACT_FORM_EMAIL: '/k8s/api/ZENDESK_CONTACT_FORM_EMAIL'
ZENDESK_CONTACT_FORM_TOKEN: '/k8s/api/ZENDESK_CONTACT_FORM_TOKEN'
ZENDESK_WEBHOOK_SECRET_GENERAL_MANDATE: '/k8s/services-auth/ZENDESK_WEBHOOK_SECRET_GENERAL_MANDATE'
securityContext:
allowPrivilegeEscalation: false
privileged: false
Expand Down
1 change: 1 addition & 0 deletions charts/identity-server/values.staging.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ services-auth-admin-api:
SYSLUMENN_USERNAME: '/k8s/services-auth/SYSLUMENN_USERNAME'
ZENDESK_CONTACT_FORM_EMAIL: '/k8s/api/ZENDESK_CONTACT_FORM_EMAIL'
ZENDESK_CONTACT_FORM_TOKEN: '/k8s/api/ZENDESK_CONTACT_FORM_TOKEN'
ZENDESK_WEBHOOK_SECRET_GENERAL_MANDATE: '/k8s/services-auth/ZENDESK_WEBHOOK_SECRET_GENERAL_MANDATE'
securityContext:
allowPrivilegeEscalation: false
privileged: false
Expand Down
1 change: 1 addition & 0 deletions libs/auth-api-lib/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export * from './lib/delegations/dto/delegation-index.dto'
export * from './lib/delegations/dto/paginated-delegation-provider.dto'
export * from './lib/delegations/dto/delegation-provider.dto'
export * from './lib/delegations/dto/merged-delegation.dto'
export * from './lib/delegations/dto/zendesk-webhook-input.dto'
export * from './lib/delegations/models/delegation.model'
export * from './lib/delegations/models/delegation.model'
export * from './lib/delegations/models/delegation-scope.model'
Expand Down
Loading

0 comments on commit 6ce1b55

Please sign in to comment.