47ng / prisma-field-encryption

Transparent field-level encryption at rest for Prisma
https://github.com/franky47/prisma-field-encryption-sandbox
MIT License
246 stars 29 forks source link

"not" filter not works in hash fields #94

Closed chenpercy closed 9 months ago

chenpercy commented 9 months ago

This is my prisma schema, and for each patient, they have 0 - N cases.

model Patient {
  uid             String           @id
  cases           Case[]
  ...

  @@map("patient")
}

model Case {
  cid                   BigInt                 @id @default(autoincrement())
  caseNumber            String                 @default("") @map("case_number") /// @encrypted
  caseNumberHash        String?                @map("case_number_hash") /// @encryption:hash(caseNumber)
  patient               Patient?               @relation(fields: [patientId], references: [uid])
  patientId             String?                @map("patient_id")
  ...

  @@map("case")
}

If I query entries with empty caseNumber, it works correctly. However, when I query entries with non-empty caseNumber, it does not work.

# exclude empty caseNumber in table case
db.case.findMany({
  where: { caseNumber: { not: '' } },
});

I still get empty chartNumber in the response

[
  {
    "cid": 47,
    "caseNumber": "",
    "patientId": "ce0d1b78-8858-4122-8c9c-d262927b5480",
    ...
  },
  ...
]

I try to trace code, and finally find the issue would be here. When query parameter is object type, function makeVisitor uses specialSubFields ['equals', 'set'] to handle it, which does not contain not operator.

const makeVisitor = (
  models: DMMFModels,
  visitor: TargetFieldVisitorFn,
  specialSubFields: string[],
  debug: Debugger
) =>
  function visitNode(state: VisitorState, { key, type, node, path }: Item) {
    const model = models[state.currentModel]
    if (!model || !key) {
      return state
    }
    if (type === 'string' && key in model.fields) {
     ...
    }
    // Special cases: {field}.set for updates, {field}.equals for queries
    for (const specialSubField of specialSubFields) {
      if (
        type === 'object' &&
        key in model.fields &&
        typeof (node as any)?.[specialSubField] === 'string'
      ) {
        const value: string = (node as any)[specialSubField]
        const targetField: TargetField = {
          field: key,
          model: state.currentModel,
          fieldConfig: model.fields[key],
          path: [...path, specialSubField].join('.'),
          value
        }
        debug('Visiting %O', targetField)
        visitor(targetField)
        return state
      }
    }
    ...
    return state
  }

export function visitInputTargetFields<
  Models extends string,
  Actions extends string
>(
  params: MiddlewareParams<Models, Actions>,
  models: DMMFModels,
  visitor: TargetFieldVisitorFn
) {
  traverseTree(
    params.args,
    makeVisitor(models, visitor, ['equals', 'set'], debug.encryption), // <---- here
    {
      currentModel: params.model!
    }
  )
}

Solve

Should add other filters like not into specialSubField?

franky47 commented 9 months ago

Yes, it seems like it should replace the empty string value to the hash of said empty string for the query to succeed.

Would you like to open a PR?

franky47 commented 9 months ago

Fixed in 1.5.1 by #95, thanks @chenpercy !