Skip to content

Commit

Permalink
Fixing race condition with PERMITs before EvaluationLog is insertted
Browse files Browse the repository at this point in the history
  • Loading branch information
mattschoch committed Aug 26, 2024
1 parent bc8c8a9 commit a5d6783
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 144 deletions.
2 changes: 1 addition & 1 deletion apps/armory/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ ARMORY_DATABASE_SCHEMA := ${ARMORY_PROJECT_DIR}/src/shared/module/persistence/sc
# === Start ===

armory/start/dev:
npx nx serve ${ARMORY_PROJECT_NAME}
npx nx serve ${ARMORY_PROJECT_NAME} -- --port 9230

# === Setup ===

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,25 +143,9 @@ export class AuthorizationRequestService {
})

const status = getStatus(evaluation.decision)

const authzRequest = await this.authzRequestRepository.update({
id: input.id,
clientId: input.clientId,
status,
evaluations: [
{
id: uuid(),
decision: evaluation.decision,
signature: evaluation.accessToken?.value || null,
approvalRequirements: evaluation.approvals,
transactionRequestIntent: evaluation.transactionRequestIntent,
createdAt: new Date()
}
]
})

// NOTE: we will track the transfer before we update the status to PERMITTED so that we don't have a brief window where a second transfer can come in before the history is tracked.
// TODO: (@wcalderipe, 01/02/24) Move to the TransferTrackingService.
if (authzRequest.request.action === Action.SIGN_TRANSACTION && status === AuthorizationRequestStatus.PERMITTED) {
if (input.request.action === Action.SIGN_TRANSACTION && status === AuthorizationRequestStatus.PERMITTED) {
// TODO: (@wcalderipe, 08/02/24) Remove the cast `as Intent`.
const intent = evaluation.transactionRequestIntent as Intent

Expand All @@ -183,13 +167,13 @@ export class AuthorizationRequestService {
}

const transfer = {
resourceId: authzRequest.request.resourceId,
clientId: authzRequest.clientId,
requestId: authzRequest.id,
resourceId: input.request.resourceId,
clientId: input.clientId,
requestId: input.id,
from: intent.from,
to: intent.to,
token: intent.token,
chainId: authzRequest.request.transactionRequest.chainId,
chainId: input.request.transactionRequest.chainId,
initiatedBy: evaluation.principal?.userId,
createdAt: new Date(),
amount: BigInt(intent.amount),
Expand All @@ -200,6 +184,22 @@ export class AuthorizationRequestService {
}
}

const authzRequest = await this.authzRequestRepository.update({
id: input.id,
clientId: input.clientId,
status,
evaluations: [
{
id: uuid(),
decision: evaluation.decision,
signature: evaluation.accessToken?.value || null,
approvalRequirements: evaluation.approvals,
transactionRequestIntent: evaluation.transactionRequestIntent,
createdAt: new Date()
}
]
})

this.logger.log('Authorization request status updated', {
clientId: authzRequest.clientId,
id: authzRequest.id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { Prisma } from '@prisma/client/armory'
import { v4 as uuid } from 'uuid'
import { ApplicationException } from '../../../shared/exception/application.exception'
import { PrismaService } from '../../../shared/module/persistence/service/prisma.service'
import { decodeAuthorizationRequest, decodeEvaluationLog } from '../decode/authorization-request.decode'
import { decodeAuthorizationRequest } from '../decode/authorization-request.decode'
import { createRequestSchema } from '../schema/request.schema'

@Injectable()
Expand All @@ -25,39 +25,46 @@ export class AuthorizationRequestRepository {
const approvals = (input.approvals || []).map((sig) => ({ sig }))
const errors = this.toErrors(clientId, input.errors)

await this.prismaService.authorizationRequest.create({
data: {
id,
status,
clientId,
request,
idempotencyKey,
createdAt,
updatedAt,
action: request.action,
authnSig: authentication,
metadata,
approvals: {
createMany: {
data: approvals
const evaluationLogs = this.toEvaluationLogs({ requestId: id, clientId, evaluations: input.evaluations })
const approvalRequirements = this.toApprovalRequirements(input.evaluations)

await this.prismaService.$transaction(async (prisma) => {
await prisma.authorizationRequest.create({
data: {
id,
status,
clientId,
request,
idempotencyKey,
createdAt,
updatedAt,
action: request.action,
authnSig: authentication,
metadata,
approvals: {
createMany: {
data: approvals
}
},
errors: {
createMany: {
data: errors
}
}
},
errors: {
createMany: {
data: errors
}
include: {
approvals: true,
errors: true
}
},
include: {
approvals: true,
errors: true
}
})
})

await prisma.evaluationLog.createMany({
data: evaluationLogs
})

await this.createEvaluationLogs({
requestId: id,
clientId,
evaluations: input.evaluations
await prisma.approvalRequirement.createMany({
data: approvalRequirements
})
})

const authRequest = await this.findById(id)
Expand All @@ -70,40 +77,6 @@ export class AuthorizationRequestRepository {
return authRequest
}

private async createEvaluationLogs({
requestId,
clientId,
evaluations
}: {
requestId: string
clientId?: string
evaluations?: Evaluation[]
}) {
const evaluationLogs = this.toEvaluationLogs({ requestId, clientId, evaluations })
const approvalRequirements = this.toApprovalRequirements(evaluations)

await this.prismaService.evaluationLog.createMany({
data: evaluationLogs
})

await this.prismaService.approvalRequirement.createMany({
data: approvalRequirements
})

return this.getEvaluationLogs(requestId)
}

private async getEvaluationLogs(requestId: string) {
const evaluationLogs = await this.prismaService.evaluationLog.findMany({
where: { requestId },
include: {
approvals: true
}
})

return evaluationLogs.map(decodeEvaluationLog)
}

private toEvaluationLogs({
requestId,
clientId,
Expand Down Expand Up @@ -185,34 +158,39 @@ export class AuthorizationRequestRepository {
(sig) => ({ sig })
)
const errors = this.toErrors(clientId, input.errors)
// TODO (@wcalderipe, 19/01/24): Cover the skipDuplicate with tests.
await this.prismaService.authorizationRequest.update({
where: { id },
data: {
status,
approvals: {
createMany: {
data: approvals,
skipDuplicates: true
}
},
errors: {
createMany: {
data: errors,
skipDuplicates: true
}

const updateData: Prisma.AuthorizationRequestUpdateInput = {
status,
approvals: {
createMany: {
data: approvals,
skipDuplicates: true
}
},
include: {
approvals: true,
errors: true
errors: {
createMany: {
data: errors,
skipDuplicates: true
}
}
})
}
const evaluationLogs = this.toEvaluationLogs({ requestId: id, clientId, evaluations: input.evaluations })
const approvalRequirements = this.toApprovalRequirements(input.evaluations)

// Do the update in a tx to avoid partial updates.
await this.prismaService.$transaction(async (prisma) => {
await prisma.authorizationRequest.update({
where: { id },
data: updateData
})

await this.createEvaluationLogs({
requestId: id,
clientId,
evaluations: input.evaluations
await prisma.evaluationLog.createMany({
data: evaluationLogs
})

await prisma.approvalRequirement.createMany({
data: approvalRequirements
})
})

const authRequest = await this.findById(id)
Expand Down
Loading

0 comments on commit a5d6783

Please sign in to comment.