From e96b72598696bccd0b08ae860944cb180e34e3c2 Mon Sep 17 00:00:00 2001 From: chris Date: Thu, 6 Jul 2023 10:09:49 +0300 Subject: [PATCH 1/7] refactor(database,authorization,grpc-sdk): refactor views in mongoose findMany --- .../src/modules/authorization/index.ts | 4 + modules/authorization/src/Authorization.ts | 15 ++ modules/authorization/src/authorization.proto | 6 + .../src/controllers/permissions.controller.ts | 131 ++++------------- modules/authorization/src/utils/index.ts | 134 ++++++++++++++---- modules/database/package.json | 3 +- .../database/src/adapters/SchemaAdapter.ts | 14 ++ .../mongoose-adapter/MongooseSchema.ts | 22 ++- .../src/adapters/mongoose-adapter/index.ts | 3 +- modules/database/src/admin/schema.admin.ts | 12 ++ yarn.lock | 5 + 11 files changed, 216 insertions(+), 133 deletions(-) diff --git a/libraries/grpc-sdk/src/modules/authorization/index.ts b/libraries/grpc-sdk/src/modules/authorization/index.ts index 99fd57341..9bc007acd 100644 --- a/libraries/grpc-sdk/src/modules/authorization/index.ts +++ b/libraries/grpc-sdk/src/modules/authorization/index.ts @@ -68,4 +68,8 @@ export class Authorization extends ConduitModule createResourceAccessList(data: ResourceAccessListRequest): Promise { return this.client!.createResourceAccessList(data); } + + getAuthorizedQuery(data: ResourceAccessListRequest): Promise { + return this.client!.getAuthorizedQuery(data).then(r => JSON.parse(r.query)); + } } diff --git a/modules/authorization/src/Authorization.ts b/modules/authorization/src/Authorization.ts index a3d18699e..06ed44182 100644 --- a/modules/authorization/src/Authorization.ts +++ b/modules/authorization/src/Authorization.ts @@ -15,6 +15,7 @@ import { Decision, DeleteResourceRequest, FindRelationRequest, + GetAuthorizedQueryResponse, PermissionCheck, PermissionRequest, Relation, @@ -53,6 +54,7 @@ export default class Authorization extends ManagedModule { getAllowedResources: this.getAllowedResources.bind(this), can: this.can.bind(this), createResourceAccessList: this.createResourceAccessList.bind(this), + getAuthorizedQuery: this.getAuthorizedQuery.bind(this), }, }; protected metricsSchema = metricsSchema; @@ -192,6 +194,19 @@ export default class Authorization extends ManagedModule { callback(null); } + async getAuthorizedQuery( + call: GrpcRequest, + callback: GrpcResponse, + ) { + const { subject, action, resourceType } = call.request; + const query = await this.permissionsController.getAuthorizedQuery( + subject, + action, + resourceType, + ); + callback(null, { query: JSON.stringify(query) }); + } + async can(call: GrpcRequest, callback: GrpcResponse) { const { subject, resource, actions } = call.request; let allow = false; diff --git a/modules/authorization/src/authorization.proto b/modules/authorization/src/authorization.proto index 59853c5c4..a03435d3c 100644 --- a/modules/authorization/src/authorization.proto +++ b/modules/authorization/src/authorization.proto @@ -59,6 +59,11 @@ message AllowedResourcesResponse { repeated string resources = 1; int32 count = 2; } + +message GetAuthorizedQueryResponse { + string query = 1; +} + message ResourceAccessListRequest { string subject = 1; string action = 2; @@ -93,4 +98,5 @@ service Authorization { rpc Can(PermissionCheck) returns (Decision); rpc GetAllowedResources(AllowedResourcesRequest) returns (AllowedResourcesResponse); rpc CreateResourceAccessList(ResourceAccessListRequest) returns (google.protobuf.Empty); + rpc GetAuthorizedQuery(ResourceAccessListRequest) returns (GetAuthorizedQueryResponse); } diff --git a/modules/authorization/src/controllers/permissions.controller.ts b/modules/authorization/src/controllers/permissions.controller.ts index dd5368d55..3939cc14d 100644 --- a/modules/authorization/src/controllers/permissions.controller.ts +++ b/modules/authorization/src/controllers/permissions.controller.ts @@ -1,7 +1,9 @@ -import ConduitGrpcSdk from '@conduitplatform/grpc-sdk'; +import ConduitGrpcSdk, { RawQuery } from '@conduitplatform/grpc-sdk'; import { + AccessListQueryParams, checkRelation, computePermissionTuple, + getMongoAccessListQuery, getPostgresAccessListQuery, getSQLAccessListQuery, } from '../utils'; @@ -124,114 +126,35 @@ export class PermissionsController { } async createAccessList(subject: string, action: string, objectType: string) { - const computedTuple = `${subject}#${action}@${objectType}`; - const objectTypeCollection = await this.grpcSdk - .database!.getSchema(objectType) - .then(r => r.collectionName); - const dbType = await this.grpcSdk.database!.getDatabaseType().then(r => r.result); + const query = await this.getAuthorizedQuery(subject, action, objectType); await this.grpcSdk.database?.createView( objectType, createHash('sha256').update(`${objectType}_${subject}_${action}`).digest('hex'), ['Permission', 'ActorIndex', 'ObjectIndex'], - { - mongoQuery: [ - // permissions lookup won't work this way - { - $lookup: { - from: 'cnd_permissions', - let: { x_id: { $toString: '$_id' } }, - pipeline: [ - { - $match: { - $expr: { - $eq: [ - '$computedTuple', - { $concat: [`${subject}#${action}@${objectType}:`, '$$x_id'] }, - ], - }, - }, - }, - ], - as: 'permissions', - }, - }, - { - $lookup: { - from: 'cnd_actorindexes', - let: { - subject: subject, - }, - pipeline: [ - { - $match: { - $expr: { - $eq: ['$subject', '$$subject'], - }, - }, - }, - ], - as: 'actors', - }, - }, - { - $lookup: { - from: 'cnd_objectindexes', - let: { - id_action: { - $concat: [`${objectType}:`, { $toString: '$_id' }, `#${action}`], - }, - }, - pipeline: [ - { - $match: { - $expr: { - $eq: ['$subject', '$$id_action'], - }, - }, - }, - ], - as: 'objects', - }, - }, - { - $addFields: { - intersection: { - $setIntersection: ['$actors.entity', '$objects.entity'], - }, - }, - }, - { - $match: { - intersection: { $ne: [] }, - }, - }, - { - $project: { - actors: 0, - objects: 0, - permissions: 0, - intersection: 0, - }, - }, - ], - sqlQuery: - dbType === 'PostgreSQL' - ? getPostgresAccessListQuery( - objectTypeCollection, - computedTuple, - subject, - objectType, - action, - ) - : getSQLAccessListQuery( - objectTypeCollection, - computedTuple, - subject, - objectType, - action, - ), - }, + query, ); return; } + + async getAuthorizedQuery(subject: string, action: string, objectType: string) { + const computedTuple = `${subject}#${action}@${objectType}`; + const objectTypeCollection = await this.grpcSdk + .database!.getSchema(objectType) + .then(r => r.collectionName); + const dbType = await this.grpcSdk.database!.getDatabaseType().then(r => r.result); + const params: AccessListQueryParams = { + objectTypeCollection, + computedTuple, + subject, + objectType, + action, + }; + return { + mongoQuery: getMongoAccessListQuery(params), + sqlQuery: + dbType === 'PostgreSQL' + ? getPostgresAccessListQuery(params) + : getSQLAccessListQuery(params), + }; + } } diff --git a/modules/authorization/src/utils/index.ts b/modules/authorization/src/utils/index.ts index 967beac0f..8b340ea40 100644 --- a/modules/authorization/src/utils/index.ts +++ b/modules/authorization/src/utils/index.ts @@ -31,13 +31,16 @@ export const computePermissionTuple = ( return `${subject}#${relation}@${object}`; }; -export function getPostgresAccessListQuery( - objectTypeCollection: string, - computedTuple: string, - subject: string, - objectType: string, - action: string, -) { +export interface AccessListQueryParams { + objectTypeCollection: string; + computedTuple: string; + subject: string; + objectType: string; + action: string; +} + +export function getPostgresAccessListQuery(params: AccessListQueryParams) { + const { objectTypeCollection, computedTuple, subject, objectType, action } = params; return `SELECT "${objectTypeCollection}".* FROM "${objectTypeCollection}" INNER JOIN ( SELECT * FROM "cnd_Permission" @@ -53,24 +56,103 @@ export function getPostgresAccessListQuery( ) objects ON actors.entity = objects.entity;`; } -export function getSQLAccessListQuery( - objectTypeCollection: string, - computedTuple: string, - subject: string, - objectType: string, - action: string, -) { +export function getSQLAccessListQuery(params: AccessListQueryParams) { + const { objectTypeCollection, computedTuple, subject, objectType, action } = params; return `SELECT ${objectTypeCollection}.* FROM ${objectTypeCollection} - INNER JOIN ( - SELECT * FROM cnd_Permission - WHERE computedTuple LIKE '${computedTuple}%' - ) permissions ON permissions.computedTuple = '${computedTuple}:' || ${objectTypeCollection}._id - INNER JOIN ( - SELECT * FROM cnd_ActorIndex - WHERE subject = '${subject}' - ) actors ON 1=1 - INNER JOIN ( - SELECT * FROM cnd_ObjectIndex - WHERE subject LIKE '${objectType}:%#${action}' - ) objects ON actors.entity = objects.entity;`; + INNER JOIN ( + SELECT * FROM cnd_Permission + WHERE computedTuple LIKE '${computedTuple}%' + ) permissions ON permissions.computedTuple = '${computedTuple}:' || ${objectTypeCollection}._id + INNER JOIN ( + SELECT * FROM cnd_ActorIndex + WHERE subject = '${subject}' + ) actors ON 1=1 + INNER JOIN ( + SELECT * FROM cnd_ObjectIndex + WHERE subject LIKE '${objectType}:%#${action}' + ) objects ON actors.entity = objects.entity;`; +} + +export function getMongoAccessListQuery(params: AccessListQueryParams) { + const { subject, objectType, action } = params; + return [ + // permissions lookup won't work this way + { + $lookup: { + from: 'cnd_permissions', + let: { x_id: { $toString: '$_id' } }, + pipeline: [ + { + $match: { + $expr: { + $eq: [ + '$computedTuple', + { $concat: [`${subject}#${action}@${objectType}:`, '$$x_id'] }, + ], + }, + }, + }, + ], + as: 'permissions', + }, + }, + { + $lookup: { + from: 'cnd_actorindexes', + let: { + subject: subject, + }, + pipeline: [ + { + $match: { + $expr: { + $eq: ['$subject', '$$subject'], + }, + }, + }, + ], + as: 'actors', + }, + }, + { + $lookup: { + from: 'cnd_objectindexes', + let: { + id_action: { + $concat: [`${objectType}:`, { $toString: '$_id' }, `#${action}`], + }, + }, + pipeline: [ + { + $match: { + $expr: { + $eq: ['$subject', '$$id_action'], + }, + }, + }, + ], + as: 'objects', + }, + }, + { + $addFields: { + intersection: { + $setIntersection: ['$actors.entity', '$objects.entity'], + }, + }, + }, + { + $match: { + intersection: { $ne: [] }, + }, + }, + { + $project: { + actors: 0, + objects: 0, + permissions: 0, + intersection: 0, + }, + }, + ]; } diff --git a/modules/database/package.json b/modules/database/package.json index 2c283f768..45ef2bf2e 100644 --- a/modules/database/package.json +++ b/modules/database/package.json @@ -32,6 +32,7 @@ "mongodb-extended-json": "^1.11.0", "mongodb-schema": "^10.0.1", "mongoose": "7.0.3", + "mongoose-cast-aggregation": "^0.3.1", "mysql2": "^3.2.0", "object-hash": "^3.0.0", "pg": "^8.10.0", @@ -50,9 +51,9 @@ "registry": "https://npm.pkg.github.com/" }, "devDependencies": { + "@types/dottie": "^2.0.4", "@types/lodash": "^4.14.192", "@types/node": "18.15.11", - "@types/dottie": "^2.0.4", "copyfiles": "^2.4.1", "rimraf": "^5.0.0", "ts-proto": "^1.146.0", diff --git a/modules/database/src/adapters/SchemaAdapter.ts b/modules/database/src/adapters/SchemaAdapter.ts index f2f6eb7ca..f9e89d0c4 100644 --- a/modules/database/src/adapters/SchemaAdapter.ts +++ b/modules/database/src/adapters/SchemaAdapter.ts @@ -169,6 +169,20 @@ export abstract class SchemaAdapter { } } + async getTestAuthorizedQuery(operation: string, userId?: string, scope?: string) { + if ( + !this.originalSchema.modelOptions.conduit?.authorization?.enabled || + (isNil(userId) && isNil(scope)) + ) + return []; + const query = await this.grpcSdk.authorization!.getAuthorizedQuery({ + subject: `User:${userId}`, + action: operation, + resourceType: this.originalSchema.name, + }); + return query.mongoQuery; + } + async getPaginatedAuthorizedQuery( operation: string, parsedQuery: Indexable, diff --git a/modules/database/src/adapters/mongoose-adapter/MongooseSchema.ts b/modules/database/src/adapters/mongoose-adapter/MongooseSchema.ts index 1d7b4588b..dea4af0a5 100644 --- a/modules/database/src/adapters/mongoose-adapter/MongooseSchema.ts +++ b/modules/database/src/adapters/mongoose-adapter/MongooseSchema.ts @@ -21,7 +21,7 @@ import ConduitGrpcSdk, { Indexable, UntypedArray, } from '@conduitplatform/grpc-sdk'; -import { cloneDeep, isNil } from 'lodash'; +import { cloneDeep, isElement, isEmpty, isNil } from 'lodash'; import { parseQuery } from './parser'; const EJSON = require('mongodb-extended-json'); @@ -260,6 +260,11 @@ export class MongooseSchema extends SchemaAdapter> { scope?: string; }, ) { + const authorizedQuery = await this.getTestAuthorizedQuery( + 'read', + options?.userId, + options?.scope, + ); let { parsedQuery, modified } = await this.getPaginatedAuthorizedQuery( 'read', parseQuery(this.parseStringToQuery(query)), @@ -285,6 +290,21 @@ export class MongooseSchema extends SchemaAdapter> { if (!isNil(options?.sort)) { finalQuery = finalQuery.sort(this.parseSort(options?.sort)); } + if (!isEmpty(authorizedQuery)) { + //{ + // $expr: { $eq: ['$productInfo.team._id', { $toObjectId: team }] }, + // } + const pipeline = [ + { + $match: { + $expr: { $eq: ['$_id', { $toObjectId: '64a41678894aa4e2c0fdbc08' }] }, + }, + }, //{ $match: parseQuery(this.parseStringToQuery(query)) }, + ...authorizedQuery, + ]; + const result = await this.model.aggregate(pipeline); + return result; + } return finalQuery.lean().exec(); } diff --git a/modules/database/src/adapters/mongoose-adapter/index.ts b/modules/database/src/adapters/mongoose-adapter/index.ts index 5305c93bb..f485d1834 100644 --- a/modules/database/src/adapters/mongoose-adapter/index.ts +++ b/modules/database/src/adapters/mongoose-adapter/index.ts @@ -23,6 +23,7 @@ import { isNil } from 'lodash'; const EJSON = require('mongodb-extended-json'); const parseSchema = require('mongodb-schema'); +const castAggregation = require('mongoose-cast-aggregation'); export class MongooseAdapter extends DatabaseAdapter { connected: boolean = false; @@ -38,7 +39,7 @@ export class MongooseAdapter extends DatabaseAdapter { constructor(connectionString: string) { super(); this.connectionString = connectionString; - this.mongoose = new Mongoose(); + this.mongoose = new Mongoose().plugin(castAggregation); } async retrieveForeignSchemas(): Promise { diff --git a/modules/database/src/admin/schema.admin.ts b/modules/database/src/admin/schema.admin.ts index fbdb7bc35..43540f599 100644 --- a/modules/database/src/admin/schema.admin.ts +++ b/modules/database/src/admin/schema.admin.ts @@ -578,6 +578,18 @@ export class SchemaAdmin { } async getDatabaseType(): Promise { + const test1 = await this.database.getSchemaModel('File').model.findMany( + { + _id: '64a41678894aa4e2c0fdbc08', + }, + { userId: '64a415a2894aa4e2c0fdbb3f' }, + ); + const test2 = await this.database.getSchemaModel('File').model.findMany( + { + _id: '64a41678894aa4e2c0fdbc08', + }, + { userId: '64a415b2894aa4e2c0fdbb42' }, + ); return this.database.getDatabaseType(); } diff --git a/yarn.lock b/yarn.lock index 4f7aa77d4..ba6d7a577 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10637,6 +10637,11 @@ mongodb@^5.0.1: optionalDependencies: saslprep "^1.0.3" +mongoose-cast-aggregation@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/mongoose-cast-aggregation/-/mongoose-cast-aggregation-0.3.1.tgz#92479e2156b42039a3323066f5840e9707927aed" + integrity sha512-20D7ce6S9QOEjvvtU5gmFzj7X+QAr0UH3M5BAbaWRkvGp4U/XtpH5ngkq8YpppW/mF6Gl+YQNL9s2chLk3DCrA== + mongoose@7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-7.0.3.tgz#576375acb436f96cd3350fb63fddbac7ae51ff9c" From c3ac320f690c07e418669ce7880916c7accce085 Mon Sep 17 00:00:00 2001 From: chris Date: Thu, 6 Jul 2023 18:15:12 +0300 Subject: [PATCH 2/7] refactor(database): wip add authenticated aggregation pipeline --- .../database/src/adapters/SchemaAdapter.ts | 14 -- .../mongoose-adapter/MongooseSchema.ts | 153 +++++++++++++----- .../src/adapters/mongoose-adapter/index.ts | 6 +- modules/database/src/admin/schema.admin.ts | 3 + 4 files changed, 124 insertions(+), 52 deletions(-) diff --git a/modules/database/src/adapters/SchemaAdapter.ts b/modules/database/src/adapters/SchemaAdapter.ts index f9e89d0c4..f2f6eb7ca 100644 --- a/modules/database/src/adapters/SchemaAdapter.ts +++ b/modules/database/src/adapters/SchemaAdapter.ts @@ -169,20 +169,6 @@ export abstract class SchemaAdapter { } } - async getTestAuthorizedQuery(operation: string, userId?: string, scope?: string) { - if ( - !this.originalSchema.modelOptions.conduit?.authorization?.enabled || - (isNil(userId) && isNil(scope)) - ) - return []; - const query = await this.grpcSdk.authorization!.getAuthorizedQuery({ - subject: `User:${userId}`, - action: operation, - resourceType: this.originalSchema.name, - }); - return query.mongoQuery; - } - async getPaginatedAuthorizedQuery( operation: string, parsedQuery: Indexable, diff --git a/modules/database/src/adapters/mongoose-adapter/MongooseSchema.ts b/modules/database/src/adapters/mongoose-adapter/MongooseSchema.ts index dea4af0a5..c2a195adb 100644 --- a/modules/database/src/adapters/mongoose-adapter/MongooseSchema.ts +++ b/modules/database/src/adapters/mongoose-adapter/MongooseSchema.ts @@ -1,6 +1,7 @@ import { Model, Mongoose, + PipelineStage, PopulateOptions, Query as MongooseQuery, Schema, @@ -17,11 +18,12 @@ import { } from '../../interfaces'; import { MongooseAdapter } from './index'; import ConduitGrpcSdk, { + ConduitModel, ConduitSchema, Indexable, UntypedArray, } from '@conduitplatform/grpc-sdk'; -import { cloneDeep, isElement, isEmpty, isNil } from 'lodash'; +import { cloneDeep, isEmpty, isNil } from 'lodash'; import { parseQuery } from './parser'; const EJSON = require('mongodb-extended-json'); @@ -260,29 +262,26 @@ export class MongooseSchema extends SchemaAdapter> { scope?: string; }, ) { - const authorizedQuery = await this.getTestAuthorizedQuery( - 'read', - options?.userId, - options?.scope, - ); - let { parsedQuery, modified } = await this.getPaginatedAuthorizedQuery( + const parsedQuery = parseQuery(this.parseStringToQuery(query)); + const authorizedQueryPipeline = await this.getAuthorizedQueryPipeline( 'read', - parseQuery(this.parseStringToQuery(query)), options?.userId, options?.scope, - options?.skip, - options?.limit, - options?.sort, ); - if (isNil(parsedQuery)) { - return []; + if (!isEmpty(authorizedQueryPipeline)) { + const pipeline = this.constructAggregationPipeline( + parsedQuery, + authorizedQueryPipeline, + options, + ); + return this.model.aggregate(pipeline as PipelineStage[]); } let finalQuery = this.model.find(parsedQuery, options?.select); - if (!isNil(options?.skip) && !modified) { - finalQuery = finalQuery.skip(options?.skip!); + if (!isNil(options?.skip)) { + finalQuery = finalQuery.skip(options!.skip); } - if (!isNil(options?.limit) && !modified) { - finalQuery = finalQuery.limit(options?.limit!); + if (!isNil(options?.limit)) { + finalQuery = finalQuery.limit(options!.limit); } if (!isNil(options?.populate)) { finalQuery = this.populate(finalQuery, options?.populate ?? []); @@ -290,21 +289,6 @@ export class MongooseSchema extends SchemaAdapter> { if (!isNil(options?.sort)) { finalQuery = finalQuery.sort(this.parseSort(options?.sort)); } - if (!isEmpty(authorizedQuery)) { - //{ - // $expr: { $eq: ['$productInfo.team._id', { $toObjectId: team }] }, - // } - const pipeline = [ - { - $match: { - $expr: { $eq: ['$_id', { $toObjectId: '64a41678894aa4e2c0fdbc08' }] }, - }, - }, //{ $match: parseQuery(this.parseStringToQuery(query)) }, - ...authorizedQuery, - ]; - const result = await this.model.aggregate(pipeline); - return result; - } return finalQuery.lean().exec(); } @@ -317,14 +301,20 @@ export class MongooseSchema extends SchemaAdapter> { populate?: string[]; }, ) { - let parsedQuery: Indexable | null = parseQuery(this.parseStringToQuery(query)); - parsedQuery = await this.getAuthorizedQuery( + const parsedQuery = parseQuery(this.parseStringToQuery(query)); + const authorizedQueryPipeline = await this.getAuthorizedQueryPipeline( 'read', - parsedQuery, - false, options?.userId, options?.scope, ); + if (!isEmpty(authorizedQueryPipeline)) { + const pipeline = this.constructAggregationPipeline( + parsedQuery, + authorizedQueryPipeline, + options, + ); + return this.model.aggregate(pipeline as PipelineStage[]); + } let finalQuery = this.model.findOne(parsedQuery!, options?.select); if (options?.populate !== undefined && options?.populate !== null) { finalQuery = this.populate(finalQuery, options?.populate); @@ -452,4 +442,95 @@ export class MongooseSchema extends SchemaAdapter> { private parseSort(sort: { [key: string]: number }): { [p: string]: SortOrder } { return sort as { [p: string]: SortOrder }; } + + private constructAggregationPipeline( + parsedQuery: Indexable, + authorizedQueryPipeline: object[], + options?: { + skip?: number; + limit?: number; + select?: string; + sort?: any; + populate?: string[]; + userId?: string; + scope?: string; + }, + ) { + const pipeline = [{ $match: parsedQuery }, ...authorizedQueryPipeline]; + if (!isNil(options?.skip)) { + pipeline.push({ $skip: options?.skip }); + } + if (!isNil(options?.limit)) { + pipeline.push({ $limit: options?.limit }); + } + if (!isNil(options?.sort)) { + pipeline.push({ $sort: this.parseSort(options?.sort) }); + } + if (!isNil(options?.populate) && !isEmpty(options?.populate)) { + pipeline.push([...this.parsePipelinePopulate(options!.populate)]); + } + return pipeline; + } + + // TODO: check this + private parsePipelinePopulate(population: string[]) { + const pipeline: object[] = []; + const populates = this.calculatePopulates(population); + for (const populate of populates) { + let field; + if (typeof populate === 'object') { + field = populate.path; + } else { + field = populate; + } + const model = ( + (this.schema as _ConduitSchema).compiledFields[field as string] as ConduitModel + ).model!; + const relatedCollection = this.adapter.models[model].originalSchema.collectionName; + pipeline.push({ + $lookup: { + from: relatedCollection, + localField: field, + foreignField: '_id', + as: field, + }, + }); + } + return pipeline; + } + + private async getAuthorizedQueryPipeline( + operation: string, + userId?: string, + scope?: string, + ) { + if ( + !this.originalSchema.modelOptions.conduit?.authorization?.enabled || + (isNil(userId) && isNil(scope)) + ) { + return []; + } + const isAvailable = this.grpcSdk.isAvailable('authorization'); + if (!isAvailable) { + throw new Error('Authorization service is not available'); + } + if (scope) { + if (userId) { + const allowed = await this.grpcSdk.authorization?.can({ + subject: `User:${userId}`, + actions: [operation], + resource: scope, + }); + if (!allowed?.allow) { + throw new Error(`User:${userId} is not allowed to ${operation} ${scope}`); + } + } + } + const query = await this.grpcSdk.authorization!.getAuthorizedQuery({ + subject: scope ?? `User:${userId}`, + action: operation, + resourceType: this.originalSchema.name, + }); + return query.mongoQuery; + } } diff --git a/modules/database/src/adapters/mongoose-adapter/index.ts b/modules/database/src/adapters/mongoose-adapter/index.ts index f485d1834..1a89717c1 100644 --- a/modules/database/src/adapters/mongoose-adapter/index.ts +++ b/modules/database/src/adapters/mongoose-adapter/index.ts @@ -23,7 +23,9 @@ import { isNil } from 'lodash'; const EJSON = require('mongodb-extended-json'); const parseSchema = require('mongodb-schema'); +const mongoose = require('mongoose'); const castAggregation = require('mongoose-cast-aggregation'); +mongoose.plugin(castAggregation); export class MongooseAdapter extends DatabaseAdapter { connected: boolean = false; @@ -39,7 +41,7 @@ export class MongooseAdapter extends DatabaseAdapter { constructor(connectionString: string) { super(); this.connectionString = connectionString; - this.mongoose = new Mongoose().plugin(castAggregation); + this.mongoose = mongoose; } async retrieveForeignSchemas(): Promise { @@ -332,7 +334,7 @@ export class MongooseAdapter extends DatabaseAdapter { } protected connect() { - this.mongoose = new Mongoose(); + this.mongoose = mongoose; ConduitGrpcSdk.Logger.log('Connecting to database...'); this.mongoose .connect(this.connectionString, this.options) diff --git a/modules/database/src/admin/schema.admin.ts b/modules/database/src/admin/schema.admin.ts index 43540f599..3b8fbbff6 100644 --- a/modules/database/src/admin/schema.admin.ts +++ b/modules/database/src/admin/schema.admin.ts @@ -590,6 +590,9 @@ export class SchemaAdmin { }, { userId: '64a415b2894aa4e2c0fdbb42' }, ); + const test3 = await this.database + .getSchemaModel('AccessToken') + .model.findMany({}, { populate: ['user'] }); return this.database.getDatabaseType(); } From bec5e79e2f86e1fef2c23c88bf84bfa3ea30f280 Mon Sep 17 00:00:00 2001 From: chris Date: Mon, 10 Jul 2023 16:52:14 +0300 Subject: [PATCH 3/7] refactor(database): refactor views with authorized query --- .../mongoose-adapter/MongooseSchema.ts | 210 +++++++++--------- modules/database/src/admin/schema.admin.ts | 15 -- 2 files changed, 110 insertions(+), 115 deletions(-) diff --git a/modules/database/src/adapters/mongoose-adapter/MongooseSchema.ts b/modules/database/src/adapters/mongoose-adapter/MongooseSchema.ts index c2a195adb..b6948f2fd 100644 --- a/modules/database/src/adapters/mongoose-adapter/MongooseSchema.ts +++ b/modules/database/src/adapters/mongoose-adapter/MongooseSchema.ts @@ -18,9 +18,10 @@ import { } from '../../interfaces'; import { MongooseAdapter } from './index'; import ConduitGrpcSdk, { - ConduitModel, ConduitSchema, + GrpcError, Indexable, + status, UntypedArray, } from '@conduitplatform/grpc-sdk'; import { cloneDeep, isEmpty, isNil } from 'lodash'; @@ -77,7 +78,6 @@ export class MongooseSchema extends SchemaAdapter> { createdAt: new Date(), updatedAt: new Date(), }; - const obj = await this.model.create(parsedQuery).then(r => r.toObject()); await this.addPermissionToData(obj, options); return obj; @@ -130,14 +130,14 @@ export class MongooseSchema extends SchemaAdapter> { populate?: string[]; }, ) { - let parsedFilter: Indexable | null = parseQuery(this.parseStringToQuery(filterQuery)); - parsedFilter = await this.getAuthorizedQuery( + const parsedFilter = await this.getAuthorizedIdsQuery( + parseQuery(this.parseStringToQuery(filterQuery)), 'edit', - parsedFilter, - false, - options?.userId, - options?.scope, - ); + options, + ).then(r => r.parsedQuery); + if (isNil(parsedFilter)) { + throw new GrpcError(status.PERMISSION_DENIED, 'Access denied'); + } let parsedQuery: ParsedQuery = this.parseStringToQuery(query); if (parsedQuery.hasOwnProperty('$set')) { parsedQuery = parsedQuery['$set']; @@ -161,14 +161,14 @@ export class MongooseSchema extends SchemaAdapter> { populate?: string[]; }, ) { - let parsedFilter: Indexable | null = parseQuery(this.parseStringToQuery(filterQuery)); - parsedFilter = await this.getAuthorizedQuery( + const parsedFilter = await this.getAuthorizedIdsQuery( + parseQuery(this.parseStringToQuery(filterQuery)), 'edit', - parsedFilter, - false, - options?.userId, - options?.scope, - ); + options, + ).then(r => r.parsedQuery); + if (isNil(parsedFilter)) { + throw new GrpcError(status.PERMISSION_DENIED, 'Access denied'); + } let parsedQuery: ParsedQuery = this.parseStringToQuery(query); if (parsedQuery.hasOwnProperty('$set')) { parsedQuery = parsedQuery['$set']; @@ -192,14 +192,11 @@ export class MongooseSchema extends SchemaAdapter> { scope?: string; }, ) { - let parsedFilter: Indexable | null = parseQuery(this.parseStringToQuery(filterQuery)); - parsedFilter = await this.getAuthorizedQuery( + const parsedFilter = await this.getAuthorizedIdsQuery( + parseQuery(this.parseStringToQuery(filterQuery)), 'edit', - parsedFilter, - true, - options?.userId, - options?.scope, - ); + options, + ).then(r => r.parsedQuery); if (isNil(parsedFilter)) { return []; } @@ -218,14 +215,14 @@ export class MongooseSchema extends SchemaAdapter> { scope?: string; }, ) { - let parsedQuery: Indexable | null = parseQuery(this.parseStringToQuery(query)); - parsedQuery = await this.getAuthorizedQuery( + const { parsedQuery } = await this.getAuthorizedIdsQuery( + parseQuery(this.parseStringToQuery(query)), 'delete', - parsedQuery, - false, - options?.userId, - options?.scope, + options, ); + if (isNil(parsedQuery)) { + throw new GrpcError(status.PERMISSION_DENIED, 'Access denied'); + } return this.model.deleteOne(parsedQuery!).exec(); } @@ -236,13 +233,10 @@ export class MongooseSchema extends SchemaAdapter> { scope?: string; }, ) { - let parsedQuery: Indexable | null = parseQuery(this.parseStringToQuery(query)); - parsedQuery = await this.getAuthorizedQuery( + const { parsedQuery } = await this.getAuthorizedIdsQuery( + parseQuery(this.parseStringToQuery(query)), 'delete', - parsedQuery, - true, - options?.userId, - options?.scope, + options, ); if (isNil(parsedQuery)) { return []; @@ -262,26 +256,20 @@ export class MongooseSchema extends SchemaAdapter> { scope?: string; }, ) { - const parsedQuery = parseQuery(this.parseStringToQuery(query)); - const authorizedQueryPipeline = await this.getAuthorizedQueryPipeline( + const { parsedQuery, modified } = await this.getAuthorizedIdsQuery( + parseQuery(this.parseStringToQuery(query)), 'read', - options?.userId, - options?.scope, + options, ); - if (!isEmpty(authorizedQueryPipeline)) { - const pipeline = this.constructAggregationPipeline( - parsedQuery, - authorizedQueryPipeline, - options, - ); - return this.model.aggregate(pipeline as PipelineStage[]); + if (isNil(parsedQuery)) { + return []; } let finalQuery = this.model.find(parsedQuery, options?.select); - if (!isNil(options?.skip)) { - finalQuery = finalQuery.skip(options!.skip); + if (!isNil(options?.skip) && !modified) { + finalQuery = finalQuery.skip(options!.skip!); } - if (!isNil(options?.limit)) { - finalQuery = finalQuery.limit(options!.limit); + if (!isNil(options?.limit) && !modified) { + finalQuery = finalQuery.limit(options!.limit!); } if (!isNil(options?.populate)) { finalQuery = this.populate(finalQuery, options?.populate ?? []); @@ -301,21 +289,15 @@ export class MongooseSchema extends SchemaAdapter> { populate?: string[]; }, ) { - const parsedQuery = parseQuery(this.parseStringToQuery(query)); - const authorizedQueryPipeline = await this.getAuthorizedQueryPipeline( + const { parsedQuery } = await this.getAuthorizedIdsQuery( + parseQuery(this.parseStringToQuery(query)), 'read', - options?.userId, - options?.scope, + options, ); - if (!isEmpty(authorizedQueryPipeline)) { - const pipeline = this.constructAggregationPipeline( - parsedQuery, - authorizedQueryPipeline, - options, - ); - return this.model.aggregate(pipeline as PipelineStage[]); + if (isNil(parsedQuery)) { + throw new GrpcError(status.PERMISSION_DENIED, 'Access denied'); } - let finalQuery = this.model.findOne(parsedQuery!, options?.select); + let finalQuery = this.model.findOne(parsedQuery, options?.select); if (options?.populate !== undefined && options?.populate !== null) { finalQuery = this.populate(finalQuery, options?.populate); } @@ -329,16 +311,39 @@ export class MongooseSchema extends SchemaAdapter> { scope?: string; }, ) { + const parsedQuery = parseQuery(this.parseStringToQuery(query)); if (!isNil(options?.userId) || !isNil(options?.scope)) { - const view = await this.permissionCheck('read', options?.userId, options?.scope); - if (view) { - return view.countDocuments(query, { - userId: undefined, - scope: undefined, - }); + const authorizedPipeline = await this.getAuthorizedPipeline( + 'read', + options?.userId, + options?.scope, + ); + if (!isEmpty(authorizedPipeline)) { + authorizedPipeline.push( + ...[ + { + $group: { + _id: null, + count: { $sum: 1 }, + }, + }, + { + $project: { + _id: 0, + count: 1, + }, + }, + ], + ); + const pipeline = this.constructAggregationPipeline( + parsedQuery, + authorizedPipeline, + ); + return this.model + .aggregate(pipeline as PipelineStage[]) + .then(r => (!isEmpty(r) ? r[0].count : 0)); } } - const parsedQuery = parseQuery(this.parseStringToQuery(query)); return this.model.find(parsedQuery).countDocuments().exec(); } @@ -466,40 +471,10 @@ export class MongooseSchema extends SchemaAdapter> { if (!isNil(options?.sort)) { pipeline.push({ $sort: this.parseSort(options?.sort) }); } - if (!isNil(options?.populate) && !isEmpty(options?.populate)) { - pipeline.push([...this.parsePipelinePopulate(options!.populate)]); - } - return pipeline; - } - - // TODO: check this - private parsePipelinePopulate(population: string[]) { - const pipeline: object[] = []; - const populates = this.calculatePopulates(population); - for (const populate of populates) { - let field; - if (typeof populate === 'object') { - field = populate.path; - } else { - field = populate; - } - const model = ( - (this.schema as _ConduitSchema).compiledFields[field as string] as ConduitModel - ).model!; - const relatedCollection = this.adapter.models[model].originalSchema.collectionName; - pipeline.push({ - $lookup: { - from: relatedCollection, - localField: field, - foreignField: '_id', - as: field, - }, - }); - } return pipeline; } - private async getAuthorizedQueryPipeline( + private async getAuthorizedPipeline( operation: string, userId?: string, scope?: string, @@ -533,4 +508,39 @@ export class MongooseSchema extends SchemaAdapter> { }); return query.mongoQuery; } + + private async getAuthorizedIdsQuery( + parsedQuery: Indexable, + operation: string, + options?: { + skip?: number; + limit?: number; + select?: string; + sort?: any; + populate?: string[]; + userId?: string; + scope?: string; + }, + ) { + const authorizedPipeline = await this.getAuthorizedPipeline( + operation, + options?.userId, + options?.scope, + ); + if (isEmpty(authorizedPipeline)) { + return { parsedQuery, modified: false }; + } + const pipeline = this.constructAggregationPipeline( + parsedQuery, + authorizedPipeline, + options, + ); + const ids = await this.model + .aggregate(pipeline as PipelineStage[]) + .then(r => r.map(r => r._id)); + if (isEmpty(ids)) { + return { parsedQuery: null, modified: false }; + } + return { parsedQuery: { _id: { $in: ids } }, modified: true }; + } } diff --git a/modules/database/src/admin/schema.admin.ts b/modules/database/src/admin/schema.admin.ts index 3b8fbbff6..fbdb7bc35 100644 --- a/modules/database/src/admin/schema.admin.ts +++ b/modules/database/src/admin/schema.admin.ts @@ -578,21 +578,6 @@ export class SchemaAdmin { } async getDatabaseType(): Promise { - const test1 = await this.database.getSchemaModel('File').model.findMany( - { - _id: '64a41678894aa4e2c0fdbc08', - }, - { userId: '64a415a2894aa4e2c0fdbb3f' }, - ); - const test2 = await this.database.getSchemaModel('File').model.findMany( - { - _id: '64a41678894aa4e2c0fdbc08', - }, - { userId: '64a415b2894aa4e2c0fdbb42' }, - ); - const test3 = await this.database - .getSchemaModel('AccessToken') - .model.findMany({}, { populate: ['user'] }); return this.database.getDatabaseType(); } From 8ca2daf5a596a9d604e11f6367a894b4b5afd3f0 Mon Sep 17 00:00:00 2001 From: Konstantinos Feretos Date: Mon, 20 Nov 2023 12:49:38 +0200 Subject: [PATCH 4/7] chore(authentication,database): housekeeping --- .../src/controllers/permissions.controller.ts | 6 +++--- .../mongoose-adapter/MongooseSchema.ts | 18 +++++++++--------- .../src/adapters/mongoose-adapter/index.ts | 5 ++--- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/modules/authorization/src/controllers/permissions.controller.ts b/modules/authorization/src/controllers/permissions.controller.ts index d131e30b0..8e66c1372 100644 --- a/modules/authorization/src/controllers/permissions.controller.ts +++ b/modules/authorization/src/controllers/permissions.controller.ts @@ -67,7 +67,7 @@ export class PermissionsController { } // if the actor is the object itself, all permissions are provided if (subject === object) { - await RuleCache.storeResolution(this.grpcSdk, computedTuple, true); + RuleCache.storeResolution(this.grpcSdk, computedTuple, true); return true; } @@ -75,13 +75,13 @@ export class PermissionsController { computedTuple, }); if (permission) { - await RuleCache.storeResolution(this.grpcSdk, computedTuple, true); + RuleCache.storeResolution(this.grpcSdk, computedTuple, true); return true; } const index = await this.indexController.findIndex(subject, action, object); - await RuleCache.storeResolution(this.grpcSdk, computedTuple, index ?? false); + RuleCache.storeResolution(this.grpcSdk, computedTuple, index ?? false); return index ?? false; } diff --git a/modules/database/src/adapters/mongoose-adapter/MongooseSchema.ts b/modules/database/src/adapters/mongoose-adapter/MongooseSchema.ts index fcc7d1ad9..72177356f 100644 --- a/modules/database/src/adapters/mongoose-adapter/MongooseSchema.ts +++ b/modules/database/src/adapters/mongoose-adapter/MongooseSchema.ts @@ -155,7 +155,7 @@ export class MongooseSchema extends SchemaAdapter> { scope?: string; populate?: string[]; }, - ) { + ): Promise { const parsedFilter = await this.getAuthorizedIdsQuery( parseQuery(this.parseStringToQuery(filterQuery)), 'edit', @@ -219,7 +219,7 @@ export class MongooseSchema extends SchemaAdapter> { return this.model .deleteOne(parsedQuery!) .exec() - .then(r => ({ deletedCount: r.deletedCount }));; + .then(r => ({ deletedCount: r.deletedCount })); } async deleteMany( @@ -260,10 +260,10 @@ export class MongooseSchema extends SchemaAdapter> { 'read', options, ); - if (isNil(filter)) { + if (isNil(parsedQuery)) { return []; } - let finalQuery = this.model.find(filter, options?.select); + let finalQuery = this.model.find(parsedQuery, options?.select); if (!isNil(options?.skip) && !modified) { finalQuery = finalQuery.skip(options!.skip!); } @@ -287,7 +287,7 @@ export class MongooseSchema extends SchemaAdapter> { select?: string; populate?: string[]; }, - ) { + ): Promise { const { parsedQuery } = await this.getAuthorizedIdsQuery( parseQuery(this.parseStringToQuery(query)), 'read', @@ -359,7 +359,7 @@ export class MongooseSchema extends SchemaAdapter> { public calculatePopulates(population: string[]) { const populates: (string | PopulateOptions)[] = []; - population.forEach((r: string | string[], index: number) => { + population.forEach((r: string | string[]) => { const final = r.toString().trim(); if (final.indexOf('.') !== -1) { let controlBool = true; @@ -454,7 +454,7 @@ export class MongooseSchema extends SchemaAdapter> { skip?: number; limit?: number; select?: string; - sort?: any; + sort?: { [p: string]: number }; populate?: string[]; userId?: string; scope?: string; @@ -468,7 +468,7 @@ export class MongooseSchema extends SchemaAdapter> { pipeline.push({ $limit: options?.limit }); } if (!isNil(options?.sort)) { - pipeline.push({ $sort: this.parseSort(options?.sort) }); + pipeline.push({ $sort: this.parseSort(options!.sort) }); } return pipeline; } @@ -515,7 +515,7 @@ export class MongooseSchema extends SchemaAdapter> { skip?: number; limit?: number; select?: string; - sort?: any; + sort?: { [p: string]: number }; populate?: string[]; userId?: string; scope?: string; diff --git a/modules/database/src/adapters/mongoose-adapter/index.ts b/modules/database/src/adapters/mongoose-adapter/index.ts index bb535f9ea..1732eebfc 100644 --- a/modules/database/src/adapters/mongoose-adapter/index.ts +++ b/modules/database/src/adapters/mongoose-adapter/index.ts @@ -29,7 +29,7 @@ mongoose.plugin(castAggregation); export class MongooseAdapter extends DatabaseAdapter { connected: boolean = false; - mongoose: Mongoose; + private readonly mongoose: Mongoose; connectionString: string; options: ConnectOptions = { minPoolSize: 5, @@ -82,7 +82,7 @@ export class MongooseAdapter extends DatabaseAdapter { return; } const model = this.models[modelName]; - let newSchema = model.schema; + const newSchema = model.schema; //@ts-ignore newSchema.name = viewName; //@ts-ignore @@ -333,7 +333,6 @@ export class MongooseAdapter extends DatabaseAdapter { } protected connect() { - this.mongoose = mongoose; ConduitGrpcSdk.Logger.log('Connecting to database...'); this.mongoose .connect(this.connectionString, this.options) From 765a5c7c45e062b03f0cede6dd686e50b7709735 Mon Sep 17 00:00:00 2001 From: Konstantinos Feretos Date: Mon, 20 Nov 2023 15:31:53 +0200 Subject: [PATCH 5/7] fix(authorization): unrebased mongo access list query --- modules/authorization/src/utils/index.ts | 34 +++++++++++++++--------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/modules/authorization/src/utils/index.ts b/modules/authorization/src/utils/index.ts index 8b340ea40..b984b60b7 100644 --- a/modules/authorization/src/utils/index.ts +++ b/modules/authorization/src/utils/index.ts @@ -121,29 +121,39 @@ export function getMongoAccessListQuery(params: AccessListQueryParams) { id_action: { $concat: [`${objectType}:`, { $toString: '$_id' }, `#${action}`], }, + entities: '$actors.entity', }, pipeline: [ { $match: { - $expr: { - $eq: ['$subject', '$$id_action'], - }, + $and: [ + { + $expr: { + $eq: ['$subject', '$$id_action'], + }, + }, + { + $expr: { + $in: ['$entity', '$$entities'], + }, + }, + ], }, }, ], - as: 'objects', - }, - }, - { - $addFields: { - intersection: { - $setIntersection: ['$actors.entity', '$objects.entity'], - }, + as: 'intersection', }, }, { $match: { - intersection: { $ne: [] }, + $or: [ + { + 'intersection.0': { $exists: true }, + }, + { + 'permissions.0': { $exists: true }, + }, + ], }, }, { From a51eeb58af2d7a575a6397334d4a1b6187acb35c Mon Sep 17 00:00:00 2001 From: Konstantinos Feretos Date: Fri, 24 Nov 2023 11:22:46 +0200 Subject: [PATCH 6/7] fix(database): mongo schema invalid authz query checks, descriptive errors --- .../mongoose-adapter/MongooseSchema.ts | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/modules/database/src/adapters/mongoose-adapter/MongooseSchema.ts b/modules/database/src/adapters/mongoose-adapter/MongooseSchema.ts index 8ef5bf360..91ff09304 100644 --- a/modules/database/src/adapters/mongoose-adapter/MongooseSchema.ts +++ b/modules/database/src/adapters/mongoose-adapter/MongooseSchema.ts @@ -19,9 +19,7 @@ import { import { MongooseAdapter } from './index'; import ConduitGrpcSdk, { ConduitSchema, - GrpcError, Indexable, - status, UntypedArray, } from '@conduitplatform/grpc-sdk'; import { cloneDeep, isEmpty, isNil } from 'lodash'; @@ -126,13 +124,16 @@ export class MongooseSchema extends SchemaAdapter> { populate?: string[]; }, ) { - const parsedFilter = await this.getAuthorizedIdsQuery( - parseQuery(this.parseStringToQuery(filterQuery)), + const parsedFilterQuery: Indexable | null = parseQuery( + this.parseStringToQuery(filterQuery), + ); + const { parsedQuery: parsedFilter } = await this.getAuthorizedIdsQuery( + parsedFilterQuery, 'edit', options, - ).then(r => r.parsedQuery); - if (isNil(parsedFilter)) { - throw new GrpcError(status.PERMISSION_DENIED, 'Access denied'); + ); + if (isNil(parsedFilter) && !isNil(filterQuery)) { + throw new Error("Document doesn't exist or can't be modified by user."); } let parsedQuery: ParsedQuery = this.parseStringToQuery(query); if (parsedQuery.hasOwnProperty('$set')) { @@ -156,13 +157,16 @@ export class MongooseSchema extends SchemaAdapter> { populate?: string[]; }, ): Promise { + const parsedFilterQuery: Indexable | null = parseQuery( + this.parseStringToQuery(filterQuery), + ); const parsedFilter = await this.getAuthorizedIdsQuery( - parseQuery(this.parseStringToQuery(filterQuery)), + parsedFilterQuery, 'edit', options, ).then(r => r.parsedQuery); - if (isNil(parsedFilter)) { - throw new GrpcError(status.PERMISSION_DENIED, 'Access denied'); + if (isNil(parsedFilter) && !isNil(filterQuery)) { + throw new Error("Document doesn't exist or can't be modified by user."); } let parsedQuery: ParsedQuery = this.parseStringToQuery(query); if (parsedQuery.hasOwnProperty('$set')) { @@ -297,7 +301,7 @@ export class MongooseSchema extends SchemaAdapter> { options?.scope, ); if (isNil(filter) && !isNil(parsedQuery)) { - throw new GrpcError(status.PERMISSION_DENIED, 'Access denied'); + return null; } let finalQuery = this.model.findOne(parsedQuery!, options?.select); if (options?.populate !== undefined && options?.populate !== null) { From 375adeb49a1a719febf7876e2e75670beab358fd Mon Sep 17 00:00:00 2001 From: Konstantinos Feretos Date: Fri, 24 Nov 2023 15:20:25 +0200 Subject: [PATCH 7/7] fix(database): query types, Authz check condition cleanups (#817) * fix(database): parsedQuery types * fix(database): sql/mongo null queries ignored due to authz checks * fix(database): isNil checks not checking empty objects * fix(database): null-less query types --- .../database/src/adapters/SchemaAdapter.ts | 6 +- .../mongoose-adapter/MongooseSchema.ts | 55 +++++++++---------- .../sequelize-adapter/SequelizeSchema.ts | 19 +++---- .../sequelize-adapter/utils/pathUtils.ts | 4 +- .../src/controllers/cms/schema.controller.ts | 3 +- 5 files changed, 37 insertions(+), 50 deletions(-) diff --git a/modules/database/src/adapters/SchemaAdapter.ts b/modules/database/src/adapters/SchemaAdapter.ts index a7aacea3b..1b733cd6a 100644 --- a/modules/database/src/adapters/SchemaAdapter.ts +++ b/modules/database/src/adapters/SchemaAdapter.ts @@ -10,8 +10,8 @@ export type SingleDocQuery = string | Indexable; export type MultiDocQuery = string | Indexable[]; export type Query = SingleDocQuery | MultiDocQuery; export type ParsedQuery = Indexable; -export type Doc = ParsedQuery; -export type Fields = ParsedQuery; +export type Doc = Indexable; +export type Fields = Indexable; export type Schema = MongooseSchema | SequelizeSchema; export abstract class SchemaAdapter { @@ -123,7 +123,7 @@ export abstract class SchemaAdapter { async getAuthorizedQuery( operation: string, - query: Indexable, + query: ParsedQuery, many: boolean = false, userId?: string, scope?: string, diff --git a/modules/database/src/adapters/mongoose-adapter/MongooseSchema.ts b/modules/database/src/adapters/mongoose-adapter/MongooseSchema.ts index 91ff09304..0b45723ce 100644 --- a/modules/database/src/adapters/mongoose-adapter/MongooseSchema.ts +++ b/modules/database/src/adapters/mongoose-adapter/MongooseSchema.ts @@ -124,22 +124,20 @@ export class MongooseSchema extends SchemaAdapter> { populate?: string[]; }, ) { - const parsedFilterQuery: Indexable | null = parseQuery( - this.parseStringToQuery(filterQuery), - ); + const parsedFilterQuery = parseQuery(this.parseStringToQuery(filterQuery)); const { parsedQuery: parsedFilter } = await this.getAuthorizedIdsQuery( parsedFilterQuery, 'edit', options, ); - if (isNil(parsedFilter) && !isNil(filterQuery)) { + if (isNil(parsedFilter)) { throw new Error("Document doesn't exist or can't be modified by user."); } let parsedQuery: ParsedQuery = this.parseStringToQuery(query); if (parsedQuery.hasOwnProperty('$set')) { parsedQuery = parsedQuery['$set']; } - let finalQuery = this.model.findOneAndReplace(parsedFilter!, parsedQuery, { + let finalQuery = this.model.findOneAndReplace(parsedFilter, parsedQuery, { new: true, }); if (options?.populate !== undefined && options?.populate !== null) { @@ -157,22 +155,20 @@ export class MongooseSchema extends SchemaAdapter> { populate?: string[]; }, ): Promise { - const parsedFilterQuery: Indexable | null = parseQuery( - this.parseStringToQuery(filterQuery), - ); - const parsedFilter = await this.getAuthorizedIdsQuery( + const parsedFilterQuery = parseQuery(this.parseStringToQuery(filterQuery)); + const { parsedQuery: parsedFilter } = await this.getAuthorizedIdsQuery( parsedFilterQuery, 'edit', options, - ).then(r => r.parsedQuery); - if (isNil(parsedFilter) && !isNil(filterQuery)) { + ); + if (isNil(parsedFilter)) { throw new Error("Document doesn't exist or can't be modified by user."); } let parsedQuery: ParsedQuery = this.parseStringToQuery(query); if (parsedQuery.hasOwnProperty('$set')) { parsedQuery = parsedQuery['$set']; } - let finalQuery = this.model.findOneAndUpdate(parsedFilter!, parsedQuery, { + let finalQuery = this.model.findOneAndUpdate(parsedFilter, parsedQuery, { new: true, }); if (options?.populate !== undefined && options?.populate !== null) { @@ -190,15 +186,15 @@ export class MongooseSchema extends SchemaAdapter> { scope?: string; }, ) { - const parsedFilter = await this.getAuthorizedIdsQuery( + const { parsedQuery: parsedFilter } = await this.getAuthorizedIdsQuery( parseQuery(this.parseStringToQuery(filterQuery)), 'edit', options, - ).then(r => r.parsedQuery); + ); if (isNil(parsedFilter)) { return []; } - let parsedQuery: Indexable = this.parseStringToQuery(query); + let parsedQuery: ParsedQuery = this.parseStringToQuery(query); if (parsedQuery.hasOwnProperty('$set')) { parsedQuery = parsedQuery['$set']; } @@ -212,16 +208,16 @@ export class MongooseSchema extends SchemaAdapter> { scope?: string; }, ) { - const { parsedQuery } = await this.getAuthorizedIdsQuery( + const { parsedQuery: parsedFilter } = await this.getAuthorizedIdsQuery( parseQuery(this.parseStringToQuery(query)), 'delete', options, ); - if (isNil(parsedQuery)) { + if (isNil(parsedFilter)) { return { deletedCount: 0 }; } return this.model - .deleteOne(parsedQuery!) + .deleteOne(parsedFilter!) .exec() .then(r => ({ deletedCount: r.deletedCount })); } @@ -233,16 +229,16 @@ export class MongooseSchema extends SchemaAdapter> { scope?: string; }, ) { - const { parsedQuery } = await this.getAuthorizedIdsQuery( + const { parsedQuery: parsedFilter } = await this.getAuthorizedIdsQuery( parseQuery(this.parseStringToQuery(query)), 'delete', options, ); - if (isNil(parsedQuery)) { + if (isNil(parsedFilter)) { return { deletedCount: 0 }; } return this.model - .deleteMany(parsedQuery) + .deleteMany(parsedFilter) .exec() .then(r => ({ deletedCount: r.deletedCount })); } @@ -259,15 +255,15 @@ export class MongooseSchema extends SchemaAdapter> { scope?: string; }, ) { - const { parsedQuery, modified } = await this.getAuthorizedIdsQuery( + const { parsedQuery: parsedFilter, modified } = await this.getAuthorizedIdsQuery( parseQuery(this.parseStringToQuery(query)), 'read', options, ); - if (isNil(parsedQuery)) { + if (isNil(parsedFilter)) { return []; } - let finalQuery = this.model.find(parsedQuery, options?.select); + let finalQuery = this.model.find(parsedFilter, options?.select); if (!isNil(options?.skip) && !modified) { finalQuery = finalQuery.skip(options!.skip!); } @@ -292,18 +288,17 @@ export class MongooseSchema extends SchemaAdapter> { populate?: string[]; }, ): Promise { - const parsedQuery: Indexable | null = parseQuery(this.parseStringToQuery(query)); const filter = await this.getAuthorizedQuery( 'read', - parsedQuery, + parseQuery(this.parseStringToQuery(query)), false, options?.userId, options?.scope, ); - if (isNil(filter) && !isNil(parsedQuery)) { + if (isNil(filter)) { return null; } - let finalQuery = this.model.findOne(parsedQuery!, options?.select); + let finalQuery = this.model.findOne(filter, options?.select); if (options?.populate !== undefined && options?.populate !== null) { finalQuery = this.populate(finalQuery, options?.populate); } @@ -455,7 +450,7 @@ export class MongooseSchema extends SchemaAdapter> { } private constructAggregationPipeline( - parsedQuery: Indexable, + parsedQuery: ParsedQuery, authorizedQueryPipeline: object[], options?: { skip?: number; @@ -516,7 +511,7 @@ export class MongooseSchema extends SchemaAdapter> { } private async getAuthorizedIdsQuery( - parsedQuery: Indexable, + parsedQuery: ParsedQuery, operation: string, options?: { skip?: number; diff --git a/modules/database/src/adapters/sequelize-adapter/SequelizeSchema.ts b/modules/database/src/adapters/sequelize-adapter/SequelizeSchema.ts index 91719f129..1755fa135 100644 --- a/modules/database/src/adapters/sequelize-adapter/SequelizeSchema.ts +++ b/modules/database/src/adapters/sequelize-adapter/SequelizeSchema.ts @@ -168,7 +168,6 @@ export class SequelizeSchema extends SchemaAdapter> { }) .then(doc => (doc ? doc.toJSON() : doc)); } - if (parsedQuery.hasOwnProperty('$push')) { const push = parsedQuery['$push']; for (const key in push) { @@ -197,7 +196,6 @@ export class SequelizeSchema extends SchemaAdapter> { await parentDoc.save(); delete parsedQuery['$push']; } - if (Object.keys(parsedQuery).length === 0) { return this.model .findByPk(parsedId, { @@ -262,9 +260,7 @@ export class SequelizeSchema extends SchemaAdapter> { t = await this.sequelize.transaction({ type: Transaction.TYPES.IMMEDIATE }); } const obj = await this.model - .create(parsedQuery, { - transaction: t, - }) + .create(parsedQuery, { transaction: t }) .then(doc => createWithPopulation(this, doc, relationObjects, t)) .then(doc => { if (!transactionProvided) { @@ -301,7 +297,7 @@ export class SequelizeSchema extends SchemaAdapter> { extractRelationsModification(this, parsedQuery[i]); } const docs = await this.model - .bulkCreate(parsedQuery, { transaction: t }) + .bulkCreate(parsedQuery as Indexable[], { transaction: t }) .then(docs => { t.commit(); return docs; @@ -339,7 +335,7 @@ export class SequelizeSchema extends SchemaAdapter> { options?.userId, options?.scope, ); - if (isNil(filter) && !isNil(query)) { + if (isNil(filter)) { return null; } const { filter: parsedFilter, parsingResult } = parseQueryFilter( @@ -566,20 +562,19 @@ export class SequelizeSchema extends SchemaAdapter> { scope?: string; }, ) { - const parsedQuery: ParsedQuery = this.parseStringToQuery(query); - const parsedFilterQuery = await this.getAuthorizedQuery( + const parsedFilter = await this.getAuthorizedQuery( 'edit', this.parseStringToQuery(filterQuery), true, options?.userId, options?.scope, ); - if (isNil(parsedFilterQuery)) { + if (isNil(parsedFilter)) { return []; } const parsingResult = parseQuery( this.originalSchema, - parsedFilterQuery, + parsedFilter, this.adapter.sequelize.getDialect(), this.extractedRelations, {}, @@ -595,7 +590,7 @@ export class SequelizeSchema extends SchemaAdapter> { try { const data = await Promise.all( docs.map(doc => - this.findByIdAndUpdate(doc._id, parsedQuery, { + this.findByIdAndUpdate(doc._id, parsedFilter, { populate: options?.populate, transaction: t, }), diff --git a/modules/database/src/adapters/sequelize-adapter/utils/pathUtils.ts b/modules/database/src/adapters/sequelize-adapter/utils/pathUtils.ts index 5aef7a79b..a4ca316bd 100644 --- a/modules/database/src/adapters/sequelize-adapter/utils/pathUtils.ts +++ b/modules/database/src/adapters/sequelize-adapter/utils/pathUtils.ts @@ -15,9 +15,7 @@ function potentialNesting(field: any) { export function processCreateQuery( query: Indexable, - keyMapping: { - [key: string]: { parentKey: string; childKey: string }; - }, + keyMapping: { [key: string]: { parentKey: string; childKey: string } }, ) { const foundKeys = []; for (const key in keyMapping) { diff --git a/modules/database/src/controllers/cms/schema.controller.ts b/modules/database/src/controllers/cms/schema.controller.ts index 95a797348..8113fc789 100644 --- a/modules/database/src/controllers/cms/schema.controller.ts +++ b/modules/database/src/controllers/cms/schema.controller.ts @@ -9,7 +9,6 @@ import { DatabaseAdapter } from '../../adapters/DatabaseAdapter'; import { MongooseSchema } from '../../adapters/mongoose-adapter/MongooseSchema'; import { SequelizeSchema } from '../../adapters/sequelize-adapter/SequelizeSchema'; import { CmsHandlers } from '../../handlers/cms/crud.handler'; -import { ParsedQuery } from '../../interfaces'; import { status } from '@grpc/grpc-js'; import { isNil } from 'lodash'; @@ -184,7 +183,7 @@ export class SchemaController { }); } - private _registerRoutes(schemas: ParsedQuery) { + private _registerRoutes(schemas: Indexable) { const handlers = new CmsHandlers(this.grpcSdk, this.database); this.router!.addRoutes(sortAndConstructRoutes(schemas, handlers)); }