bde148f286
* wip * some extra fixes * stuff kinda works? * need to figure out mocks * need to figure out mocks * fix db listener * gqlgen fix * minor gqlgen watch adjustment * lint fixes * delete old codegen file * converting migrations to ESM * getModuleDIrectory * vitest sort of works * added back ts-vitest * resolve gql double load * fixing test timeout configs * TSC lint fix * fix automate tests * moar debugging * debugging * more debugging * codegen update * server works * yargs migrated * chore(server): getting rid of global mocks for Server ESM (#5046) * got rid of email mock * got rid of comment mocks * got rid of multi region mocks * got rid of stripe mock * admin override mock updated * removed final mock * fixing import.meta.resolve calls * another import.meta.resolve fix * added requested test * nyc ESM fix * removed unneeded deps + linting * yarn lock forgot to commit * tryna fix flakyness * email capture util fix * sendEmail fix * fix TSX check * sender transporter fix + CR comments * merge main fix * test fixx * circleci fix * gqlgen bigint fix * error formatter fix * more error formatting improvements * esmloader added to Dockerfile * more dockerfile fixes * bg jobs fix
180 lines
5.7 KiB
TypeScript
180 lines
5.7 KiB
TypeScript
import { getEncryptionKeysPath } from '@/modules/shared/helpers/envHelper'
|
|
import { packageRoot } from '@/bootstrap'
|
|
import path from 'node:path'
|
|
import fs from 'node:fs/promises'
|
|
import { has, isArray, isObjectLike } from 'lodash-es'
|
|
import { Nullable, Optional } from '@speckle/shared'
|
|
import { MisconfiguredEnvironmentError } from '@/modules/shared/errors'
|
|
import { AutomationFunctionInputEncryptionError } from '@/modules/automate/errors/management'
|
|
import { KeyPair, buildDecryptor } from '@/modules/shared/utils/libsodium'
|
|
import { AutomationRevisionFunctionRecord } from '@/modules/automate/helpers/types'
|
|
import { AutomationRevisionFunctionGraphQLReturn } from '@/modules/automate/helpers/graphTypes'
|
|
import { FunctionReleaseSchemaType } from '@/modules/automate/helpers/executionEngine'
|
|
import { LibsodiumEncryptionError } from '@/modules/shared/errors/encryption'
|
|
import { Merge } from 'type-fest'
|
|
import { convertFunctionReleaseToGraphQLReturn } from '@/modules/automate/services/functionManagement'
|
|
import { redactWriteOnlyInputData } from '@/modules/automate/utils/jsonSchemaRedactor'
|
|
import {
|
|
GetEncryptionKeyPair,
|
|
GetEncryptionKeyPairFor
|
|
} from '@/modules/automate/domain/operations'
|
|
|
|
type KeysFileContents = Array<KeyPair>
|
|
|
|
let keys: Optional<KeysFileContents> = undefined
|
|
|
|
const isKeysFileContents = (
|
|
fileContents: unknown
|
|
): fileContents is KeysFileContents => {
|
|
return (
|
|
isArray(fileContents) &&
|
|
fileContents.length > 0 &&
|
|
fileContents.every((entry) => has(entry, 'publicKey') && has(entry, 'privateKey'))
|
|
)
|
|
}
|
|
|
|
const getEncryptionKeys = async () => {
|
|
if (keys) return keys
|
|
|
|
const relativePath = getEncryptionKeysPath()
|
|
const fullPath = path.resolve(packageRoot, relativePath)
|
|
const file = await fs.readFile(fullPath, 'utf-8')
|
|
|
|
const parsedJson = JSON.parse(file)
|
|
const keysFileContents = isKeysFileContents(parsedJson) ? parsedJson : null
|
|
if (!keysFileContents)
|
|
throw new MisconfiguredEnvironmentError('Invalid encryption keys file format')
|
|
|
|
keys = keysFileContents
|
|
return keys
|
|
}
|
|
|
|
export const getEncryptionKeyPair: GetEncryptionKeyPair = async () => {
|
|
return (await getEncryptionKeys())[0]
|
|
}
|
|
|
|
export const getEncryptionPublicKey = async () => {
|
|
return (await getEncryptionKeyPair()).publicKey
|
|
}
|
|
|
|
export const getEncryptionKeyPairFor: GetEncryptionKeyPairFor = async (
|
|
publicKey: string
|
|
) => {
|
|
const keyPairs = await getEncryptionKeys()
|
|
const keyPair = keyPairs.find((keyPair) => keyPair.publicKey === publicKey)
|
|
if (!keyPair) {
|
|
throw new MisconfiguredEnvironmentError(
|
|
'Environment does not have a key pair for the requested public key',
|
|
{
|
|
info: { publicKey }
|
|
}
|
|
)
|
|
}
|
|
|
|
return keyPair
|
|
}
|
|
|
|
const isValidInputObject = (
|
|
input: unknown
|
|
): input is Nullable<Record<string, unknown>> => {
|
|
return input === null || (!isArray(input) && isObjectLike(input))
|
|
}
|
|
|
|
export type GetFunctionInputDecryptorDeps = {
|
|
buildDecryptor: typeof buildDecryptor
|
|
}
|
|
|
|
export const getFunctionInputDecryptorFactory =
|
|
(deps: GetFunctionInputDecryptorDeps) => async (params: { keyPair: KeyPair }) => {
|
|
const { buildDecryptor } = deps
|
|
const { keyPair } = params
|
|
|
|
const coreDecryptor = await buildDecryptor(keyPair)
|
|
|
|
const decryptInputs = async (data: Nullable<string>) => {
|
|
if (data === null) return null
|
|
|
|
const decryptedString = await coreDecryptor.decrypt(data)
|
|
const inputsObject = JSON.parse(decryptedString)
|
|
if (!isValidInputObject(inputsObject)) {
|
|
throw new AutomationFunctionInputEncryptionError(
|
|
'Decrypted input is not a valid inputs object'
|
|
)
|
|
}
|
|
|
|
return inputsObject
|
|
}
|
|
|
|
return {
|
|
decryptInputs,
|
|
dispose: coreDecryptor.dispose
|
|
}
|
|
}
|
|
export type FunctionInputDecryptor = ReturnType<typeof getFunctionInputDecryptorFactory>
|
|
|
|
export type GetFunctionInputsForFrontendDeps = {
|
|
getEncryptionKeyPairFor: GetEncryptionKeyPairFor
|
|
redactWriteOnlyInputData: typeof redactWriteOnlyInputData
|
|
} & GetFunctionInputDecryptorDeps
|
|
|
|
export type AutomationRevisionFunctionForInputRedaction = Merge<
|
|
AutomationRevisionFunctionRecord,
|
|
{ release: FunctionReleaseSchemaType }
|
|
>
|
|
|
|
export const getFunctionInputsForFrontendFactory =
|
|
(deps: GetFunctionInputsForFrontendDeps) =>
|
|
async (params: {
|
|
fns: Array<AutomationRevisionFunctionForInputRedaction>
|
|
publicKey: string
|
|
}) => {
|
|
const { getEncryptionKeyPairFor, redactWriteOnlyInputData } = deps
|
|
const { fns, publicKey } = params
|
|
|
|
const keyPair = await getEncryptionKeyPairFor(publicKey)
|
|
const inputDecryptor = await getFunctionInputDecryptorFactory(deps)({ keyPair })
|
|
|
|
let results: AutomationRevisionFunctionGraphQLReturn[] = []
|
|
try {
|
|
results = await Promise.all(
|
|
fns.map(async (fn) => {
|
|
let inputs = await inputDecryptor.decryptInputs(fn.functionInputs)
|
|
const schema = fn.release.inputSchema
|
|
|
|
if (schema || inputs) {
|
|
inputs = redactWriteOnlyInputData(inputs, schema)
|
|
}
|
|
|
|
return {
|
|
...fn,
|
|
functionInputs: inputs,
|
|
release: convertFunctionReleaseToGraphQLReturn({
|
|
...fn.release,
|
|
functionId: fn.functionId
|
|
})
|
|
}
|
|
})
|
|
)
|
|
} catch (e) {
|
|
if (e instanceof AutomationFunctionInputEncryptionError) {
|
|
throw new AutomationFunctionInputEncryptionError(
|
|
'One or more function inputs are not proper input objects',
|
|
{ cause: e }
|
|
)
|
|
}
|
|
|
|
if (e instanceof LibsodiumEncryptionError) {
|
|
throw new AutomationFunctionInputEncryptionError(
|
|
'Failed to decrypt one or more function inputs, they might not have been properly encrypted',
|
|
{ cause: e }
|
|
)
|
|
}
|
|
|
|
throw e
|
|
} finally {
|
|
inputDecryptor.dispose()
|
|
}
|
|
|
|
return results
|
|
}
|