Skip to content

Commit b41d3fc

Browse files
committedSep 22, 2022
feat(server): apply evaluator to router, pass profile name to context
1 parent 134d675 commit b41d3fc

File tree

16 files changed

+79
-14
lines changed

16 files changed

+79
-14
lines changed
 

‎labs/playground1/profile.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@
44
persistent-path: ./test-data/moma.db
55
log-queries: true
66
log-parameters: true
7+
allow: '*'

‎packages/build/src/lib/schema-parser/middleware/checkProfile.ts

+13-7
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,25 @@ export class CheckProfile extends SchemaParserMiddleware {
1414
}
1515

1616
public async handle(schemas: RawAPISchema, next: () => Promise<void>) {
17+
if (!schemas.profiles && schemas.profile) {
18+
schemas.profiles = [schemas.profile];
19+
}
20+
1721
await next();
1822
const transformedSchemas = schemas as APISchema;
19-
if (!transformedSchemas.profile)
23+
if (!transformedSchemas.profiles)
2024
throw new Error(
2125
`The profile of schema ${transformedSchemas.urlPath} is not defined`
2226
);
2327

24-
try {
25-
this.dataSourceFactory(transformedSchemas.profile);
26-
} catch (e: any) {
27-
throw new Error(
28-
`The profile of schema ${transformedSchemas.urlPath} is invalid: ${e?.message}`
29-
);
28+
for (const profile of transformedSchemas.profiles) {
29+
try {
30+
this.dataSourceFactory(profile);
31+
} catch (e: any) {
32+
throw new Error(
33+
`The profile ${profile} of schema ${transformedSchemas.urlPath} is invalid: ${e?.message}`
34+
);
35+
}
3036
}
3137
}
3238
}

‎packages/build/src/lib/schema-parser/middleware/middleware.ts

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export interface RawAPISchema
2424
request?: DeepPartial<RawRequestParameter[]>;
2525
response?: DeepPartial<RawResponseProperty[]>;
2626
metadata?: Record<string, any>;
27+
profile?: string;
2728
}
2829

2930
@injectable()

‎packages/build/test/schema-parser/middleware/checkProfile.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ it('Should throw error when the profile is invalid', async () => {
2626
await expect(
2727
checkProfile.handle(schema, async () => Promise.resolve())
2828
).rejects.toThrow(
29-
`The profile of schema /user is invalid: profile not found`
29+
`The profile profile1 of schema /user is invalid: profile not found`
3030
);
3131
});
3232

‎packages/core/src/lib/template-engine/nunjucksExecutionMetadata.ts

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export class NunjucksExecutionMetadata {
3232
context: {
3333
params: this.parameters,
3434
user: this.userInfo,
35+
profile: this.profileName,
3536
},
3637
[ReservedContextKeys.CurrentProfileName]: this.profileName,
3738
};

‎packages/core/src/models/artifact.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ export interface APISchema {
9191
// If not set pagination, then API request not provide the field to do it
9292
pagination?: PaginationSchema;
9393
sample?: Sample;
94-
profile: string;
94+
profiles: Array<string>;
9595
}
9696

9797
export interface BuiltArtifact {
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
- name: pg-mem
22
type: pg-mem
3+
allow: '*'

‎packages/serve/src/containers/container.ts

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Container as CoreContainer } from '@vulcan-sql/core';
33
import {
44
applicationModule,
55
documentRouterModule,
6+
evaluationModule,
67
extensionModule,
78
routeGeneratorModule,
89
} from './modules';
@@ -32,6 +33,7 @@ export class Container {
3233
await this.inversifyContainer.loadAsync(extensionModule(config));
3334
await this.inversifyContainer.loadAsync(applicationModule());
3435
await this.inversifyContainer.loadAsync(documentRouterModule());
36+
await this.inversifyContainer.loadAsync(evaluationModule());
3537
}
3638

3739
public async unload() {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { Evaluator } from '@vulcan-sql/serve/evaluator';
2+
import { AsyncContainerModule } from 'inversify';
3+
import { TYPES } from '../types';
4+
5+
export const evaluationModule = () =>
6+
new AsyncContainerModule(async (bind) => {
7+
bind<Evaluator>(TYPES.Evaluator).to(Evaluator).inSingletonScope();
8+
});

‎packages/serve/src/containers/modules/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export * from './routeGenerator';
22
export * from './extension';
33
export * from './application';
44
export * from './documentRouter';
5+
export * from './evaluation';

‎packages/serve/src/containers/types.ts

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ export const TYPES = {
1010
VulcanApplication: Symbol.for('VulcanApplication'),
1111
// Document router
1212
Factory_DocumentRouter: Symbol.for('Factory_DocumentRouter'),
13+
// Evaluation
14+
Evaluator: Symbol.for('Evaluator'),
1315
// Extensions
1416
Extension_RouteMiddleware: Symbol.for('Extension_RouteMiddleware'),
1517
Extension_Authenticator: Symbol.for('Extension_Authenticator'),

‎packages/serve/src/lib/evaluator/evaluator.ts

+14-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import {
2+
getLogger,
23
Profile,
34
ProfileAllowConstraints,
45
TYPES as CORE_TYPES,
56
} from '@vulcan-sql/core';
6-
import { injectable, multiInject } from 'inversify';
7+
import { injectable, multiInject, optional } from 'inversify';
78
import { isArray } from 'lodash';
89
import { AuthUserInfo } from '../../models/extensions';
910
import {
@@ -35,12 +36,22 @@ import {
3536
// name: group
3637
// value: admin
3738

39+
const logger = getLogger({ scopeName: 'SERVE' });
40+
3841
@injectable()
3942
export class Evaluator {
4043
private profiles = new Map<string, AuthConstraint[][]>();
4144

42-
constructor(@multiInject(CORE_TYPES.Profile) profiles: Profile[]) {
45+
constructor(
46+
@multiInject(CORE_TYPES.Profile) @optional() profiles: Profile[] = []
47+
) {
4348
for (const profile of profiles) {
49+
if (!profile.allow) {
50+
logger.warn(
51+
`Profile ${profile.name} doesn't have allow property, which means nobody can use it`
52+
);
53+
continue;
54+
}
4455
this.profiles.set(profile.name, this.getConstraints(profile.allow));
4556
}
4657
}
@@ -76,7 +87,7 @@ export class Evaluator {
7687
andConstraints: AuthConstraint[]
7788
): boolean {
7889
for (const constraint of andConstraints) {
79-
if (!constraint.evaluate(user)) return false;
90+
if (!constraint.evaluate(user || { name: '', attr: {} })) return false;
8091
}
8192
return true;
8293
}

‎packages/serve/src/lib/route/route-component/baseRoute.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { APISchema, TemplateEngine, Pagination } from '@vulcan-sql/core';
33
import { IRequestValidator } from './requestValidator';
44
import { IRequestTransformer, RequestParameters } from './requestTransformer';
55
import { IPaginationTransformer } from './paginationTransformer';
6+
import { Evaluator } from '@vulcan-sql/serve/evaluator';
67

78
export interface TransformedRequest {
89
reqParams: RequestParameters;
@@ -15,6 +16,7 @@ export interface RouteOptions {
1516
reqValidator: IRequestValidator;
1617
paginationTransformer: IPaginationTransformer;
1718
templateEngine: TemplateEngine;
19+
evaluator: Evaluator;
1820
}
1921

2022
export interface IRoute {
@@ -27,19 +29,23 @@ export abstract class BaseRoute implements IRoute {
2729
protected readonly reqValidator: IRequestValidator;
2830
protected readonly templateEngine: TemplateEngine;
2931
protected readonly paginationTransformer: IPaginationTransformer;
32+
private evaluator: Evaluator;
3033

34+
// TODO: Too many injection from constructor, we should try to use container or compose some components
3135
constructor({
3236
apiSchema,
3337
reqTransformer,
3438
reqValidator,
3539
paginationTransformer,
3640
templateEngine,
41+
evaluator,
3742
}: RouteOptions) {
3843
this.apiSchema = apiSchema;
3944
this.reqTransformer = reqTransformer;
4045
this.reqValidator = reqValidator;
4146
this.paginationTransformer = paginationTransformer;
4247
this.templateEngine = templateEngine;
48+
this.evaluator = evaluator;
4349
}
4450

4551
public abstract respond(ctx: KoaContext): Promise<any>;
@@ -49,7 +55,12 @@ export abstract class BaseRoute implements IRoute {
4955
protected async handle(user: AuthUserInfo, transformed: TransformedRequest) {
5056
const { reqParams } = transformed;
5157
// could template name or template path, use for template engine
52-
const { templateSource, profile } = this.apiSchema;
58+
const { templateSource, profiles } = this.apiSchema;
59+
60+
const profile = this.evaluator.evaluateProfile(user, profiles);
61+
if (!profile)
62+
// Should be 403
63+
throw new Error(`Forbidden`);
5364

5465
const result = await this.templateEngine.execute(templateSource, {
5566
parameters: reqParams,

‎packages/serve/src/lib/route/routeGenerator.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
import { inject, injectable } from 'inversify';
1010
import { TYPES as CORE_TYPES } from '@vulcan-sql/core';
1111
import { TYPES } from '../../containers/types';
12+
import { Evaluator } from '../evaluator';
1213

1314
export enum APIProviderType {
1415
RESTFUL = 'RESTFUL',
@@ -27,6 +28,7 @@ export class RouteGenerator {
2728
private reqTransformer: IRequestTransformer;
2829
private paginationTransformer: IPaginationTransformer;
2930
private templateEngine: TemplateEngine;
31+
private evaluator: Evaluator;
3032
private apiOptions: APIRouteBuilderOption = {
3133
[APIProviderType.RESTFUL]: RestfulRoute,
3234
[APIProviderType.GRAPHQL]: GraphQLRoute,
@@ -37,12 +39,14 @@ export class RouteGenerator {
3739
@inject(TYPES.RequestValidator) reqValidator: IRequestValidator,
3840
@inject(TYPES.PaginationTransformer)
3941
paginationTransformer: IPaginationTransformer,
40-
@inject(CORE_TYPES.TemplateEngine) templateEngine: TemplateEngine
42+
@inject(CORE_TYPES.TemplateEngine) templateEngine: TemplateEngine,
43+
@inject(TYPES.Evaluator) evaluator: Evaluator
4144
) {
4245
this.reqValidator = reqValidator;
4346
this.reqTransformer = reqTransformer;
4447
this.paginationTransformer = paginationTransformer;
4548
this.templateEngine = templateEngine;
49+
this.evaluator = evaluator;
4650
}
4751

4852
public async generate(apiSchema: APISchema, optionType: APIProviderType) {
@@ -55,6 +59,7 @@ export class RouteGenerator {
5559
reqValidator: this.reqValidator,
5660
paginationTransformer: this.paginationTransformer,
5761
templateEngine: this.templateEngine,
62+
evaluator: this.evaluator,
5863
});
5964
}
6065

‎packages/serve/test/app.spec.ts

+9
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,18 @@ import { KoaContext } from '@vulcan-sql/serve/models';
3030
import { Container } from 'inversify';
3131
import { extensionModule } from '../src/containers/modules';
3232
import { TYPES } from '@vulcan-sql/serve';
33+
import { Evaluator } from '@vulcan-sql/serve/evaluator';
3334

3435
describe('Test vulcan server for practicing middleware', () => {
3536
let container: Container;
3637
let stubTemplateEngine: sinon.StubbedInstance<TemplateEngine>;
3738
let stubDataSource: sinon.StubbedInstance<DataSource>;
39+
let stubEvaluator: sinon.StubbedInstance<Evaluator>;
3840
beforeEach(async () => {
3941
container = new Container();
4042
stubTemplateEngine = sinon.stubInterface<TemplateEngine>();
4143
stubDataSource = sinon.stubInterface<DataSource>();
44+
stubEvaluator = sinon.stubInterface<Evaluator>();
4245

4346
await container.loadAsync(
4447
coreExtensionModule({
@@ -82,6 +85,7 @@ describe('Test vulcan server for practicing middleware', () => {
8285
router: [],
8386
})
8487
);
88+
container.bind(TYPES.Evaluator).toConstantValue(stubEvaluator);
8589
});
8690

8791
afterEach(() => {
@@ -122,6 +126,7 @@ describe('Test vulcan server for calling restful APIs', () => {
122126
let container: Container;
123127
let stubTemplateEngine: sinon.StubbedInstance<TemplateEngine>;
124128
let stubDataSource: sinon.StubbedInstance<DataSource>;
129+
let stubEvaluator: sinon.StubbedInstance<Evaluator>;
125130
let server: http.Server;
126131
const fakeSchemas: Array<APISchema> = [
127132
{
@@ -274,6 +279,7 @@ describe('Test vulcan server for calling restful APIs', () => {
274279
container = new Container();
275280
stubTemplateEngine = sinon.stubInterface<TemplateEngine>();
276281
stubDataSource = sinon.stubInterface<DataSource>();
282+
stubEvaluator = sinon.stubInterface<Evaluator>();
277283

278284
stubTemplateEngine.execute.callsFake(async (_: string, data: any) => {
279285
return {
@@ -282,6 +288,8 @@ describe('Test vulcan server for calling restful APIs', () => {
282288
};
283289
});
284290

291+
stubEvaluator.evaluateProfile.returns('profile1');
292+
285293
await container.loadAsync(
286294
coreExtensionModule({
287295
artifact: {} as any,
@@ -321,6 +329,7 @@ describe('Test vulcan server for calling restful APIs', () => {
321329
router: [],
322330
})
323331
);
332+
container.bind(TYPES.Evaluator).toConstantValue(stubEvaluator);
324333
});
325334

326335
afterEach(() => {

‎packages/serve/test/route/routeGenerator.spec.ts

+6
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
} from '@vulcan-sql/serve/route';
1818
import { Container } from 'inversify';
1919
import { TYPES } from '@vulcan-sql/serve/containers';
20+
import { Evaluator } from '@vulcan-sql/serve/evaluator';
2021

2122
describe('Test route generator ', () => {
2223
let container: Container;
@@ -25,6 +26,7 @@ describe('Test route generator ', () => {
2526
let stubPaginationTransformer: sinon.StubbedInstance<IPaginationTransformer>;
2627
let stubTemplateEngine: sinon.StubbedInstance<TemplateEngine>;
2728
let stubDataSource: sinon.StubbedInstance<DataSource>;
29+
let stubEvaluator: sinon.StubbedInstance<Evaluator>;
2830
const fakeSchemas: Array<APISchema> = Array(
2931
faker.datatype.number({ min: 2, max: 4 })
3032
).fill(sinon.stubInterface<APISchema>());
@@ -38,6 +40,7 @@ describe('Test route generator ', () => {
3840
stubPaginationTransformer = sinon.stubInterface<IPaginationTransformer>();
3941
stubDataSource = sinon.stubInterface<DataSource>();
4042
stubTemplateEngine = sinon.stubInterface<TemplateEngine>();
43+
stubEvaluator = sinon.stubInterface<Evaluator>();
4144

4245
container
4346
.bind(TYPES.PaginationTransformer)
@@ -53,6 +56,7 @@ describe('Test route generator ', () => {
5356
.bind(CORE_TYPES.Factory_DataSource)
5457
.toConstantValue(() => stubDataSource);
5558
container.bind(TYPES.RouteGenerator).to(RouteGenerator);
59+
container.bind(TYPES.Evaluator).toConstantValue(stubEvaluator);
5660
});
5761

5862
afterEach(() => {
@@ -76,6 +80,7 @@ describe('Test route generator ', () => {
7680
templateEngine: container.get<TemplateEngine>(
7781
CORE_TYPES.TemplateEngine
7882
),
83+
evaluator: container.get<Evaluator>(TYPES.Evaluator),
7984
});
8085

8186
// Act
@@ -111,6 +116,7 @@ describe('Test route generator ', () => {
111116
templateEngine: container.get<TemplateEngine>(
112117
CORE_TYPES.TemplateEngine
113118
),
119+
evaluator: container.get<Evaluator>(TYPES.Evaluator),
114120
});
115121

116122
// Act

0 commit comments

Comments
 (0)
Please sign in to comment.