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

Validate package contents #2313

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 3 additions & 2 deletions catalog/app/containers/Bucket/errors.tsx
@@ -1,3 +1,4 @@
import type Ajv from 'ajv'
import * as R from 'ramda'
import * as React from 'react'
import * as redux from 'react-redux'
Expand Down Expand Up @@ -43,7 +44,7 @@ export class FileNotFound extends BucketError {}
export class VersionNotFound extends BucketError {}

export interface BucketPreferencesInvalidProps {
errors: { dataPath: string; message: string }[]
errors: Ajv.ErrorObject[]
}

export class BucketPreferencesInvalid extends BucketError {
Expand All @@ -58,7 +59,7 @@ export class BucketPreferencesInvalid extends BucketError {
}

export interface WorkflowsConfigInvalidProps {
errors: { dataPath: string; message: string }[]
errors: Ajv.ErrorObject[]
}

export class WorkflowsConfigInvalid extends BucketError {
Expand Down
144 changes: 108 additions & 36 deletions catalog/app/containers/Bucket/requests/package.ts
@@ -1,3 +1,4 @@
import type Ajv from 'ajv'
import type { S3 } from 'aws-sdk'
import * as R from 'ramda'
import * as React from 'react'
Expand All @@ -6,11 +7,67 @@ import { JsonValue } from 'components/JsonEditor/constants'
import * as APIConnector from 'utils/APIConnector'
import * as AWS from 'utils/AWS'
import * as Config from 'utils/Config'
import { makeSchemaDefaultsSetter, JsonSchema } from 'utils/json-schema'
import {
makeSchemaDefaultsSetter,
makeSchemaValidator,
JsonSchema,
} from 'utils/json-schema'
import mkSearch from 'utils/mkSearch'
import pipeThru from 'utils/pipeThru'
import * as s3paths from 'utils/s3paths'
import * as workflows from 'utils/workflows'

import * as errors from '../errors'
import * as requests from './requestsUntyped'

export const objectSchema = async ({ s3, schemaUrl }: { s3: S3; schemaUrl: string }) => {
if (!schemaUrl) return null

const { bucket, key, version } = s3paths.parseS3Url(schemaUrl)

try {
const response = await requests.fetchFile({ s3, bucket, path: key, version })
return JSON.parse(response.Body.toString('utf-8'))
} catch (e) {
if (e instanceof errors.FileNotFound || e instanceof errors.VersionNotFound) throw e

// eslint-disable-next-line no-console
console.log('Unable to fetch')
// eslint-disable-next-line no-console
console.error(e)
}

return null
}

function formatErrorMessage(validationErrors: Ajv.ErrorObject[]): string {
const { dataPath, message = '' } = validationErrors[0]
return dataPath ? `"${dataPath}" ${message}` : message
}

async function validatePackageManifest(
s3: S3,
body: $TSFixMe,
schemaUrl?: string,
): Promise<Error | undefined> {
if (!schemaUrl) return undefined

const schema = await objectSchema({
s3,
schemaUrl,
})
const normalizedBody = {
contents: body.entries || body.contents,
message: body.message,
meta: body.meta,
workflow: body.workflow,
}
const validationErrors = makeSchemaValidator(schema)(normalizedBody)
if (!validationErrors.length) return undefined

return new Error(formatErrorMessage(validationErrors))
}

interface AWSCredentials {
accessKeyId: string
secretAccessKey: string
Expand Down Expand Up @@ -233,6 +290,17 @@ const mkCreatePackage =
Body: payload,
})
const res = await upload.promise()

const error = await validatePackageManifest(
s3,
{
...header,
contents,
},
workflow.manifestSchema,
)
if (error) throw error

return makeBackendRequest(
req,
ENDPOINT_CREATE,
Expand All @@ -253,6 +321,7 @@ export function useCreatePackage() {
}

const copyPackage = async (
s3: S3,
req: ApiRequest,
credentials: AWSCredentials,
{ message, meta, source, target, workflow }: CopyPackageParams,
Expand All @@ -261,32 +330,33 @@ const copyPackage = async (
// refresh credentials and load if they are not loaded
await credentials.getPromise()

return makeBackendRequest(
req,
ENDPOINT_COPY,
{
message,
meta: getMetaValue(meta, schema),
name: target.name,
parent: {
top_hash: source.revision,
registry: `s3://${source.bucket}`,
name: source.name,
},
registry: `s3://${target.bucket}`,
workflow: getWorkflowApiParam(workflow.slug),
const body = {
message,
meta: getMetaValue(meta, schema),
name: target.name,
parent: {
top_hash: source.revision,
registry: `s3://${source.bucket}`,
name: source.name,
},
getCredentialsQuery(credentials),
)
registry: `s3://${target.bucket}`,
workflow: getWorkflowApiParam(workflow.slug),
}

const error = await validatePackageManifest(s3, body, workflow.manifestSchema)
if (error) throw error

return makeBackendRequest(req, ENDPOINT_COPY, body, getCredentialsQuery(credentials))
}

export function useCopyPackage() {
const credentials = AWS.Credentials.use()
const req: ApiRequest = APIConnector.use()
const s3 = AWS.S3.use()
return React.useCallback(
(params: CopyPackageParams, schema?: JsonSchema) =>
copyPackage(req, credentials, params, schema),
[credentials, req],
copyPackage(s3, req, credentials, params, schema),
[credentials, req, s3],
)
}

Expand Down Expand Up @@ -320,6 +390,7 @@ export function useDeleteRevision() {
}

const wrapPackage = async (
s3: S3,
req: ApiRequest,
credentials: AWSCredentials,
{ message, meta, source, target, workflow, entries }: WrapPackageParams,
Expand All @@ -328,30 +399,31 @@ const wrapPackage = async (
// refresh credentials and load if they are not loaded
await credentials.getPromise()

return makeBackendRequest(
req,
ENDPOINT_WRAP,
{
dst: {
registry: `s3://${target.bucket}`,
name: target.name,
},
entries,
message,
meta: getMetaValue(meta, schema),
registry: `s3://${source}`,
workflow: getWorkflowApiParam(workflow.slug),
const body = {
dst: {
registry: `s3://${target.bucket}`,
name: target.name,
},
getCredentialsQuery(credentials),
)
entries,
message,
meta: getMetaValue(meta, schema),
registry: `s3://${source}`,
workflow: getWorkflowApiParam(workflow.slug),
}

const error = await validatePackageManifest(s3, body, workflow.manifestSchema)
if (error) throw error

return makeBackendRequest(req, ENDPOINT_WRAP, body, getCredentialsQuery(credentials))
}

export function useWrapPackage() {
const credentials = AWS.Credentials.use()
const req: ApiRequest = APIConnector.use()
const s3 = AWS.S3.use()
return React.useCallback(
(params: WrapPackageParams, schema?: JsonSchema) =>
wrapPackage(req, credentials, params, schema),
[credentials, req],
wrapPackage(s3, req, credentials, params, schema),
[credentials, req, s3],
)
}
6 changes: 4 additions & 2 deletions catalog/app/utils/json-schema/json-schema.ts
Expand Up @@ -134,7 +134,9 @@ export function doesTypeMatchSchema(value: any, optSchema?: JsonSchema): boolean

export const EMPTY_SCHEMA = {}

export function makeSchemaValidator(optSchema?: JsonSchema) {
export function makeSchemaValidator(
optSchema?: JsonSchema,
): (obj?: any) => Ajv.ErrorObject[] {
const schema = optSchema || EMPTY_SCHEMA

const ajv = new Ajv({ useDefaults: true, schemaId: 'auto' })
Expand All @@ -149,7 +151,7 @@ export function makeSchemaValidator(optSchema?: JsonSchema) {
}
} catch (e) {
// TODO: add custom errors
return () => [e]
return () => [e as Ajv.ErrorObject]
}
}

Expand Down
3 changes: 3 additions & 0 deletions catalog/app/utils/workflows.ts
Expand Up @@ -19,6 +19,7 @@ interface WorkflowYaml {
name: string
description?: string
metadata_schema?: string
object_schema?: string
is_message_required?: boolean
}

Expand All @@ -42,6 +43,7 @@ export interface Workflow {
description?: string
isDefault: boolean
isDisabled: boolean
manifestSchema?: string
name?: string
schema?: Schema
slug: string | typeof notAvailable | typeof notSelected
Expand Down Expand Up @@ -93,6 +95,7 @@ function parseWorkflow(
description: workflow.description,
isDefault: workflowSlug === data.default_workflow,
isDisabled: false,
manifestSchema: data.schemas?.[workflow.object_schema || '']?.url,
name: workflow.name,
schema: parseSchema(workflow.metadata_schema, data.schemas),
slug: workflowSlug,
Expand Down