Skip to content

Commit c7c15d7

Browse files
committed
feat: aligned schemas with standard schema behaviour
1 parent e9abc78 commit c7c15d7

File tree

5 files changed

+247
-129
lines changed

5 files changed

+247
-129
lines changed

ref.schema.json

Lines changed: 0 additions & 14 deletions
This file was deleted.

src/errors.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,10 +202,15 @@ class ErrorPolykeyCLITouchSecret<T> extends ErrorPolykeyCLI<T> {
202202
}
203203

204204
class ErrorPolykeyCLIInvalidJWT<T> extends ErrorPolykeyCLI<T> {
205-
static description: 'JWT is not valid';
205+
static description = 'JWT is not valid';
206206
exitCode = sysexits.USAGE;
207207
}
208208

209+
class ErrorPolykeyCLISchemaInvalid<T> extends ErrorPolykeyCLI<T> {
210+
static description = 'The provided JSON schema is invalid';
211+
exitCode = sysexits.CONFIG;
212+
}
213+
209214
export {
210215
ErrorPolykeyCLI,
211216
ErrorPolykeyCLIUncaughtException,
@@ -236,4 +241,5 @@ export {
236241
ErrorPolykeyCLIEditSecret,
237242
ErrorPolykeyCLITouchSecret,
238243
ErrorPolykeyCLIInvalidJWT,
244+
ErrorPolykeyCLISchemaInvalid,
239245
};

src/secrets/CommandEnv.ts

Lines changed: 34 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
11
import type PolykeyClient from 'polykey/PolykeyClient.js';
2-
import type {
3-
JSONSchema,
4-
JSONSchemaInfo,
5-
ParsedSecretPathValue,
6-
} from '../types.js';
2+
import type { ParsedSecretPathValue } from '../types.js';
73
import path from 'node:path';
84
import os from 'node:os';
95
import $RefParser from '@apidevtools/json-schema-ref-parser';
10-
import { Ajv2019 as Ajv } from 'ajv/dist/2019.js';
6+
import { Ajv2019 } from 'ajv/dist/2019.js';
117
import { InvalidArgumentError } from 'commander';
128
import CommandPolykey from '../CommandPolykey.js';
139
import * as binProcessors from '../utils/processors.js';
@@ -126,65 +122,25 @@ class CommandEnv extends CommandPolykey {
126122
logger: this.logger.getChild(PolykeyClient.name),
127123
});
128124

129-
let schema: JSONSchema | undefined = undefined;
130-
let unwrappedSchema: JSONSchemaInfo | undefined = undefined;
131-
if (options.egressSchema != null) {
132-
schema = (await $RefParser.bundle(
133-
options.egressSchema,
134-
)) satisfies JSONSchema;
135-
unwrappedSchema = binUtils.loadSchema(schema!);
136-
}
137-
138125
// Getting envs
139126
const [envp] = await binUtils.retryAuthentication(async (auth) => {
140127
const responseStream =
141128
await pkClient.rpcClient.methods.vaultsSecretsEnv();
142129

143-
// Writing desired secrets
130+
// Writing desired secrets. Attempt to get all the required secrets.
131+
// If the schema is provided, then the resulting variables will be
132+
// validated.
144133
const secretRenameMap = new Map<string, string | undefined>();
145134
const writer = responseStream.writable.getWriter();
146135
let first = true;
147136
for (const envVariable of envVariables) {
148137
const [nameOrId, secretName, secretNameNew] = envVariable;
149138
secretRenameMap.set(secretName ?? '/', secretNameNew);
150-
151-
// If there is no secret name provided, then attempt to export the
152-
// secrets from the entire vault. Otherwise, check if the selected
153-
// secret exists in the schema before requesting it. This will
154-
// only run if a schema has been specified.
155-
if (schema != null && unwrappedSchema != null) {
156-
const { allKeys } = unwrappedSchema;
157-
if (nameOrId != null && secretName == null) {
158-
// Only vault specified
159-
for (const key of allKeys) {
160-
// When exporting secrets from a vault, it is impossible to
161-
// rename the resulting secrets.
162-
await writer.write({
163-
nameOrId: nameOrId,
164-
secretName: key,
165-
metadata: first ? auth : undefined,
166-
});
167-
}
168-
} else {
169-
// Individual secret name specified
170-
const name: string =
171-
secretNameNew != null ? secretNameNew : secretName!;
172-
if (allKeys.includes(name)) {
173-
await writer.write({
174-
nameOrId: nameOrId,
175-
secretName: secretName!,
176-
metadata: first ? auth : undefined,
177-
});
178-
}
179-
}
180-
} else {
181-
// No schema specified
182-
await writer.write({
183-
nameOrId: nameOrId,
184-
secretName: secretName ?? '/',
185-
metadata: first ? auth : undefined,
186-
});
187-
}
139+
await writer.write({
140+
nameOrId: nameOrId,
141+
secretName: secretName ?? '/',
142+
metadata: first ? auth : undefined,
143+
});
188144
first = false;
189145
}
190146
await writer.close();
@@ -207,10 +163,6 @@ class CommandEnv extends CommandPolykey {
207163
`TMP Vault "${value.data?.nameOrId}" does not exist`,
208164
);
209165
case 'ENOENT':
210-
// If we are working with schemas, then missing keys will be
211-
// validated later.
212-
if (unwrappedSchema != null) break;
213-
214166
// It is expected for the data to be populated with the
215167
// offending secret and vault name if a secret was not found.
216168
throw new Error(
@@ -301,44 +253,34 @@ class CommandEnv extends CommandPolykey {
301253
};
302254
}
303255

304-
// Apply defaults using the schema
305-
const filteredEnvp: Record<string, string> = {};
306-
if (schema != null && unwrappedSchema != null) {
307-
// Parse the schema for manual filtering
308-
const { requiredKeys, allKeys, defaults } = unwrappedSchema;
256+
// Validate the schema
257+
if (options.egressSchema != null) {
258+
// Compose the schema as ajv cannot parse cross-schema refs
259+
const schema = await $RefParser.bundle(options.egressSchema);
309260

310-
// Add allowed secrets to a filtered set of secrets. This runs after
311-
// the duplication is processed, so all secrets here are guaranteed
312-
// to be unique.
313-
for (const key of allKeys) {
314-
let value = envp[key];
315-
if (value == null && defaults[key] != null) {
316-
value = defaults[key];
317-
}
318-
if (
319-
requiredKeys.includes(key) &&
320-
(value == null || value === '')
321-
) {
322-
throw new binErrors.ErrorPolykeyCLIMissingRequiredEnvName(
323-
`Expected definition for ${key}`,
324-
);
325-
}
326-
if (value != null) {
327-
filteredEnvp[key] = value.toString();
328-
}
329-
}
330-
331-
// Validate the schema using ajv. All defaults have already been
332-
// applied. This is now the final state of the exported variables.
333-
const ajv = new Ajv({ allErrors: true });
261+
// Validate the schema using ajv. This will also apply defaults and
262+
// coerce types as necessary.
263+
const ajv = new Ajv2019({
264+
strict: true,
265+
allErrors: true,
266+
useDefaults: true,
267+
coerceTypes: true,
268+
});
334269
const validate = ajv.compile(schema);
335-
validate(envp);
270+
const valid = validate(envp);
271+
if (!valid && validate.errors != null) {
272+
throw new binErrors.ErrorPolykeyCLISchemaInvalid(
273+
'JSON schema validation failed',
274+
{
275+
data: {
276+
errors: [...validate.errors],
277+
},
278+
},
279+
);
280+
}
336281
}
337282

338-
return [
339-
utils.isEmptyObject(filteredEnvp) ? envp : filteredEnvp,
340-
envpPath,
341-
];
283+
return [envp, envpPath];
342284
}, meta);
343285
// End connection early to avoid errors on server
344286
await pkClient.stop();

test.schema.json

Lines changed: 0 additions & 11 deletions
This file was deleted.

0 commit comments

Comments
 (0)