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-admin): Webhook for general mandate delegation #16257

Merged
merged 12 commits into from
Oct 8, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ import {
import { ApiTags } from '@nestjs/swagger'

import {
BypassAuth,
CurrentUser,
IdsUserGuard,
Scopes,
ScopesGuard,
User,
ZendeskAuthGuard,
} from '@island.is/auth-nest-tools'
import {
CreatePaperDelegationDto,
Expand All @@ -30,9 +32,10 @@ import flatMap from 'lodash/flatMap'
import { isDefined } from '@island.is/shared/utils'

const namespace = '@island.is/auth/delegation-admin'
const ZENDESK_WEBHOOK_SECRET_GENERAL_MANDATE =
process.env.ZENDESK_WEBHOOK_SECRET_GENERAL_MANDATE ?? ''
GunnlaugurG marked this conversation as resolved.
Show resolved Hide resolved

@UseGuards(IdsUserGuard, ScopesGuard)
@Scopes(DelegationAdminScopes.read)
GunnlaugurG marked this conversation as resolved.
Show resolved Hide resolved
@ApiTags('delegation-admin')
@Controller('delegation-admin')
@Audit({ namespace })
Expand All @@ -42,6 +45,7 @@ export class DelegationAdminController {
private readonly auditService: AuditService,
) {}

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

@BypassAuth()
@UseGuards(ZendeskAuthGuard(ZENDESK_WEBHOOK_SECRET_GENERAL_MANDATE))
@Post(':zendeskId')
GunnlaugurG marked this conversation as resolved.
Show resolved Hide resolved
@Documentation({
response: { status: 201, type: DelegationDTO },
request: {
params: {
zendeskId: {
required: true,
description: 'The id of the zendesk ticket containing the delegation',
},
},
},
})
createByZendeskId(
@Param('zendeskId') zendeskId: string,
): Promise<DelegationDTO> {
return this.delegationAdminService.createDelegationByZendeskId(zendeskId)
}

@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,7 +12,8 @@ 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'

Expand Down Expand Up @@ -132,4 +134,89 @@ describe('withoutAuth and permissions', () => {
app.cleanUp()
},
)

describe('POST /delegation-admin/:zendeskId', () => {
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(
bodyParser.json({
verify: (req: any, res, buf) => {
if (buf && buf.length) {
GunnlaugurG marked this conversation as resolved.
Show resolved Hide resolved
req.rawBody = buf
}
},
}),
),
),
)
GunnlaugurG marked this conversation as resolved.
Show resolved Hide resolved
},
})

server = request(app.getHttpServer())

delegationAdminService = app.get(DelegationAdminCustomService)

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

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

it('POST /delegation-admin/:zendeskId should return 403 Forbidden when user does not have correct headers for the body', async () => {
// Act
const res = await getRequestMethod(
server,
'POST',
)('/delegation-admin/123')
.send({
custom: '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',
})
})
GunnlaugurG marked this conversation as resolved.
Show resolved Hide resolved

it('POST /delegation-admin/:zendeskId should return 201 since the correct headers are set for that body', async () => {
// Act
const res = await getRequestMethod(
server,
'POST',
)('/delegation-admin/123')
.send({
custom: 'test',
})
.set(
'x-zendesk-webhook-signature',
'6sUtGV8C8OdoGgCdsV2xRm3XeskZ33Bc5124RiAK4Q4=',
)
.set('x-zendesk-webhook-signature-timestamp', '2024-10-02T14:21:04Z')

// Assert
expect(res.status).toEqual(201)
})
GunnlaugurG marked this conversation as resolved.
Show resolved Hide resolved
})
})
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)
})
})
})
12 changes: 12 additions & 0 deletions apps/services/auth/admin-api/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { bootstrap } from '@island.is/infra-nest-server'
import { AppModule } from './app/app.module'
import { environment as env } from './environments'
import { openApi } from './openApi'
import bodyParser from 'body-parser'

bootstrap({
appModule: AppModule,
Expand All @@ -14,4 +15,15 @@ bootstrap({
healthCheck: {
database: true,
},
beforeServerStart: async (app) => {
GunnlaugurG marked this conversation as resolved.
Show resolved Hide resolved
app.use(
bodyParser.json({
verify: (req: any, res, buf) => {
if (buf && buf.length) {
req.rawBody = buf
}
},
}),
)
},
GunnlaugurG marked this conversation as resolved.
Show resolved Hide resolved
})
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
Loading
Loading