Skip to content

Commit 2860f00

Browse files
authored
feat: implement openapi security inferrence and override (#341)
1 parent d996d09 commit 2860f00

File tree

11 files changed

+185
-108
lines changed

11 files changed

+185
-108
lines changed

packages/plugins/openapi/src/generator.ts

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
// Inspired by: https://github.com/omar-dulaimi/prisma-trpc-generator
22

33
import { DMMF } from '@prisma/generator-helper';
4-
import { AUXILIARY_FIELDS, getDataModels, hasAttribute, PluginError, PluginOptions } from '@zenstackhq/sdk';
4+
import {
5+
analyzePolicies,
6+
AUXILIARY_FIELDS,
7+
getDataModels,
8+
hasAttribute,
9+
PluginError,
10+
PluginOptions,
11+
} from '@zenstackhq/sdk';
512
import { DataModel, isDataModel, type Model } from '@zenstackhq/sdk/ast';
613
import {
714
addMissingInputObjectTypesForAggregate,
@@ -201,11 +208,15 @@ export class OpenAPIGenerator {
201208
inputType?: object;
202209
outputType: object;
203210
successCode?: number;
211+
security?: Array<Record<string, string[]>>;
204212
};
205213

206214
const definitions: OperationDefinition[] = [];
207215
const hasRelation = zmodel.fields.some((f) => isDataModel(f.type.reference?.ref));
208216

217+
// analyze access policies to determine default security
218+
const { create, read, update, delete: del } = analyzePolicies(zmodel);
219+
209220
if (ops['createOne']) {
210221
definitions.push({
211222
method: 'post',
@@ -225,6 +236,7 @@ export class OpenAPIGenerator {
225236
outputType: this.ref(model.name),
226237
description: `Create a new ${model.name}`,
227238
successCode: 201,
239+
security: create === true ? [] : undefined,
228240
});
229241
}
230242

@@ -245,6 +257,7 @@ export class OpenAPIGenerator {
245257
outputType: this.ref('BatchPayload'),
246258
description: `Create several ${model.name}`,
247259
successCode: 201,
260+
security: create === true ? [] : undefined,
248261
});
249262
}
250263

@@ -266,6 +279,7 @@ export class OpenAPIGenerator {
266279
),
267280
outputType: this.ref(model.name),
268281
description: `Find one unique ${model.name}`,
282+
security: read === true ? [] : undefined,
269283
});
270284
}
271285

@@ -287,6 +301,7 @@ export class OpenAPIGenerator {
287301
),
288302
outputType: this.ref(model.name),
289303
description: `Find the first ${model.name} matching the given condition`,
304+
security: read === true ? [] : undefined,
290305
});
291306
}
292307

@@ -308,6 +323,7 @@ export class OpenAPIGenerator {
308323
),
309324
outputType: this.array(this.ref(model.name)),
310325
description: `Find a list of ${model.name}`,
326+
security: read === true ? [] : undefined,
311327
});
312328
}
313329

@@ -330,6 +346,7 @@ export class OpenAPIGenerator {
330346
),
331347
outputType: this.ref(model.name),
332348
description: `Update a ${model.name}`,
349+
security: update === true ? [] : undefined,
333350
});
334351
}
335352

@@ -350,6 +367,7 @@ export class OpenAPIGenerator {
350367
),
351368
outputType: this.ref('BatchPayload'),
352369
description: `Update ${model.name}s matching the given condition`,
370+
security: update === true ? [] : undefined,
353371
});
354372
}
355373

@@ -373,6 +391,7 @@ export class OpenAPIGenerator {
373391
),
374392
outputType: this.ref(model.name),
375393
description: `Upsert a ${model.name}`,
394+
security: create === true && update == true ? [] : undefined,
376395
});
377396
}
378397

@@ -394,6 +413,7 @@ export class OpenAPIGenerator {
394413
),
395414
outputType: this.ref(model.name),
396415
description: `Delete one unique ${model.name}`,
416+
security: del === true ? [] : undefined,
397417
});
398418
}
399419

@@ -413,6 +433,7 @@ export class OpenAPIGenerator {
413433
),
414434
outputType: this.ref('BatchPayload'),
415435
description: `Delete ${model.name}s matching the given condition`,
436+
security: del === true ? [] : undefined,
416437
});
417438
}
418439

@@ -433,6 +454,7 @@ export class OpenAPIGenerator {
433454
),
434455
outputType: this.oneOf({ type: 'integer' }, this.ref(`${model.name}CountAggregateOutputType`)),
435456
description: `Find a list of ${model.name}`,
457+
security: read === true ? [] : undefined,
436458
});
437459

438460
if (ops['aggregate']) {
@@ -456,6 +478,7 @@ export class OpenAPIGenerator {
456478
),
457479
outputType: this.ref(`Aggregate${model.name}`),
458480
description: `Aggregate ${model.name}s`,
481+
security: read === true ? [] : undefined,
459482
});
460483
}
461484

@@ -481,13 +504,14 @@ export class OpenAPIGenerator {
481504
),
482505
outputType: this.array(this.ref(`${model.name}GroupByOutputType`)),
483506
description: `Group ${model.name}s by fields`,
507+
security: read === true ? [] : undefined,
484508
});
485509
}
486510

487511
// get meta specified with @@openapi.meta
488512
const resourceMeta = getModelResourceMeta(zmodel);
489513

490-
for (const { method, operation, description, inputType, outputType, successCode } of definitions) {
514+
for (const { method, operation, description, inputType, outputType, successCode, security } of definitions) {
491515
const meta = resourceMeta?.[operation];
492516

493517
if (meta?.ignore === true) {
@@ -511,7 +535,8 @@ export class OpenAPIGenerator {
511535
description: meta?.description ?? description,
512536
tags: meta?.tags || [camelCase(model.name)],
513537
summary: meta?.summary,
514-
security: meta?.security,
538+
// security priority: operation-level > model-level > inferred
539+
security: meta?.security ?? resourceMeta?.security ?? security,
515540
deprecated: meta?.deprecated,
516541
responses: {
517542
[successCode !== undefined ? successCode : '200']: {

packages/plugins/openapi/src/meta.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { DataModel } from '@zenstackhq/sdk/ast';
66
*/
77
export type ModelMeta = {
88
tagDescription?: string;
9+
security?: Array<Record<string, string[]>>;
910
};
1011

1112
/**

packages/plugins/openapi/tests/openapi.test.ts

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -196,18 +196,52 @@ model User {
196196
);
197197
});
198198

199-
it('security override', async () => {
199+
it('security model level override', async () => {
200200
const { model, dmmf, modelFile } = await loadZModelAndDmmf(`
201201
plugin openapi {
202202
provider = '${process.cwd()}/dist'
203+
securitySchemes = {
204+
myBasic: { type: 'http', scheme: 'basic' }
205+
}
206+
}
207+
208+
model User {
209+
id String @id
210+
211+
@@openapi.meta({
212+
security: []
213+
})
214+
}
215+
`);
216+
217+
const { name: output } = tmp.fileSync({ postfix: '.yaml' });
218+
const options = buildOptions(model, modelFile, output);
219+
await generate(model, options, dmmf);
220+
221+
console.log('OpenAPI specification generated:', output);
222+
223+
const api = await OpenAPIParser.validate(output);
224+
expect(api.paths?.['/user/findMany']?.['get']?.security).toHaveLength(0);
225+
});
226+
227+
it('security operation level override', async () => {
228+
const { model, dmmf, modelFile } = await loadZModelAndDmmf(`
229+
plugin openapi {
230+
provider = '${process.cwd()}/dist'
231+
securitySchemes = {
232+
myBasic: { type: 'http', scheme: 'basic' }
233+
}
203234
}
204235
205236
model User {
206237
id String @id
207238
239+
@@allow('read', true)
240+
208241
@@openapi.meta({
242+
security: [],
209243
findMany: {
210-
security: []
244+
security: [{ myBasic: [] }]
211245
}
212246
})
213247
}
@@ -220,7 +254,33 @@ model User {
220254
console.log('OpenAPI specification generated:', output);
221255

222256
const api = await OpenAPIParser.validate(output);
223-
expect(api.paths?.['/user/findMany']?.['get']?.security).toHaveLength(0);
257+
expect(api.paths?.['/user/findMany']?.['get']?.security).toHaveLength(1);
258+
});
259+
260+
it('security inferred', async () => {
261+
const { model, dmmf, modelFile } = await loadZModelAndDmmf(`
262+
plugin openapi {
263+
provider = '${process.cwd()}/dist'
264+
securitySchemes = {
265+
myBasic: { type: 'http', scheme: 'basic' }
266+
}
267+
}
268+
269+
model User {
270+
id String @id
271+
@@allow('create', true)
272+
}
273+
`);
274+
275+
const { name: output } = tmp.fileSync({ postfix: '.yaml' });
276+
const options = buildOptions(model, modelFile, output);
277+
await generate(model, options, dmmf);
278+
279+
console.log('OpenAPI specification generated:', output);
280+
281+
const api = await OpenAPIParser.validate(output);
282+
expect(api.paths?.['/user/create']?.['post']?.security).toHaveLength(0);
283+
expect(api.paths?.['/user/findMany']?.['get']?.security).toBeUndefined();
224284
});
225285

226286
it('v3.1.0 fields', async () => {

packages/schema/src/language-server/validator/datamodel-validator.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,12 @@ import {
66
isLiteralExpr,
77
ReferenceExpr,
88
} from '@zenstackhq/language/ast';
9+
import { analyzePolicies, getLiteral } from '@zenstackhq/sdk';
910
import { ValidationAcceptor } from 'langium';
10-
import { analyzePolicies } from '../../utils/ast-utils';
1111
import { IssueCodes, SCALAR_TYPES } from '../constants';
1212
import { AstValidator } from '../types';
1313
import { getIdFields, getUniqueFields } from '../utils';
1414
import { validateAttributeApplication, validateDuplicatedDeclarations } from './utils';
15-
import { getLiteral } from '@zenstackhq/sdk';
1615

1716
/**
1817
* Validates data model declarations.

packages/schema/src/plugins/access-policy/policy-guard-generator.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
} from '@zenstackhq/language/ast';
1616
import type { PolicyKind, PolicyOperationKind } from '@zenstackhq/runtime';
1717
import {
18+
analyzePolicies,
1819
getDataModels,
1920
getLiteral,
2021
GUARD_FIELD_NAME,
@@ -29,7 +30,7 @@ import path from 'path';
2930
import { FunctionDeclaration, Project, SourceFile, VariableDeclarationKind } from 'ts-morph';
3031
import { name } from '.';
3132
import { isFromStdlib } from '../../language-server/utils';
32-
import { analyzePolicies, getIdFields } from '../../utils/ast-utils';
33+
import { getIdFields } from '../../utils/ast-utils';
3334
import { ALL_OPERATION_KINDS, getDefaultOutputFolder } from '../plugin-utils';
3435
import { ExpressionWriter } from './expression-writer';
3536
import { isFutureExpr } from './utils';

packages/schema/src/plugins/access-policy/zod-schema-generator.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import { DataModel, DataModelField, DataModelFieldAttribute, isDataModelField } from '@zenstackhq/language/ast';
2-
import { AUXILIARY_FIELDS, getLiteral } from '@zenstackhq/sdk';
2+
import { AUXILIARY_FIELDS, VALIDATION_ATTRIBUTES, getLiteral } from '@zenstackhq/sdk';
33
import { camelCase } from 'change-case';
44
import { CodeBlockWriter } from 'ts-morph';
5-
import { VALIDATION_ATTRIBUTES } from '../../utils/ast-utils';
65

76
/**
87
* Writes Zod schema for data models.

packages/schema/src/plugins/prisma/schema-generator.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
Model,
2121
} from '@zenstackhq/language/ast';
2222
import {
23+
analyzePolicies,
2324
getLiteral,
2425
getLiteralArray,
2526
GUARD_FIELD_NAME,
@@ -31,24 +32,23 @@ import {
3132
import fs from 'fs';
3233
import { writeFile } from 'fs/promises';
3334
import path from 'path';
34-
import { analyzePolicies } from '../../utils/ast-utils';
3535
import { execSync } from '../../utils/exec-utils';
3636
import {
37+
ModelFieldType,
3738
AttributeArg as PrismaAttributeArg,
3839
AttributeArgValue as PrismaAttributeArgValue,
39-
ContainerAttribute as PrismaModelAttribute,
4040
ContainerDeclaration as PrismaContainerDeclaration,
41+
Model as PrismaDataModel,
4142
DataSourceUrl as PrismaDataSourceUrl,
4243
Enum as PrismaEnum,
4344
FieldAttribute as PrismaFieldAttribute,
4445
FieldReference as PrismaFieldReference,
4546
FieldReferenceArg as PrismaFieldReferenceArg,
4647
FunctionCall as PrismaFunctionCall,
4748
FunctionCallArg as PrismaFunctionCallArg,
48-
Model as PrismaDataModel,
49-
ModelFieldType,
50-
PassThroughAttribute as PrismaPassThroughAttribute,
5149
PrismaModel,
50+
ContainerAttribute as PrismaModelAttribute,
51+
PassThroughAttribute as PrismaPassThroughAttribute,
5252
SimpleField,
5353
} from './prisma-builder';
5454
import ZModelCodeGenerator from './zmodel-code-generator';

0 commit comments

Comments
 (0)