Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions src/server/server.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,12 @@ const conf = convict({
env: 'ZOOM_MINIMAL_MATCHLEVEL',
default: 1
},
minAgeGroup: {
doc: 'Minimal FaceTec age group threshold',
format: Number,
env: 'ZOOM_MINIMAL_AGEGROUP',
default: 3
},
zoomSearchIndexName: {
doc: 'FaceTec 3d DB search index name',
format: '*',
Expand Down
9 changes: 1 addition & 8 deletions src/server/storage/storageAPI.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import { cancelDisposalTask, getDisposalTask } from '../verification/cron/taskUt
import createEnrollmentProcessor from '../verification/processor/EnrollmentProcessor'
import requestRateLimiter from '../utils/requestRateLimiter'
import { default as AdminWallet } from '../blockchain/MultiWallet'
import { deleteFaceId } from '../verification/verificationAPI'
import Logger from '../../imports/logger'

const { fishManager } = stakingModelTasks
Expand Down Expand Up @@ -417,8 +416,7 @@ const setup = (app: Router, storage: StorageAPI) => {
app.post(
'/user/delete',
wrapAsync(async (req, res) => {
const { user, log, body } = req
const { enrollmentIdentifier = '', fvSigner = '' } = body
const { user, log } = req
log.info('delete user', { user })

//first get number of accounts using same crmId before we delete the account
Expand All @@ -430,11 +428,6 @@ const setup = (app: Router, storage: StorageAPI) => {
: 0

const results = await Promise.all([
fvSigner || enrollmentIdentifier
? deleteFaceId(fvSigner, enrollmentIdentifier, user, storage, log)
.then(() => ({ fv: 'ok' }))
.catch(() => ({ fv: 'failed' }))
: Promise.resolve({ fv: 'skipping' }),
(user.identifier ? storage.deleteUser(user) : Promise.reject())
.then(() => ({ mongodb: 'ok' }))
.catch(() => ({ mongodb: 'failed' })),
Expand Down
87 changes: 72 additions & 15 deletions src/server/verification/__tests__/verificationAPI.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import request from 'supertest'
import MockAdapter from 'axios-mock-adapter'

import { assign, omit } from 'lodash'
import moment from 'moment'
import Config from '../../server.config'

import storage from '../../db/mongo/user-privat-provider'
Expand All @@ -15,7 +16,7 @@ import createEnrollmentProcessor from '../processor/EnrollmentProcessor'
import { getToken, getCreds } from '../../__util__/'
import createMockingHelper from '../api/__tests__/__util__'

import { DisposeAt, scheduleDisposalTask, DISPOSE_ENROLLMENTS_TASK, forEnrollment } from '../cron/taskUtil'
import { DisposeAt, scheduleDisposalTask, forEnrollment } from '../cron/taskUtil'

describe('verificationAPI', () => {
let server
Expand Down Expand Up @@ -65,6 +66,7 @@ describe('verificationAPI', () => {
const topWalletMock = jest.fn()
const isVerifiedMock = jest.fn()
const lastAuthenticatedMock = jest.fn()
const getAuthenticationPeriodMock = jest.fn()

const licenseKey = 'fake-license'
const licenseType = ZoomLicenseType.Browser
Expand Down Expand Up @@ -163,6 +165,7 @@ describe('verificationAPI', () => {
AdminWallet.isVerified = isVerifiedMock
AdminWallet.removeWhitelisted = removeWhitelistedMock
AdminWallet.lastAuthenticated = lastAuthenticatedMock
AdminWallet.getAuthenticationPeriod = getAuthenticationPeriodMock

await storage.deleteUser({ identifier: userIdentifier })
await storage.addUser({ identifier: userIdentifier })
Expand All @@ -185,6 +188,7 @@ describe('verificationAPI', () => {
topWalletMock.mockImplementation(noopAsync)
removeWhitelistedMock.mockImplementation(noopAsync)
lastAuthenticatedMock.mockResolvedValue(0)
getAuthenticationPeriodMock.mockResolvedValue(180)
})

afterEach(() => {
Expand Down Expand Up @@ -339,10 +343,14 @@ describe('verificationAPI', () => {
})

test("PUT /verify/face/:enrollmentIdentifier returns duplicate's data", async () => {
const mock = jest.spyOn(storage, 'getTask')
const today = moment()
mock.mockResolvedValue({ createdAt: today.toISOString() })
const expiration = today.add((await AdminWallet.getAuthenticationPeriod()) + 1, 'day').toISOString()

helper.mockEnrollmentNotFound(enrollmentIdentifier)
helper.mockSuccessEnrollment(enrollmentIdentifier)
helper.mockDuplicateFound(enrollmentIdentifier)

await request(server)
.put(enrollmentUri)
.send(payload)
Expand All @@ -351,18 +359,20 @@ describe('verificationAPI', () => {
success: false,
error: helper.duplicateFoundMessage,
enrollmentResult: {
isVerified: false,
isDuplicate: true,
success: true,
error: false,
isDuplicate: true,
duplicate: {
identifier: helper.duplicateEnrollmentIdentifier,
matchLevel: 10
}
matchLevel: 10,
expiration
},
isVerified: false
}
})

await testNotVerified()
mock.mockRestore()
})

test('PUT /verify/face/:enrollmentIdentifier returns 200 and success: false when unexpected error happens', async () => {
Expand Down Expand Up @@ -390,6 +400,28 @@ describe('verificationAPI', () => {
await testNotVerified()
})

test('PUT /verify/face/:enrollmentIdentifier returns 200 and success: false when under age', async () => {
const unexpectedError = 'age check failed'

helper.mockEnrollmentNotFound(enrollmentIdentifier)
helper.mockSuccessEnrollmentUnderAge(enrollmentIdentifier)

await request(server)
.put(enrollmentUri)
.send(payload)
.set('Authorization', `Bearer ${token}`)
.expect(200, {
success: false,
error: unexpectedError,
enrollmentResult: {
isVerified: false,
isUnderAge: true
}
})

await testNotVerified()
})

test('PUT /verify/face/:enrollmentIdentifier passes full verification flow even if user was already verified', async () => {
await storage.updateUser({ identifier: userIdentifier, isVerified: true })

Expand All @@ -402,34 +434,58 @@ describe('verificationAPI', () => {
await testWhitelisted()
})

test('DELETE /verify/face/:enrollmentIdentifier returns 200, success = true and enqueues disposal task if signature is valid', async () => {
test('DELETE /verify/face/:enrollmentIdentifier returns 200, success = true and returns future deletion date', async () => {
mockWhitelisted()
helper.mockEnrollmentFound(enrollmentIdentifier)
const mock = jest.spyOn(storage, 'getTask')
const today = moment()
mock.mockResolvedValue({ createdAt: today.toISOString(), status: 'pending' })
const expiration = today.add((await AdminWallet.getAuthenticationPeriod()) + 1, 'day').toISOString()

await request(server)
.delete(enrollmentUri)
.query({ fvSigner: '0x' + enrollmentIdentifier })
.set('Authorization', `Bearer ${token}`)
.expect(200, { success: true })
.expect(200, { success: true, executeAt: expiration, status: 'pending' })

const filters = forEnrollment(enrollmentIdentifier, DisposeAt.AccountRemoved)
// const filters = forEnrollment(enrollmentIdentifier, DisposeAt.AccountRemoved)

await expect(storage.hasTasksQueued(DISPOSE_ENROLLMENTS_TASK, filters)).resolves.toBe(true)
// await expect(storage.hasTasksQueued(DISPOSE_ENROLLMENTS_TASK, filters)).resolves.toBe(true)
mock.mockRestore()
})

test("DELETE /verify/face/:enrollmentIdentifier returns 200, success = true if user isn't whitelisted", async () => {
test("DELETE /verify/face/:enrollmentIdentifier returns 200, success = true and status: complete if user isn't whitelisted", async () => {
helper.mockEnrollmentFound(enrollmentIdentifier)
const mock = jest.spyOn(storage, 'getTask')
mock.mockResolvedValue(null)
await request(server)
.delete(enrollmentUri)
.query({ signature })
.set('Authorization', `Bearer ${token}`)
.expect(200, {
success: true,
status: 'complete'
})
mock.mockRestore()
})

test('DELETE /verify/face/:enrollmentIdentifier returns 200, success = true and status: complete if user already expired', async () => {
helper.mockEnrollmentFound(enrollmentIdentifier)
const mock = jest.spyOn(storage, 'getTask')
mock.mockResolvedValue({ status: 'complete' })
await request(server)
.delete(enrollmentUri)
.query({ signature })
.set('Authorization', `Bearer ${token}`)
.expect(200, {
success: true
success: true,
status: 'complete'
})
mock.mockRestore()
})

test('DELETE /verify/face/:enrollmentIdentifier returns 400 and success = false if signature is invalid', async () => {
// no longer relevant when just returning expiration
test.skip('DELETE /verify/face/:enrollmentIdentifier returns 400 and success = false if signature is invalid', async () => {
const fakeCreds = await getCreds(true)
const result = await request(server)
.delete(baseUri + '/' + encodeURIComponent(fakeCreds.fvV2Identifier))
Expand All @@ -441,7 +497,7 @@ describe('verificationAPI', () => {
expect(result.body.error).toStartWith('FV identifier signature verification faild')
})

test('DELETE /verify/face/:enrollmentIdentifier returns 200 and success = true if v2 signature is valid', async () => {
test.skip('DELETE /verify/face/:enrollmentIdentifier returns 200 and success = true if v2 signature is valid', async () => {
helper.mockEnrollmentFound(v2Creds.fvV2Identifier.slice(0, 42))

await request(server)
Expand All @@ -458,7 +514,8 @@ describe('verificationAPI', () => {
await testDisposalState(false)
})

test('GET /verify/face/:enrollmentIdentifier returns isDisposing = true if face snapshot has been enqueued for the disposal', async () => {
// no longer valid when cant delete until expiration
test.skip('GET /verify/face/:enrollmentIdentifier returns isDisposing = true if face snapshot has been enqueued for the disposal', async () => {
helper.mockEnrollmentFound(enrollmentIdentifier)
mockWhitelisted()

Expand Down
7 changes: 7 additions & 0 deletions src/server/verification/api/__tests__/__util__/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,12 @@ export default zoomServiceMock => {
faceScanResponse('enrollment', enrollmentPayloadMatcher(enrollmentIdentifier), payload)
}

const mockSuccessEnrollmentUnderAge = enrollmentIdentifier => {
const payload = { externalDatabaseRefID: enrollmentIdentifier, ageV2GroupEnumInt: 2 }

faceScanResponse('enrollment', enrollmentPayloadMatcher(enrollmentIdentifier), payload)
}

const mockFailedEnrollment = (enrollmentIdentifier, withReasonFlags = {}, resultBlob = null) => {
const payloadMatcher = enrollmentPayloadMatcher(enrollmentIdentifier)
let reasonFlags = withReasonFlags
Expand Down Expand Up @@ -305,6 +311,7 @@ export default zoomServiceMock => {
mockSuccessEnrollment,
mockFailedEnrollment,
mockEnrollmentAlreadyExists,
mockSuccessEnrollmentUnderAge,

failedMatchMessage,
mockSuccessUpdateEnrollment,
Expand Down
29 changes: 14 additions & 15 deletions src/server/verification/processor/EnrollmentSession.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// @flow
import { assign, bindAll, get, omit, over } from 'lodash'
import fs from 'fs'
import { assign, bindAll, omit, over } from 'lodash'
import { type IEnrollmentEventPayload } from './typings'
import logger from '../../../imports/logger'
import { DisposeAt, DISPOSE_ENROLLMENTS_TASK, forEnrollment, scheduleDisposalTask } from '../cron/taskUtil'
Expand Down Expand Up @@ -65,19 +64,19 @@ export default class EnrollmentSession {
}

// TODO: remove this after research
if (conf.env.startsWith('prod') && get(exception, 'response.isDuplicate', false)) {
try {
const fileName = `${enrollmentIdentifier}-${exception.response.duplicate.identifier}`
const { auditTrailBase64 } = await this.provider.getEnrollment(exception.response.duplicate.identifier)
let a = Buffer.from(payload.auditTrailImage, 'base64')
let b = Buffer.from(auditTrailBase64, 'base64')
fs.writeFileSync(fileName + '-a.jpg', a)
fs.writeFileSync(fileName + '-b.jpg', b)
log.debug('wrote duplicate file:', { fileName })
} catch (e) {
log.error('failed writing duplicate files', e.message, e)
}
}
// if (conf.env.startsWith('prod') && get(exception, 'response.isDuplicate', false)) {
// try {
// const fileName = `${enrollmentIdentifier}-${exception.response.duplicate.identifier}`
// const { auditTrailBase64 } = await this.provider.getEnrollment(exception.response.duplicate.identifier)
// let a = Buffer.from(payload.auditTrailImage, 'base64')
// let b = Buffer.from(auditTrailBase64, 'base64')
// fs.writeFileSync(fileName + '-a.jpg', a)
// fs.writeFileSync(fileName + '-b.jpg', b)
// log.debug('wrote duplicate file:', { fileName })
// } catch (e) {
// log.error('failed writing duplicate files', e.message, e)
// }
// }

if (shouldLogVerificaitonError(exception)) {
log.error(...logArgs)
Expand Down
12 changes: 12 additions & 0 deletions src/server/verification/processor/__tests__/EnrollmentProcessor.js
Original file line number Diff line number Diff line change
Expand Up @@ -358,4 +358,16 @@ describe('EnrollmentProcessor', () => {
[unexistingEnrollmentIdentifier, enrollmentIdentifier, nonIndexedEnrollmentIdentifier].map(taskId)
)
})

test('enroll() failes when under age', async () => {
helper.mockEnrollmentNotFound(enrollmentIdentifier)
helper.mockSuccessEnrollmentUnderAge(enrollmentIdentifier)

const wrappedResponse = expect(enrollmentProcessor.enroll(user, enrollmentIdentifier, payload)).resolves

await wrappedResponse.toBeDefined()
await wrappedResponse.toHaveProperty('success', false)
await wrappedResponse.toHaveProperty('enrollmentResult.isVerified', false)
await wrappedResponse.toHaveProperty('error', 'age check failed')
})
})
13 changes: 12 additions & 1 deletion src/server/verification/processor/provider/ZoomProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ class ZoomProvider implements IEnrollmentProvider {
logger = null

constructor(api, Config, logger) {
const { skipFaceVerification, disableFaceVerification } = Config
const { skipFaceVerification, disableFaceVerification, minAgeGroup } = Config

this.minAgeGroup = minAgeGroup
this.api = api
this.logger = logger
this.storeRecords = !disableFaceVerification && !skipFaceVerification
Expand Down Expand Up @@ -132,10 +133,20 @@ class ZoomProvider implements IEnrollmentProvider {
alreadyEnrolled,
enrollResult
})

if (enrollResult.ageV2GroupEnumInt < this.minAgeGroup) {
const e = new Error('age check failed')
e.name = 'AgeCheck'
throw e
}
} catch (exception) {
const { name, message, response } = exception

log.warn('enroll failed:', { enrollmentIdentifier, name, message })
if ('AgeCheck' === name) {
await notifyProcessor({ isUnderAge: true })
throwException(message, { isUnderAge: true }, response)
}
// if facemap doesn't match we won't show retry screen
if (FacemapDoesNotMatch === name) {
isNotMatch = true
Expand Down
Loading
Loading