mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-08 10:28:00 +00:00
feat: added template based permission
This commit is contained in:
@@ -3,12 +3,12 @@ import slugify from "@sindresorhus/slugify";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { ProjectMembershipRole, ProjectMembershipsSchema, ProjectRolesSchema } from "@app/db/schemas";
|
import { ProjectMembershipRole, ProjectMembershipsSchema, ProjectRolesSchema } from "@app/db/schemas";
|
||||||
|
import { ProjectPermissionSchema } from "@app/ee/services/permission/project-permission";
|
||||||
import { PROJECT_ROLE } from "@app/lib/api-docs";
|
import { PROJECT_ROLE } from "@app/lib/api-docs";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
|
||||||
import { ProjectPermissionSchema } from "@app/ee/services/permission/project-permission";
|
|
||||||
import { SanitizedRoleSchema } from "@app/server/routes/sanitizedSchemas";
|
import { SanitizedRoleSchema } from "@app/server/routes/sanitizedSchemas";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
server.route({
|
||||||
|
|||||||
@@ -170,6 +170,7 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
.join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`)
|
.join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`)
|
||||||
.select(
|
.select(
|
||||||
db.ref("id").withSchema(TableName.Users).as("userId"),
|
db.ref("id").withSchema(TableName.Users).as("userId"),
|
||||||
|
db.ref("username").withSchema(TableName.Users).as("username"),
|
||||||
// groups specific
|
// groups specific
|
||||||
db.ref("id").withSchema(TableName.GroupProjectMembership).as("groupMembershipId"),
|
db.ref("id").withSchema(TableName.GroupProjectMembership).as("groupMembershipId"),
|
||||||
db.ref("createdAt").withSchema(TableName.GroupProjectMembership).as("groupMembershipCreatedAt"),
|
db.ref("createdAt").withSchema(TableName.GroupProjectMembership).as("groupMembershipCreatedAt"),
|
||||||
@@ -267,6 +268,7 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
key: "projectId",
|
key: "projectId",
|
||||||
parentMapper: ({
|
parentMapper: ({
|
||||||
orgId,
|
orgId,
|
||||||
|
username,
|
||||||
orgAuthEnforced,
|
orgAuthEnforced,
|
||||||
membershipId,
|
membershipId,
|
||||||
groupMembershipId,
|
groupMembershipId,
|
||||||
@@ -279,6 +281,7 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
orgAuthEnforced,
|
orgAuthEnforced,
|
||||||
userId,
|
userId,
|
||||||
projectId,
|
projectId,
|
||||||
|
username,
|
||||||
id: membershipId || groupMembershipId,
|
id: membershipId || groupMembershipId,
|
||||||
createdAt: membershipCreatedAt || groupMembershipCreatedAt,
|
createdAt: membershipCreatedAt || groupMembershipCreatedAt,
|
||||||
updatedAt: membershipUpdatedAt || groupMembershipUpdatedAt
|
updatedAt: membershipUpdatedAt || groupMembershipUpdatedAt
|
||||||
@@ -399,6 +402,7 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
`${TableName.IdentityProjectMembershipRole}.projectMembershipId`,
|
`${TableName.IdentityProjectMembershipRole}.projectMembershipId`,
|
||||||
`${TableName.IdentityProjectMembership}.id`
|
`${TableName.IdentityProjectMembership}.id`
|
||||||
)
|
)
|
||||||
|
.join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.IdentityProjectMembership}.identityId`)
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
TableName.ProjectRoles,
|
TableName.ProjectRoles,
|
||||||
`${TableName.IdentityProjectMembershipRole}.customRoleId`,
|
`${TableName.IdentityProjectMembershipRole}.customRoleId`,
|
||||||
@@ -420,6 +424,7 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
.select(selectAllTableCols(TableName.IdentityProjectMembershipRole))
|
.select(selectAllTableCols(TableName.IdentityProjectMembershipRole))
|
||||||
.select(
|
.select(
|
||||||
db.ref("id").withSchema(TableName.IdentityProjectMembership).as("membershipId"),
|
db.ref("id").withSchema(TableName.IdentityProjectMembership).as("membershipId"),
|
||||||
|
db.ref("name").withSchema(TableName.Identity).as("identityName"),
|
||||||
db.ref("orgId").withSchema(TableName.Project).as("orgId"), // Now you can select orgId from Project
|
db.ref("orgId").withSchema(TableName.Project).as("orgId"), // Now you can select orgId from Project
|
||||||
db.ref("createdAt").withSchema(TableName.IdentityProjectMembership).as("membershipCreatedAt"),
|
db.ref("createdAt").withSchema(TableName.IdentityProjectMembership).as("membershipCreatedAt"),
|
||||||
db.ref("updatedAt").withSchema(TableName.IdentityProjectMembership).as("membershipUpdatedAt"),
|
db.ref("updatedAt").withSchema(TableName.IdentityProjectMembership).as("membershipUpdatedAt"),
|
||||||
@@ -449,9 +454,10 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
const permission = sqlNestRelationships({
|
const permission = sqlNestRelationships({
|
||||||
data: docs,
|
data: docs,
|
||||||
key: "membershipId",
|
key: "membershipId",
|
||||||
parentMapper: ({ membershipId, membershipCreatedAt, membershipUpdatedAt, orgId }) => ({
|
parentMapper: ({ membershipId, membershipCreatedAt, membershipUpdatedAt, orgId, identityName }) => ({
|
||||||
id: membershipId,
|
id: membershipId,
|
||||||
identityId,
|
identityId,
|
||||||
|
username: identityName,
|
||||||
projectId,
|
projectId,
|
||||||
createdAt: membershipCreatedAt,
|
createdAt: membershipCreatedAt,
|
||||||
updatedAt: membershipUpdatedAt,
|
updatedAt: membershipUpdatedAt,
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
export type TBuildProjectPermissionDTO = {
|
||||||
|
permissions?: unknown;
|
||||||
|
role: string;
|
||||||
|
}[];
|
||||||
|
|
||||||
|
export type TBuildOrgPermissionDTO = {
|
||||||
|
permissions?: unknown;
|
||||||
|
role: string;
|
||||||
|
}[];
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { createMongoAbility, MongoAbility, RawRuleOf } from "@casl/ability";
|
import { createMongoAbility, MongoAbility, RawRuleOf } from "@casl/ability";
|
||||||
import { PackRule, unpackRules } from "@casl/ability/extra";
|
import { PackRule, unpackRules } from "@casl/ability/extra";
|
||||||
import { MongoQuery } from "@ucast/mongo2js";
|
import { MongoQuery } from "@ucast/mongo2js";
|
||||||
|
import handlebars from "handlebars";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
OrgMembershipRole,
|
OrgMembershipRole,
|
||||||
@@ -20,7 +21,7 @@ import { TServiceTokenDALFactory } from "@app/services/service-token/service-tok
|
|||||||
import { orgAdminPermissions, orgMemberPermissions, orgNoAccessPermissions, OrgPermissionSet } from "./org-permission";
|
import { orgAdminPermissions, orgMemberPermissions, orgNoAccessPermissions, OrgPermissionSet } from "./org-permission";
|
||||||
import { TPermissionDALFactory } from "./permission-dal";
|
import { TPermissionDALFactory } from "./permission-dal";
|
||||||
import { validateOrgSAML } from "./permission-fns";
|
import { validateOrgSAML } from "./permission-fns";
|
||||||
import { TBuildOrgPermissionDTO, TBuildProjectPermissionDTO } from "./permission-types";
|
import { TBuildOrgPermissionDTO, TBuildProjectPermissionDTO } from "./permission-service-types";
|
||||||
import {
|
import {
|
||||||
buildServiceTokenProjectPermission,
|
buildServiceTokenProjectPermission,
|
||||||
projectAdminPermissions,
|
projectAdminPermissions,
|
||||||
@@ -72,7 +73,7 @@ export const permissionServiceFactory = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildProjectPermission = (projectUserRoles: TBuildProjectPermissionDTO) => {
|
const buildProjectPermissionRules = (projectUserRoles: TBuildProjectPermissionDTO) => {
|
||||||
const rules = projectUserRoles
|
const rules = projectUserRoles
|
||||||
.map(({ role, permissions }) => {
|
.map(({ role, permissions }) => {
|
||||||
switch (role) {
|
switch (role) {
|
||||||
@@ -98,9 +99,7 @@ export const permissionServiceFactory = ({
|
|||||||
})
|
})
|
||||||
.reduce((curr, prev) => prev.concat(curr), []);
|
.reduce((curr, prev) => prev.concat(curr), []);
|
||||||
|
|
||||||
return createMongoAbility<ProjectPermissionSet>(rules, {
|
return rules;
|
||||||
conditionsMatcher
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -223,8 +222,21 @@ export const permissionServiceFactory = ({
|
|||||||
permissions
|
permissions
|
||||||
})) || [];
|
})) || [];
|
||||||
|
|
||||||
|
const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges));
|
||||||
|
const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false });
|
||||||
|
const interpolateRules = templatedRules(
|
||||||
|
{ identity: { id: userProjectPermission.userId, username: userProjectPermission.username } },
|
||||||
|
{ data: false }
|
||||||
|
);
|
||||||
|
const permission = createMongoAbility<ProjectPermissionSet>(
|
||||||
|
JSON.parse(interpolateRules) as RawRuleOf<MongoAbility<ProjectPermissionSet>>[],
|
||||||
|
{
|
||||||
|
conditionsMatcher
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
permission: buildProjectPermission(rolePermissions.concat(additionalPrivileges)),
|
permission,
|
||||||
membership: userProjectPermission,
|
membership: userProjectPermission,
|
||||||
hasRole: (role: string) =>
|
hasRole: (role: string) =>
|
||||||
userProjectPermission.roles.findIndex(
|
userProjectPermission.roles.findIndex(
|
||||||
@@ -262,8 +274,21 @@ export const permissionServiceFactory = ({
|
|||||||
permissions
|
permissions
|
||||||
})) || [];
|
})) || [];
|
||||||
|
|
||||||
|
const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges));
|
||||||
|
const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false });
|
||||||
|
const interpolateRules = templatedRules(
|
||||||
|
{ identity: { id: identityProjectPermission.identityId, username: identityProjectPermission.username } },
|
||||||
|
{ data: false }
|
||||||
|
);
|
||||||
|
const permission = createMongoAbility<ProjectPermissionSet>(
|
||||||
|
JSON.parse(interpolateRules) as RawRuleOf<MongoAbility<ProjectPermissionSet>>[],
|
||||||
|
{
|
||||||
|
conditionsMatcher
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
permission: buildProjectPermission(rolePermissions.concat(additionalPrivileges)),
|
permission,
|
||||||
membership: identityProjectPermission,
|
membership: identityProjectPermission,
|
||||||
hasRole: (role: string) =>
|
hasRole: (role: string) =>
|
||||||
identityProjectPermission.roles.findIndex(
|
identityProjectPermission.roles.findIndex(
|
||||||
@@ -346,14 +371,22 @@ export const permissionServiceFactory = ({
|
|||||||
if (isCustomRole) {
|
if (isCustomRole) {
|
||||||
const projectRole = await projectRoleDAL.findOne({ slug: role, projectId });
|
const projectRole = await projectRoleDAL.findOne({ slug: role, projectId });
|
||||||
if (!projectRole) throw new NotFoundError({ message: `Specified role was not found: ${role}` });
|
if (!projectRole) throw new NotFoundError({ message: `Specified role was not found: ${role}` });
|
||||||
|
const rules = buildProjectPermissionRules([
|
||||||
|
{ role: ProjectMembershipRole.Custom, permissions: projectRole.permissions }
|
||||||
|
]);
|
||||||
return {
|
return {
|
||||||
permission: buildProjectPermission([
|
permission: createMongoAbility<ProjectPermissionSet>(rules, {
|
||||||
{ role: ProjectMembershipRole.Custom, permissions: projectRole.permissions }
|
conditionsMatcher
|
||||||
]),
|
}),
|
||||||
role: projectRole
|
role: projectRole
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return { permission: buildProjectPermission([{ role, permissions: [] }]) };
|
|
||||||
|
const rules = buildProjectPermissionRules([{ role, permissions: [] }]);
|
||||||
|
const permission = createMongoAbility<ProjectPermissionSet>(rules, {
|
||||||
|
conditionsMatcher
|
||||||
|
});
|
||||||
|
return { permission };
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -364,6 +397,6 @@ export const permissionServiceFactory = ({
|
|||||||
getOrgPermissionByRole,
|
getOrgPermissionByRole,
|
||||||
getProjectPermissionByRole,
|
getProjectPermissionByRole,
|
||||||
buildOrgPermission,
|
buildOrgPermission,
|
||||||
buildProjectPermission
|
buildProjectPermissionRules
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,22 +1,12 @@
|
|||||||
import picomatch from "picomatch";
|
import picomatch from "picomatch";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
export type TBuildProjectPermissionDTO = {
|
|
||||||
permissions?: unknown;
|
|
||||||
role: string;
|
|
||||||
}[];
|
|
||||||
|
|
||||||
export type TBuildOrgPermissionDTO = {
|
|
||||||
permissions?: unknown;
|
|
||||||
role: string;
|
|
||||||
}[];
|
|
||||||
|
|
||||||
export enum PermissionConditionOperators {
|
export enum PermissionConditionOperators {
|
||||||
$IN = "$in",
|
$IN = "$in",
|
||||||
$ALL = "$all",
|
$ALL = "$all",
|
||||||
$REGEX = "$regex",
|
$REGEX = "$regex",
|
||||||
$EQ = "$eq",
|
$EQ = "$eq",
|
||||||
$NEQ = "$neq",
|
$NEQ = "$ne",
|
||||||
$GLOB = "$glob"
|
$GLOB = "$glob"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import { AbilityBuilder, createMongoAbility, ForcedSubject, MongoAbility } from "@casl/ability";
|
import { AbilityBuilder, createMongoAbility, ForcedSubject, MongoAbility } from "@casl/ability";
|
||||||
|
|
||||||
import { conditionsMatcher } from "@app/lib/casl";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { conditionsMatcher } from "@app/lib/casl";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
|
||||||
import { PermissionConditionOperators, PermissionConditionSchema } from "./permission-types";
|
import { PermissionConditionOperators, PermissionConditionSchema } from "./permission-types";
|
||||||
|
|
||||||
export enum ProjectPermissionActions {
|
export enum ProjectPermissionActions {
|
||||||
@@ -38,7 +41,25 @@ export enum ProjectPermissionSub {
|
|||||||
Kms = "kms"
|
Kms = "kms"
|
||||||
}
|
}
|
||||||
|
|
||||||
type SubjectFields = {
|
export type SecretSubjectFields = {
|
||||||
|
environment: string;
|
||||||
|
secretPath: string;
|
||||||
|
// secretName: string;
|
||||||
|
// secretTags: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CaslSecretsV2SubjectKnexMapper = (field: string) => {
|
||||||
|
switch (field) {
|
||||||
|
case "secretName":
|
||||||
|
return `${TableName.SecretV2}.key`;
|
||||||
|
case "secretTags":
|
||||||
|
return `${TableName.SecretTag}.slug`;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SecretFolderSubjectFields = {
|
||||||
environment: string;
|
environment: string;
|
||||||
secretPath: string;
|
secretPath: string;
|
||||||
};
|
};
|
||||||
@@ -46,11 +67,14 @@ type SubjectFields = {
|
|||||||
export type ProjectPermissionSet =
|
export type ProjectPermissionSet =
|
||||||
| [
|
| [
|
||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
ProjectPermissionSub.Secrets | (ForcedSubject<ProjectPermissionSub.Secrets> & SubjectFields)
|
ProjectPermissionSub.Secrets | (ForcedSubject<ProjectPermissionSub.Secrets> & SecretSubjectFields)
|
||||||
]
|
]
|
||||||
| [
|
| [
|
||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
ProjectPermissionSub.SecretFolders | (ForcedSubject<ProjectPermissionSub.SecretFolders> & SubjectFields)
|
(
|
||||||
|
| ProjectPermissionSub.SecretFolders
|
||||||
|
| (ForcedSubject<ProjectPermissionSub.SecretFolders> & SecretFolderSubjectFields)
|
||||||
|
)
|
||||||
]
|
]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.Role]
|
| [ProjectPermissionActions, ProjectPermissionSub.Role]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.Tags]
|
| [ProjectPermissionActions, ProjectPermissionSub.Tags]
|
||||||
@@ -595,3 +619,18 @@ export const isAtLeastAsPrivilegedWorkspace = (
|
|||||||
return set1.size >= set2.size;
|
return set1.size >= set2.size;
|
||||||
};
|
};
|
||||||
/* eslint-enable */
|
/* eslint-enable */
|
||||||
|
|
||||||
|
export const SecretV2SubjectFieldMapper = (arg: string) => {
|
||||||
|
switch (arg) {
|
||||||
|
case "environment":
|
||||||
|
return null;
|
||||||
|
case "secretPath":
|
||||||
|
return null;
|
||||||
|
case "secretName":
|
||||||
|
return `${TableName.SecretV2}.key`;
|
||||||
|
case "secretTags":
|
||||||
|
return `${TableName.SecretTag}.slug`;
|
||||||
|
default:
|
||||||
|
throw new BadRequestError({ message: `Invalid dynamic knex operator field: ${arg}` });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import { AnyAbility, ExtractSubjectType } from "@casl/ability";
|
||||||
|
import { AbilityQuery, rulesToQuery } from "@casl/ability/extra";
|
||||||
|
import { Tables } from "knex/types/tables";
|
||||||
|
|
||||||
|
import { BadRequestError, UnauthorizedError } from "../errors";
|
||||||
|
import { TKnexDynamicOperator } from "../knex/dynamic";
|
||||||
|
|
||||||
|
type TBuildKnexQueryFromCaslDTO<K extends AnyAbility> = {
|
||||||
|
ability: K;
|
||||||
|
subject: ExtractSubjectType<Parameters<K["rulesFor"]>[1]>;
|
||||||
|
action: Parameters<K["rulesFor"]>[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const buildKnexQueryFromCaslOperators = <K extends AnyAbility>({
|
||||||
|
ability,
|
||||||
|
subject,
|
||||||
|
action
|
||||||
|
}: TBuildKnexQueryFromCaslDTO<K>) => {
|
||||||
|
const query = rulesToQuery(ability, action, subject, (rule) => {
|
||||||
|
if (!rule.ast) throw new Error("Ast not defined");
|
||||||
|
return rule.ast;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (query === null) throw new UnauthorizedError({ message: `You don't have permission to do ${action} ${subject}` });
|
||||||
|
return query;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TFieldMapper<T extends keyof Tables> = {
|
||||||
|
[K in T]: `${K}.${Exclude<keyof Tables[K]["base"], symbol>}`;
|
||||||
|
}[T];
|
||||||
|
|
||||||
|
type TFormatCaslFieldsWithTableNames<T extends keyof Tables> = {
|
||||||
|
// handle if any missing operator else throw error let the app break because this is executing again the db
|
||||||
|
missingOperatorCallback?: (operator: string) => void;
|
||||||
|
fieldMapping: (arg: string) => TFieldMapper<T> | null;
|
||||||
|
dynamicQuery: TKnexDynamicOperator;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatCaslOperatorFieldsWithTableNames = <T extends keyof Tables>({
|
||||||
|
missingOperatorCallback = (arg) => {
|
||||||
|
throw new BadRequestError({ message: `Unknown permission operator: ${arg}` });
|
||||||
|
},
|
||||||
|
dynamicQuery: dynamicQueryAst,
|
||||||
|
fieldMapping
|
||||||
|
}: TFormatCaslFieldsWithTableNames<T>) => {
|
||||||
|
const stack: [TKnexDynamicOperator, TKnexDynamicOperator | null][] = [[dynamicQueryAst, null]];
|
||||||
|
|
||||||
|
while (stack.length) {
|
||||||
|
const [filterAst, parentAst] = stack.pop()!;
|
||||||
|
|
||||||
|
if (filterAst.operator === "and" || filterAst.operator === "or" || filterAst.operator === "not") {
|
||||||
|
filterAst.value.forEach((el) => {
|
||||||
|
stack.push([el, filterAst]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
filterAst.operator === "eq" ||
|
||||||
|
filterAst.operator === "ne" ||
|
||||||
|
filterAst.operator === "in" ||
|
||||||
|
filterAst.operator === "endsWith" ||
|
||||||
|
filterAst.operator === "startsWith"
|
||||||
|
) {
|
||||||
|
const attrPath = fieldMapping(filterAst.field);
|
||||||
|
if (attrPath) {
|
||||||
|
filterAst.field = attrPath;
|
||||||
|
} else if (parentAst && Array.isArray(parentAst.value)) {
|
||||||
|
parentAst.value = parentAst.value.filter((childAst) => childAst !== filterAst) as string[];
|
||||||
|
} else throw new Error("Unknown casl field");
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parentAst && Array.isArray(parentAst.value)) {
|
||||||
|
parentAst.value = parentAst.value.filter((childAst) => childAst !== filterAst) as string[];
|
||||||
|
} else {
|
||||||
|
missingOperatorCallback?.(filterAst.operator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dynamicQueryAst;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const convertCaslOperatorToKnexOperator = <T extends keyof Tables>(
|
||||||
|
caslKnexOperators: AbilityQuery,
|
||||||
|
fieldMapping: (arg: string) => TFieldMapper<T> | null
|
||||||
|
) => {
|
||||||
|
const value = [];
|
||||||
|
if (caslKnexOperators.$and) {
|
||||||
|
value.push({
|
||||||
|
operator: "not" as const,
|
||||||
|
value: caslKnexOperators.$and as TKnexDynamicOperator[]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (caslKnexOperators.$or) {
|
||||||
|
value.push({
|
||||||
|
operator: "or" as const,
|
||||||
|
value: caslKnexOperators.$or as TKnexDynamicOperator[]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatCaslOperatorFieldsWithTableNames({
|
||||||
|
dynamicQuery: {
|
||||||
|
operator: "and",
|
||||||
|
value
|
||||||
|
},
|
||||||
|
fieldMapping
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { UnauthorizedError } from "../errors";
|
||||||
|
|
||||||
|
type TKnexDynamicPrimitiveOperator = {
|
||||||
|
operator: "eq" | "ne" | "startsWith" | "endsWith";
|
||||||
|
value: string;
|
||||||
|
field: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TKnexDynamicInOperator = {
|
||||||
|
operator: "in";
|
||||||
|
value: string[] | number[];
|
||||||
|
field: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TKnexNonGroupOperator = TKnexDynamicInOperator | TKnexDynamicPrimitiveOperator;
|
||||||
|
|
||||||
|
type TKnexGroupOperator = {
|
||||||
|
operator: "and" | "or" | "not";
|
||||||
|
value: (TKnexNonGroupOperator | TKnexGroupOperator)[];
|
||||||
|
};
|
||||||
|
|
||||||
|
// used when you need to write a complex query with the orm
|
||||||
|
// use it when you need complex or and and condition - most of the time not needed
|
||||||
|
// majorly used with casl permission to filter data based on permission
|
||||||
|
export type TKnexDynamicOperator = TKnexGroupOperator | TKnexNonGroupOperator;
|
||||||
|
|
||||||
|
export const buildDynamicKnexQuery = (dynamicQuery: TKnexDynamicOperator, rootQueryBuild: Knex.QueryBuilder) => {
|
||||||
|
const stack = [{ filterAst: dynamicQuery, queryBuilder: rootQueryBuild }];
|
||||||
|
|
||||||
|
while (stack.length) {
|
||||||
|
const { filterAst, queryBuilder } = stack.pop()!;
|
||||||
|
switch (filterAst.operator) {
|
||||||
|
case "eq": {
|
||||||
|
void queryBuilder.where(filterAst.field, filterAst.value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "ne": {
|
||||||
|
void queryBuilder.where(filterAst.field, filterAst.value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "startsWith": {
|
||||||
|
void queryBuilder.where(filterAst.field, filterAst.value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "endsWith": {
|
||||||
|
void queryBuilder.where(filterAst.field, filterAst.value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "and": {
|
||||||
|
void queryBuilder.andWhere((subQueryBuilder) => {
|
||||||
|
filterAst.value.forEach((el) => {
|
||||||
|
stack.push({
|
||||||
|
queryBuilder: subQueryBuilder,
|
||||||
|
filterAst: el
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "or": {
|
||||||
|
void queryBuilder.orWhere((subQueryBuilder) => {
|
||||||
|
filterAst.value.forEach((el) => {
|
||||||
|
stack.push({
|
||||||
|
queryBuilder: subQueryBuilder,
|
||||||
|
filterAst: el
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "not": {
|
||||||
|
void queryBuilder.whereNot((subQueryBuilder) => {
|
||||||
|
filterAst.value.forEach((el) => {
|
||||||
|
stack.push({
|
||||||
|
queryBuilder: subQueryBuilder,
|
||||||
|
filterAst: el
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new UnauthorizedError({ message: `Invalid knex dynamic operator: ${filterAst.operator}` });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user