feat(infisical-pg: added services for project, project key and environment

This commit is contained in:
Akhil Mohan
2023-12-14 15:59:54 +05:30
parent 53bb3bc610
commit 76e5d61da5
33 changed files with 1469 additions and 28 deletions

View File

@@ -11,6 +11,7 @@ console.log(`
Component List
--------------
1. Service component
2. DAL component
`);
const componentType = parseInt(prompt("Select a component: "), 10);
@@ -62,4 +63,31 @@ export const ${serviceName} = ({ ${camelCase}Dal }: ${serviceTypeName}Dep) => {
`
);
writeFileSync(path.join(dir, `${componentName}-types.ts`), "");
} else if (componentType === 2) {
const componentName = prompt("Enter service name: ");
const componentPath = prompt("Path wrt service folder: ");
const pascalCase = componentName
.split("-")
.map((el) => `${el[0].toUpperCase()}${el.slice(1)}`)
.join("");
const camelCase = componentName
.split("-")
.map((el, index) => (index === 0 ? el : `${el[0].toUpperCase()}${el.slice(1)}`))
.join("");
const dalTypeName = `T${pascalCase}DalFactory`;
const dalName = `${camelCase}DalFactory`;
writeFileSync(
path.join(__dirname, "../src/services", componentPath, `${componentName}-dal.ts`),
`import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
export type ${dalTypeName} = ReturnType<typeof ${dalName}>;
export const ${dalName} = (db: TDbClient) => {
return { };
};
`
);
}

View File

@@ -5,6 +5,7 @@ import { TAuthLoginFactory } from "@app/services/auth/auth-login-service";
import { TAuthPasswordFactory } from "@app/services/auth/auth-password-service";
import { TAuthSignupFactory } from "@app/services/auth/auth-signup-service";
import { AuthMode } from "@app/services/auth/auth-signup-type";
import { ActorType } from "@app/services/auth/auth-type";
import { TOrgRoleServiceFactory } from "@app/services/org/org-role-service";
import { TOrgServiceFactory } from "@app/services/org/org-service";
import { TSuperAdminServiceFactory } from "@app/services/super-admin/super-admin-service";
@@ -25,10 +26,15 @@ declare module "fastify" {
// identity injection. depending on which kinda of token the information is filled in auth
auth: {
authMode: AuthMode.JWT | AuthMode.API_KEY_V2 | AuthMode.API_KEY;
actor: ActorType.USER;
userId: string;
tokenVersionId: string; // the session id of token used
user: TUsers;
};
permission: {
type: ActorType;
id: string;
};
// passport data
passportUser: {
isCompleted: string;

View File

@@ -27,6 +27,10 @@ import {
TOrgRolesUpdate,
TProjectEnvironments,
TProjectEnvironmentsInsert,
TProjectEnvironmentsUpdate,
TProjectKeys,
TProjectKeysInsert,
TProjectKeysUpdate,
TProjectMemberships,
TProjectMembershipsInsert,
TProjectMembershipsUpdate,
@@ -109,12 +113,17 @@ declare module "knex/types/tables" {
[TableName.Environment]: Knex.CompositeTableType<
TProjectEnvironments,
TProjectEnvironmentsInsert,
TPRo
TProjectEnvironmentsUpdate
>;
[TableName.ProjectRoles]: Knex.CompositeTableType<
TProjectRoles,
TProjectRolesInsert,
TProjectRolesUpdate
>;
[TableName.ProjectKeys]: Knex.CompositeTableType<
TProjectKeys,
TProjectKeysInsert,
TProjectKeysUpdate
>;
}
}

View File

@@ -1,4 +1,5 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
@@ -13,6 +14,7 @@ export async function up(knex: Knex): Promise<void> {
t.timestamps(true, true, true);
});
}
// environments
await createOnUpdateTrigger(knex, TableName.Project);
if (!(await knex.schema.hasTable(TableName.Environment))) {
await knex.schema.createTable(TableName.Environment, (t) => {
@@ -24,10 +26,28 @@ export async function up(knex: Knex): Promise<void> {
t.timestamps(true, true, true);
});
}
// project key
if (!(await knex.schema.hasTable(TableName.ProjectKeys))) {
await knex.schema.createTable(TableName.ProjectKeys, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.text("encryptedKey").notNullable();
t.text("nonce").notNullable();
t.uuid("receiverId").notNullable();
t.foreign("receiverId").references("id").inTable(TableName.Users).onDelete("CASCADE");
t.uuid("senderId").notNullable();
// if sender is deleted just don't do anything to this record
t.foreign("senderId").references("id").inTable(TableName.Users).onDelete("NO ACTION");
t.uuid("projectId").notNullable();
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
t.timestamps(true, true, true);
});
}
await createOnUpdateTrigger(knex, TableName.ProjectKeys);
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.Environment);
await knex.schema.dropTableIfExists(TableName.Project);
await dropOnUpdateTrigger(knex, TableName.Project);
await dropOnUpdateTrigger(knex, TableName.ProjectKeys);
}

View File

@@ -1,4 +1,5 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";

View File

@@ -8,6 +8,7 @@ export * from "./org-memberships";
export * from "./org-roles";
export * from "./organizations";
export * from "./project-environments";
export * from "./project-keys";
export * from "./project-memberships";
export * from "./project-roles";
export * from "./projects";

View File

@@ -16,7 +16,8 @@ export enum TableName {
Project = "projects",
Environment = "project_environments",
ProjectMembership = "project_memberships",
ProjectRoles = "project_roles"
ProjectRoles = "project_roles",
ProjectKeys = "project_keys"
}
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt";

View File

@@ -0,0 +1,23 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const ProjectKeysSchema = z.object({
id: z.string().uuid(),
encryptedKey: z.string(),
nonce: z.string(),
receiverId: z.string().uuid(),
senderId: z.string().uuid(),
projectId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date(),
});
export type TProjectKeys = z.infer<typeof ProjectKeysSchema>;
export type TProjectKeysInsert = Omit<TProjectKeys, TImmutableDBKeys>;
export type TProjectKeysUpdate = Partial<Omit<TProjectKeys, TImmutableDBKeys>>;

View File

@@ -1,5 +1,5 @@
import { TDbClient } from "@app/db";
import { TableName, TOrgMemberships } from "@app/db/schemas";
import { TableName, TOrgMemberships, TProjectMemberships } from "@app/db/schemas";
export type TPermissionDalFactory = ReturnType<typeof permissionDalFactory>;
@@ -18,7 +18,26 @@ export const permissionDalFactory = (db: TDbClient) => {
return membership;
};
const getProjectPermission = async (
userId: string,
projectId: string
): Promise<(TProjectMemberships & { permissions: string }) | undefined> => {
const membership = await db(TableName.ProjectMembership)
.leftJoin(
TableName.ProjectRoles,
`${TableName.ProjectMembership}.roleId`,
`${TableName.ProjectRoles}.id`
)
.where("userId", userId)
.where(`${TableName.ProjectMembership}.projectId`, projectId)
.select(`${TableName.ProjectMembership}.*`, "permissions")
.first();
return membership;
};
return {
getOrgPermission
getOrgPermission,
getProjectPermission
};
};

View File

@@ -1,4 +1,6 @@
import { ProjectMembershipRole } from "@app/db/schemas";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import { ActorType } from "@app/services/auth/auth-type";
import {
conditionsMatcher,
@@ -7,6 +9,11 @@ import {
OrgPermissionSet
} from "./org-permission";
import { TPermissionDalFactory } from "./permission-dal";
import {
projectAdminPermissions,
projectMemberPermissions,
projectNoAccessPermissions,
projectViewerPermission} from "./project-permission";
import { createMongoAbility, MongoAbility, RawRuleOf } from "@casl/ability";
import { unpackRules } from "@casl/ability/extra";
@@ -44,7 +51,64 @@ export const permissionServiceFactory = ({ permissionDal }: TPermissionServiceFa
throw new BadRequestError({ name: "Role missing", message: "User role not found" });
};
// user permission for a project in an organization
const getUserProjectPermission = async (userId: string, projectId: string) => {
const membership = await permissionDal.getProjectPermission(userId, projectId);
if (!membership) throw new UnauthorizedError({ name: "User not in org" });
if (membership.role === "custom" && !membership.permissions) {
throw new BadRequestError({ name: "Custom permission not found" });
}
if (membership.role === ProjectMembershipRole.Admin)
return { permission: projectAdminPermissions, membership };
if (membership.role === ProjectMembershipRole.Member)
return { permission: projectMemberPermissions, membership };
if (membership.role === ProjectMembershipRole.Viewer)
return { permission: projectViewerPermission, membership };
if (membership.role === ProjectMembershipRole.NoAccess)
return { permission: projectNoAccessPermissions, membership };
if (membership.role === ProjectMembershipRole.Custom) {
const permission = createMongoAbility<OrgPermissionSet>(
// akhilmhdh: putting any due to ts incompatiable matching with string and the other
unpackRules<RawRuleOf<MongoAbility<OrgPermissionSet>>>(membership.permissions as any),
{
conditionsMatcher
}
);
return { permission, membership };
}
throw new BadRequestError({ name: "Role missing", message: "User role not found" });
};
const getProjectPermission = async (type: ActorType, id: string, projectId: string) => {
switch (type) {
case ActorType.USER:
return getUserProjectPermission(id, projectId);
default:
throw new UnauthorizedError({
message: "Permission not defined",
name: "Get org permission"
});
}
};
const getOrgPermission = async (type: ActorType, id: string, orgId: string) => {
switch (type) {
case ActorType.USER:
return getUserOrgPermission(id, orgId);
default:
throw new UnauthorizedError({
message: "Permission not defined",
name: "Get org permission"
});
}
};
return {
getUserOrgPermission
getUserOrgPermission,
getOrgPermission,
getUserProjectPermission,
getProjectPermission
};
};

View File

@@ -0,0 +1,286 @@
import picomatch from "picomatch";
import {
AbilityBuilder,
buildMongoQueryMatcher,
createMongoAbility,
ForcedSubject,
MongoAbility} from "@casl/ability";
import { FieldCondition, FieldInstruction, JsInterpreter } from "@ucast/mongo2js";
const $glob: FieldInstruction<string> = {
type: "field",
validate(instruction, value) {
if (typeof value !== "string") {
throw new Error(`"${instruction.name}" expects value to be a string`);
}
}
};
const glob: JsInterpreter<FieldCondition<string>> = (node, object, context) => {
const secretPath = context.get(object, node.field);
const permissionSecretGlobPath = node.value;
return picomatch.isMatch(secretPath, permissionSecretGlobPath, { strictSlashes: false });
};
export const conditionsMatcher = buildMongoQueryMatcher({ $glob }, { glob });
export enum ProjectPermissionActions {
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete"
}
export enum ProjectPermissionSub {
Role = "role",
Member = "member",
Settings = "settings",
Integrations = "integrations",
Webhooks = "webhooks",
ServiceTokens = "service-tokens",
Environments = "environments",
Tags = "tags",
AuditLogs = "audit-logs",
IpAllowList = "ip-allowlist",
Project = "workspace",
Secrets = "secrets",
SecretRollback = "secret-rollback",
SecretApproval = "secret-approval",
SecretRotation = "secret-rotation",
Identity = "identity"
}
type SubjectFields = {
environment: string;
secretPath: string;
};
export type ProjectPermissionSet =
| [
ProjectPermissionActions,
ProjectPermissionSub.Secrets | (ForcedSubject<ProjectPermissionSub.Secrets> & SubjectFields)
]
| [ProjectPermissionActions, ProjectPermissionSub.Role]
| [ProjectPermissionActions, ProjectPermissionSub.Tags]
| [ProjectPermissionActions, ProjectPermissionSub.Member]
| [ProjectPermissionActions, ProjectPermissionSub.Integrations]
| [ProjectPermissionActions, ProjectPermissionSub.Webhooks]
| [ProjectPermissionActions, ProjectPermissionSub.AuditLogs]
| [ProjectPermissionActions, ProjectPermissionSub.Environments]
| [ProjectPermissionActions, ProjectPermissionSub.IpAllowList]
| [ProjectPermissionActions, ProjectPermissionSub.Settings]
| [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens]
| [ProjectPermissionActions, ProjectPermissionSub.SecretApproval]
| [ProjectPermissionActions, ProjectPermissionSub.SecretRotation]
| [ProjectPermissionActions, ProjectPermissionSub.Identity]
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Project]
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Project]
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback];
const buildAdminPermission = () => {
const { can, build } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Secrets);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Secrets);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Secrets);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretApproval);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.SecretApproval);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation);
can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretRotation);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretRotation);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.SecretRotation);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Member);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Member);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Role);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Role);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Role);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Webhooks);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Webhooks);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Webhooks);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Identity);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Identity);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Identity);
can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens);
can(ProjectPermissionActions.Create, ProjectPermissionSub.ServiceTokens);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.ServiceTokens);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.ServiceTokens);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Settings);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Settings);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Settings);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Environments);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Environments);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Environments);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Environments);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Tags);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Tags);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Tags);
can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
can(ProjectPermissionActions.Create, ProjectPermissionSub.AuditLogs);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.AuditLogs);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.AuditLogs);
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
can(ProjectPermissionActions.Create, ProjectPermissionSub.IpAllowList);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.IpAllowList);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.IpAllowList);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Project);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Project);
return build({ conditionsMatcher });
};
export const projectAdminPermissions = buildAdminPermission();
const buildMemberPermission = () => {
const { can, build } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Secrets);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Secrets);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Secrets);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Member);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Webhooks);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Webhooks);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Webhooks);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Identity);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Identity);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Identity);
can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens);
can(ProjectPermissionActions.Create, ProjectPermissionSub.ServiceTokens);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.ServiceTokens);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.ServiceTokens);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Settings);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Settings);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Settings);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Environments);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Environments);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Environments);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Environments);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Tags);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Tags);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Tags);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
return build({ conditionsMatcher });
};
export const projectMemberPermissions = buildMemberPermission();
const buildViewerPermission = () => {
const { can, build } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Identity);
can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Settings);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Environments);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
return build({ conditionsMatcher });
};
export const projectViewerPermission = buildViewerPermission();
const buildNoAccessProjectPermission = () => {
const { build } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
return build({ conditionsMatcher });
};
export const projectNoAccessPermissions = buildNoAccessProjectPermission();
/**
* Extracts and formats permissions from a CASL Ability object or a raw permission set.
* @param ability
* @returns
*/
const extractPermissions = (ability: any) =>
ability.A.map((permission: any) => `${permission.action}_${permission.subject}`);
/**
* Compares two sets of permissions to determine if the first set is at least as privileged as the second set.
* The function checks if all permissions in the second set are contained within the first set and if the first set has equal or more permissions.
*
*/
export const isAtLeastAsPrivilegedWorkspace = (
permissions1: MongoAbility<ProjectPermissionSet> | ProjectPermissionSet,
permissions2: MongoAbility<ProjectPermissionSet> | ProjectPermissionSet
) => {
const set1 = new Set(extractPermissions(permissions1));
const set2 = new Set(extractPermissions(permissions2));
// eslint-disable-next-line
for (const perm of set2) {
if (!set1.has(perm)) {
return false;
}
}
return set1.size >= set2.size;
};

View File

@@ -5,6 +5,8 @@ import { TableName } from "@app/db/schemas";
import { DatabaseError } from "../errors";
export * from "./join";
export const withTransaction = <K extends object>(db: Knex, dal: K) => ({
transaction: async <T>(cb: (tx: Knex) => Promise<T>) =>
db.transaction(async (trx) => {
@@ -53,19 +55,29 @@ export const ormify = <DbOps extends object, Tname extends TableName>(
},
create: async (data: Tables[Tname]["insert"], tx?: Knex) => {
try {
const [user] = await (tx || db)(tableName).insert(data).returning("*");
return user;
const [res] = await (tx || db)(tableName).insert(data).returning("*");
return res;
} catch (error) {
throw new DatabaseError({ error, name: "Create" });
}
},
insertMany: async (data: readonly Tables[Tname]["insert"][], tx?: Knex) => {
try {
const res = await (tx || db)(tableName)
.insert(data as any)
.returning("*");
return res;
} catch (error) {
throw new DatabaseError({ error, name: "Create" });
}
},
updateById: async (id: string, data: Tables[Tname]["update"], tx?: Knex) => {
try {
const [user] = await (tx || db)(tableName)
const [res] = await (tx || db)(tableName)
.where({ id } as any)
.update(data as any)
.returning("*");
return user;
return res;
} catch (error) {
throw new DatabaseError({ error, name: "Update by id" });
}
@@ -76,30 +88,30 @@ export const ormify = <DbOps extends object, Tname extends TableName>(
tx?: Knex
) => {
try {
const user = await (tx || db)(tableName)
const res = await (tx || db)(tableName)
.where(filter)
.update(data as any)
.returning("*");
return user;
return res;
} catch (error) {
throw new DatabaseError({ error, name: "Update" });
}
},
deleteById: async (id: string, tx?: Knex) => {
try {
const [user] = await (tx || db)(tableName)
const [res] = await (tx || db)(tableName)
.where({ id } as any)
.delete()
.returning("*");
return user;
return res;
} catch (error) {
throw new DatabaseError({ error, name: "Delete by id" });
}
},
delete: async (filter: Partial<Tables[Tname]["base"]>, tx?: Knex) => {
try {
const user = await (tx || db)(tableName).where(filter).delete().returning("*");
return user;
const res = await (tx || db)(tableName).where(filter).delete().returning("*");
return res;
} catch (error) {
throw new DatabaseError({ error, name: "Delete" });
}

View File

@@ -0,0 +1,28 @@
export const mergeOneToManyRelation = <
T extends Record<string, any>,
Pk extends keyof T,
P extends Record<string, any>,
C extends Record<string, any>,
Ck extends string = "child"
>(
data: T[],
key: Pk,
parentMapper: (arg: T) => P,
childMapper: (arg: T) => C,
childKey: Ck
) => {
const recordFirstVisitIndex: Record<string, number> = {};
const groupedRecord: (P & Record<Ck, C[]>)[] = [];
for (let i = 0; i < data.length; i += 1) {
const pk = data[i][key];
const row = data[i];
if (typeof recordFirstVisitIndex[pk] === "undefined") {
const parent = parentMapper(row) as any;
parent[childKey] = [];
groupedRecord.push(parent);
recordFirstVisitIndex[pk] = i;
}
groupedRecord[recordFirstVisitIndex[pk]][childKey].push(childMapper(row));
}
return groupedRecord;
};

View File

@@ -1,3 +1,11 @@
import { ActorType } from "@app/services/auth/auth-type";
export type TProjectPermission = {
actor: ActorType;
actorId: string;
projectId: string;
};
export type RequiredKeys<T> = {
[K in keyof T]-?: undefined extends T[K] ? never : K;
}[keyof T];

View File

@@ -4,29 +4,46 @@ import jwt, { JwtPayload } from "jsonwebtoken";
import { getConfig } from "@app/lib/config/env";
import { UnauthorizedError } from "@app/lib/errors";
import { AuthMode, AuthModeJwtTokenPayload, AuthTokenType } from "@app/services/auth/auth-type";
import {
ActorType,
AuthMode,
AuthModeJwtTokenPayload,
AuthTokenType
} from "@app/services/auth/auth-type";
const extractAuth = async (req: FastifyRequest, jwtSecret: string) => {
const apiKey = req.headers?.["x-api-key"];
if (apiKey) {
return { authMode: AuthMode.API_KEY, token: apiKey };
return { authMode: AuthMode.API_KEY, token: apiKey, actor: ActorType.USER } as const;
}
const authHeader = req.headers?.authorization;
if (!authHeader) return { authMode: null, token: null };
const authTokenValue = authHeader.slice(7); // slice of after Bearer
if (authTokenValue.startsWith("st.")) {
return { authMode: AuthMode.SERVICE_TOKEN, token: authTokenValue } as const;
return {
authMode: AuthMode.SERVICE_TOKEN,
token: authTokenValue,
actor: ActorType.USER
} as const;
}
const decodedToken = jwt.verify(authTokenValue, jwtSecret) as JwtPayload;
switch (decodedToken.authTokenType) {
case AuthTokenType.ACCESS_TOKEN:
return { authMode: AuthMode.JWT, token: decodedToken as AuthModeJwtTokenPayload } as const;
return {
authMode: AuthMode.JWT,
token: decodedToken as AuthModeJwtTokenPayload,
actor: ActorType.USER
} as const;
case AuthTokenType.API_KEY:
return { authMode: AuthMode.API_KEY_V2, token: decodedToken } as const;
return { authMode: AuthMode.API_KEY_V2, token: decodedToken, actor: ActorType.USER } as const;
case AuthMode.SERVICE_ACCESS_TOKEN:
return { authMode: AuthMode.SERVICE_ACCESS_TOKEN, token: decodedToken } as const;
return {
authMode: AuthMode.SERVICE_ACCESS_TOKEN,
token: decodedToken,
actor: ActorType.USER
} as const;
default:
return { authMode: null, token: null } as const;
}
@@ -52,7 +69,7 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => {
server.decorateRequest("auth", null);
server.addHook("onRequest", async (req) => {
const appCfg = getConfig();
const { authMode, token } = await extractAuth(req, appCfg.JWT_AUTH_SECRET);
const { authMode, token, actor } = await extractAuth(req, appCfg.JWT_AUTH_SECRET);
if (!authMode) return;
// TODO(akhilmhdh-pg): fill in rest of auth mode logic
switch (authMode) {
@@ -61,7 +78,7 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => {
server,
token as AuthModeJwtTokenPayload
);
req.auth = { authMode: AuthMode.JWT, user, userId: user.id, tokenVersionId };
req.auth = { authMode: AuthMode.JWT, user, userId: user.id, tokenVersionId, actor };
break;
}
case AuthMode.SERVICE_TOKEN:

View File

@@ -0,0 +1,13 @@
import fp from "fastify-plugin";
import { ActorType } from "@app/services/auth/auth-type";
// inject permission type needed based on auth extracted
export const injectPermission = fp(async (server) => {
server.decorateRequest("permission", null);
server.addHook("onRequest", async (req) => {
if (req.auth.actor === ActorType.USER) {
req.permission = { type: ActorType.USER, id: req.auth.userId };
}
});
});

View File

@@ -27,6 +27,12 @@ export enum AuthMode {
API_KEY_V2 = "apiKeyV2"
}
export enum ActorType { // would extend to AWS, Azure, ...
USER = "user", // userIdentity
SERVICE = "service",
IDENTITY = "identity"
}
export type AuthModeJwtTokenPayload = {
authTokenType: AuthTokenType.ACCESS_TOKEN;
userId: string;

View File

@@ -145,14 +145,20 @@ export const orgServiceFactory = ({
const customRole = await orgRoleDal.findOne({ slug: role, orgId });
if (!customRole)
throw new BadRequestError({ name: "Update membership", message: "Role not found" });
const membership = await orgDal.updateMembershipById(membershipId, {
role: OrgMembershipRole.Custom,
roleId: customRole.id
});
const [membership] = await orgDal.updateMembership(
{ id: membershipId, orgId },
{
role: OrgMembershipRole.Custom,
roleId: customRole.id
}
);
return membership;
}
const membership = await orgDal.updateMembershipById(membershipId, { role, roleId: null });
const [membership] = await orgDal.updateMembership(
{ id: membershipId, orgId },
{ role, roleId: null }
);
return membership;
};
/*

View File

@@ -0,0 +1,10 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TProjectEnvDalFactory = ReturnType<typeof projectEnvDalFactory>;
export const projectEnvDalFactory = (db: TDbClient) => {
const projectEnvOrm = ormify(db, TableName.Environment);
return projectEnvOrm;
};

View File

@@ -0,0 +1,87 @@
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import {
ProjectPermissionActions,
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { BadRequestError } from "@app/lib/errors";
import { TProjectEnvDalFactory } from "./project-env-dal";
import { TCreateEnvDTO, TDeleteEnvDTO, TUpdateEnvDTO } from "./project-env-types";
import { ForbiddenError } from "@casl/ability";
type TProjectEnvServiceFactoryDep = {
projectEnvDal: TProjectEnvDalFactory;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
};
export type TProjectEnvServiceFactory = ReturnType<typeof projectEnvServiceFactory>;
export const projectEnvServiceFactory = ({
projectEnvDal,
permissionService
}: TProjectEnvServiceFactoryDep) => {
const createEnvironment = async ({ projectId, actorId, actor, name, slug }: TCreateEnvDTO) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.Environments
);
// TODO(akhilmhdh-pg): add licence service here
const existingEnv = await projectEnvDal.findOne({ slug });
if (existingEnv)
throw new BadRequestError({
message: "Environment with slug already exist",
name: "Create envv"
});
const env = await projectEnvDal.create({ slug, name, projectId });
return env;
};
const updateEnvironment = async ({
projectId,
slug,
actor,
actorId,
name,
id
}: TUpdateEnvDTO) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Environments
);
if (slug) {
const existingEnv = await projectEnvDal.findOne({ slug });
if (existingEnv && existingEnv.id !== id) {
throw new BadRequestError({
message: "Environment with slug already exist",
name: "Create envv"
});
}
}
const [env] = await projectEnvDal.update({ id, projectId }, { name, slug });
return env;
};
const deleteEnvironment = async ({ projectId, actor, actorId, id }: TDeleteEnvDTO) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.Environments
);
const [env] = await projectEnvDal.delete({ id, projectId });
return env;
};
return {
createEnvironment,
updateEnvironment,
deleteEnvironment
};
};

View File

@@ -0,0 +1,16 @@
import { TProjectPermission } from "@app/lib/types";
export type TCreateEnvDTO = {
name: string;
slug: string;
} & TProjectPermission;
export type TUpdateEnvDTO = {
id: string;
name?: string;
slug?: string;
} & TProjectPermission;
export type TDeleteEnvDTO = {
id: string;
} & TProjectPermission;

View File

@@ -0,0 +1,42 @@
import { TDbClient } from "@app/db";
import { TableName,TProjectKeys } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex";
export type TProjectKeyDalFactory = ReturnType<typeof projectKeyDalFactory>;
export const projectKeyDalFactory = (db: TDbClient) => {
const projectKeyOrm = ormify(db, TableName.ProjectKeys);
const findLatestProjectKey = async (
userId: string,
projectId: string
): Promise<TProjectKeys & { sender: { publicKey: string } }> => {
try {
const projectKey = await db(TableName.ProjectKeys)
.where({ projectId, receiverId: userId })
.join(TableName.Users, `${TableName.ProjectKeys}.senderId`, `${TableName.Users}.id`)
.join(
TableName.UserEncryptionKey,
`${TableName.UserEncryptionKey}.userId`,
`${TableName.Users}.id`
)
.orderBy("createdAt", "desc", "last")
.select(`${TableName.ProjectKeys}.*`, `${TableName.UserEncryptionKey}.publicKey`)
.first();
if (projectKey) {
projectKey.sender = {
publicKey: projectKey.publicKey
};
}
return projectKey;
} catch (error) {
throw new DatabaseError({ error, name: "Find latest project key" });
}
};
return {
...projectKeyOrm,
findLatestProjectKey
};
};

View File

@@ -0,0 +1,64 @@
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import {
ProjectPermissionActions,
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { BadRequestError } from "@app/lib/errors";
import { TProjectMembershipDalFactory } from "../project-membership/project-membership-dal";
import { TProjectKeyDalFactory } from "./project-key-dal";
import { TGetLatestProjectKeyDTO, TUploadProjectKeyDTO } from "./project-key-types";
import { ForbiddenError } from "@casl/ability";
type TProjectKeyServiceFactoryDep = {
permissionService: TPermissionServiceFactory;
projectKeyDal: TProjectKeyDalFactory;
projectMembershipDal: TProjectMembershipDalFactory;
};
export type TProjectKeyServiceFactory = ReturnType<typeof projectKeyServiceFactory>;
export const projectKeyServiceFactory = ({
projectKeyDal,
projectMembershipDal,
permissionService
}: TProjectKeyServiceFactoryDep) => {
const uploadProjectKeys = async ({
receiverId,
actor,
actorId,
projectId,
nonce,
encryptedKey
}: TUploadProjectKeyDTO) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Member
);
const receiverMembership = await projectMembershipDal.findOne({
userId: receiverId,
projectId
});
if (!receiverMembership)
throw new BadRequestError({
message: "Failed to validate receiver membership",
name: "Upload project keys"
});
await projectKeyDal.create({ projectId, receiverId, encryptedKey, nonce, senderId: actorId });
};
const getLatestProjectKey = async ({ actorId, projectId, actor }: TGetLatestProjectKeyDTO) => {
await permissionService.getProjectPermission(actor, actorId, projectId);
const latestKey = await projectKeyDal.findLatestProjectKey(actorId, projectId);
return latestKey;
};
return {
uploadProjectKeys,
getLatestProjectKey
};
};

View File

@@ -0,0 +1,9 @@
import { TProjectPermission } from "@app/lib/types";
export type TGetLatestProjectKeyDTO = TProjectPermission;
export type TUploadProjectKeyDTO = {
encryptedKey: string;
nonce: string;
receiverId: string;
} & TProjectPermission;

View File

@@ -0,0 +1,45 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex";
export type TProjectMembershipDalFactory = ReturnType<typeof projectMembershipDalFactory>;
export const projectMembershipDalFactory = (db: TDbClient) => {
const projectMemberOrm = ormify(db, TableName.ProjectMembership);
// special query
const findAllProjectMembers = async (projectId: string) => {
try {
const members = await db(TableName.ProjectMembership)
.where({ projectId })
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
.join(
TableName.UserEncryptionKey,
`${TableName.UserEncryptionKey}.userId`,
`${TableName.Users}.id`
)
.select(
db.ref("id").withSchema(TableName.ProjectMembership),
db.ref("inviteEmail").withSchema(TableName.ProjectMembership),
db.ref("orgId").withSchema(TableName.ProjectMembership),
db.ref("role").withSchema(TableName.ProjectMembership),
db.ref("roleId").withSchema(TableName.ProjectMembership),
db.ref("status").withSchema(TableName.ProjectMembership),
db.ref("email").withSchema(TableName.Users),
db.ref("firstName").withSchema(TableName.Users),
db.ref("lastName").withSchema(TableName.Users),
db.ref("id").withSchema(TableName.Users).as("userId"),
db.ref("publicKey").withSchema(TableName.UserEncryptionKey)
);
return members.map(({ email, firstName, lastName, userId, publicKey, ...data }) => ({
...data,
user: { email, firstName, lastName, id: userId, publicKey }
}));
} catch (error) {
throw new DatabaseError({ error, name: "Find all project members" });
}
};
return { ...projectMemberOrm, findAllProjectMembers };
};

View File

@@ -0,0 +1,190 @@
import { OrgMembershipStatus, ProjectMembershipRole } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import {
ProjectPermissionActions,
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { TOrgDalFactory } from "../org/org-dal";
import { TProjectDalFactory } from "../project/project-dal";
import { TProjectKeyDalFactory } from "../project-key/project-key-dal";
import { TProjectRoleDalFactory } from "../project-role/project-role-dal";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TUserDalFactory } from "../user/user-dal";
import { TProjectMembershipDalFactory } from "./project-membership-dal";
import {
TDeleteProjectMembershipDTO,
TGetProjectMembershipDTO,
TInviteUserToProjectDTO,
TUpdateProjectMembershipDTO
} from "./project-membership-types";
import { ForbiddenError } from "@casl/ability";
type TProjectMembershipServiceFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
smtpService: TSmtpService;
projectMembershipDal: TProjectMembershipDalFactory;
userDal: Pick<TUserDalFactory, "findById" | "findOne">;
projectRoleDal: Pick<TProjectRoleDalFactory, "findOne">;
orgDal: Pick<TOrgDalFactory, "findMembership">;
projectDal: Pick<TProjectDalFactory, "findById">;
projectKeyDal: Pick<TProjectKeyDalFactory, "findLatestProjectKey" | "delete">;
};
export type TProjectMembershipServiceFactory = ReturnType<typeof projectMembershipServiceFactory>;
export const projectMembershipServiceFactory = ({
permissionService,
projectMembershipDal,
smtpService,
projectRoleDal,
orgDal,
userDal,
projectDal,
projectKeyDal
}: TProjectMembershipServiceFactoryDep) => {
const getProjectMemberships = async ({ actorId, actor, projectId }: TGetProjectMembershipDTO) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Member
);
return projectMembershipDal.findAllProjectMembers(projectId);
};
const inviteUserToProject = async ({
actorId,
actor,
projectId,
email
}: TInviteUserToProjectDTO) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.Member
);
const invitee = await userDal.findOne({ email });
if (!invitee || !invitee.isAccepted)
throw new BadRequestError({
message: "Faield to validate invitee",
name: "Invite user to project"
});
const inviteeMembership = await projectMembershipDal.findOne({
userId: invitee.id,
projectId
});
if (inviteeMembership)
throw new BadRequestError({
message: "Existing member of project",
name: "Invite user to project"
});
const project = await projectDal.findById(projectId);
const inviteeMembershipOrg = await orgDal.findMembership({
userId: invitee.id,
orgId: project.orgId,
status: OrgMembershipStatus.Accepted
});
if (!inviteeMembershipOrg)
throw new BadRequestError({
message: "Failed to validate invitee org membership",
name: "Invite user to project"
});
const latestKey = await projectKeyDal.findLatestProjectKey(actorId, projectId);
await projectMembershipDal.create({
userId: invitee.id,
projectId,
role: ProjectMembershipRole.Member
});
const sender = await userDal.findById(actorId);
const appCfg = getConfig();
await smtpService.sendMail({
template: SmtpTemplates.WorkspaceInvite,
subjectLine: "Infisical workspace invitation",
recipients: [invitee.email],
substitutions: {
inviterFirstName: sender.firstName,
inviterEmail: sender.email,
workspaceName: project.name,
callback_url: `${appCfg.SITE_URL}/login`
}
});
// TODO(akhilmhdh-pg): Audit log
return { invitee, latestKey };
};
const updateProjectMembership = async ({
actorId,
actor,
projectId,
membershipId,
role
}: TUpdateProjectMembershipDTO) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Member
);
const isCustomRole = !Object.values(ProjectMembershipRole).includes(
role as ProjectMembershipRole
);
if (isCustomRole) {
const customRole = await projectRoleDal.findOne({ slug: role, projectId });
if (!customRole)
throw new BadRequestError({ name: "Update project membership", message: "Role not found" });
const [membership] = await projectMembershipDal.update(
{ id: membershipId, projectId },
{
role: ProjectMembershipRole.Custom,
roleId: customRole.id
}
);
return membership;
}
const [membership] = await projectMembershipDal.update(
{ id: membershipId, projectId },
{ role, roleId: null }
);
return membership;
};
const deleteProjectMembership = async ({
actorId,
actor,
projectId,
membershipId
}: TDeleteProjectMembershipDTO) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.Member
);
const membership = await projectMembershipDal.transaction(async (tx) => {
const [deletedMembership] = await projectMembershipDal.delete(
{ projectId, id: membershipId },
tx
);
await projectKeyDal.delete({ receiverId: deletedMembership.userId, projectId }, tx);
});
return membership;
};
return {
getProjectMemberships,
inviteUserToProject,
updateProjectMembership,
deleteProjectMembership
};
};

View File

@@ -0,0 +1,16 @@
import { TProjectPermission } from "@app/lib/types";
export type TGetProjectMembershipDTO = TProjectPermission;
export type TInviteUserToProjectDTO = {
email: string;
} & TProjectPermission;
export type TUpdateProjectMembershipDTO = {
membershipId: string;
role: string;
} & TProjectPermission;
export type TDeleteProjectMembershipDTO = {
membershipId: string;
} & TProjectPermission;

View File

@@ -0,0 +1,7 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TProjectRoleDalFactory = ReturnType<typeof projectRoleDalFactory>;
export const projectRoleDalFactory = (db: TDbClient) => ormify(db, TableName.ProjectRoles);

View File

@@ -0,0 +1,158 @@
import { ProjectMembershipRole, TOrgRolesUpdate, TProjectRolesInsert } from "@app/db/schemas";
import {
OrgPermissionActions,
OrgPermissionSubjects
} from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import {
projectAdminPermissions,
projectMemberPermissions,
projectNoAccessPermissions,
projectViewerPermission
} from "@app/ee/services/permission/project-permission";
import { BadRequestError } from "@app/lib/errors";
import { ActorType } from "../auth/auth-type";
import { TProjectRoleDalFactory } from "./project-role-dal";
import { ForbiddenError } from "@casl/ability";
import { packRules } from "@casl/ability/extra";
type TProjectRoleServiceFactoryDep = {
projectRoleDal: TProjectRoleDalFactory;
permissionService: Pick<
TPermissionServiceFactory,
"getProjectPermission" | "getUserOrgPermission"
>;
};
export type TProjectRoleServiceFactory = ReturnType<typeof projectRoleServiceFactory>;
export const projectRoleServiceFactory = ({
projectRoleDal,
permissionService
}: TProjectRoleServiceFactoryDep) => {
const createRole = async (
actor: ActorType,
actorId: string,
projectId: string,
data: Omit<TProjectRolesInsert, "projectId">
) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Create,
OrgPermissionSubjects.Role
);
const existingRole = await projectRoleDal.findOne({ slug: data.slug, projectId });
if (existingRole) throw new BadRequestError({ name: "Create Role", message: "Duplicate role" });
const role = await projectRoleDal.create({
...data,
projectId,
permissions: JSON.stringify(data.permissions)
});
return role;
};
const updateRole = async (
actor: ActorType,
actorId: string,
projectId: string,
roleId: string,
data: Omit<TOrgRolesUpdate, "orgId">
) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Edit,
OrgPermissionSubjects.Role
);
if (data?.slug) {
const existingRole = await projectRoleDal.findOne({ slug: data.slug, projectId });
if (existingRole && existingRole.id !== roleId)
throw new BadRequestError({ name: "Update Role", message: "Duplicate role" });
}
const [updatedRole] = await projectRoleDal.update({ id: roleId, projectId }, { ...data });
if (!updateRole) throw new BadRequestError({ message: "Role not found", name: "Update role" });
return updatedRole;
};
const deleteRole = async (
actor: ActorType,
actorId: string,
projectId: string,
roleId: string
) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Delete,
OrgPermissionSubjects.Role
);
const [deletedRole] = await projectRoleDal.delete({ id: roleId, projectId });
if (!deleteRole) throw new BadRequestError({ message: "Role not found", name: "Update role" });
return deletedRole;
};
const listRoles = async (actor: ActorType, actorId: string, projectId: string) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Role
);
const customRoles = await projectRoleDal.find({ projectId });
const roles = [
{
id: "b11b49a9-09a9-4443-916a-4246f9ff2c69", // dummy userid
projectId,
name: "Admin",
slug: ProjectMembershipRole.Admin,
description: "Complete administration access over the organization",
permissions: packRules(projectAdminPermissions.rules),
createdAt: new Date(),
updatedAt: new Date()
},
{
id: "b11b49a9-09a9-4443-916a-4246f9ff2c70", // dummy user for zod validation in response
projectId,
name: "Developer",
slug: ProjectMembershipRole.Member,
description: "Non-administrative role in an organization",
permissions: packRules(projectMemberPermissions.rules),
createdAt: new Date(),
updatedAt: new Date()
},
{
id: "b11b49a9-09a9-4443-916a-4246f9ff2c71", // dummy user for zod validation in response
projectId,
name: "Viewer",
slug: ProjectMembershipRole.Viewer,
description: "Non-administrative role in an organization",
permissions: packRules(projectViewerPermission.rules),
createdAt: new Date(),
updatedAt: new Date()
},
{
id: "b11b49a9-09a9-4443-916a-4246f9ff2c72", // dummy user for zod validation in response
projectId,
name: "No Access",
slug: "no-access",
description: "No access to any resources in the organization",
permissions: packRules(projectNoAccessPermissions.rules),
createdAt: new Date(),
updatedAt: new Date()
},
...(customRoles || []).map(({ permissions, ...data }) => ({
...data,
permissions
}))
];
return roles;
};
const getUserPermission = async (userId: string, orgId: string) => {
const { permission, membership } = await permissionService.getUserOrgPermission(userId, orgId);
return { permissions: packRules(permission.rules), membership };
};
return { createRole, updateRole, deleteRole, listRoles, getUserPermission };
};

View File

@@ -0,0 +1,116 @@
import { TDbClient } from "@app/db";
import { TableName,TProjects } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { mergeOneToManyRelation, ormify } from "@app/lib/knex";
export type TProjectDalFactory = ReturnType<typeof projectDalFactory>;
export const projectDalFactory = (db: TDbClient) => {
const projectOrm = ormify(db, TableName.Project);
const findAllProjects = async (
userId: string
): Promise<(TProjects & { environments: { id: string; slug: string; name: string }[] })[]> => {
try {
const workspaces = await db(TableName.ProjectMembership)
.where({ userId })
.join(
TableName.Environment,
`${TableName.Environment}.projectId`,
`${TableName.Project}.id`
)
.join(
TableName.Project,
`${TableName.ProjectMembership}.projectId`,
`${TableName.Project}.id`
)
.select(
db.ref("id").withSchema(TableName.Project),
db.ref("name").withSchema(TableName.Project),
db.ref("autoCapitalization").withSchema(TableName.Project),
db.ref("orgId").withSchema(TableName.Project),
db.ref("createdAt").withSchema(TableName.Project),
db.ref("updatedAt").withSchema(TableName.Project),
db.ref("id").withSchema(TableName.Environment).as("envId"),
db.ref("slug").withSchema(TableName.Environment).as("envSlug"),
db.ref("name").withSchema(TableName.Environment).as("envName")
)
.orderBy("createdAt", "asc", "last");
return mergeOneToManyRelation(
workspaces,
"id",
({ envId, envSlug, envName, ...data }) => data,
({ envName, envSlug, envId }) => ({ id: envId, slug: envSlug, name: envName }),
"environments"
);
} catch (error) {
throw new DatabaseError({ error, name: "Find all projects" });
}
};
const findProjectById = async (
id: string
): Promise<
(TProjects & { environments: { id: string; slug: string; name: string }[] }) | undefined
> => {
try {
const workspaces = await db(TableName.ProjectMembership)
.where(`${TableName.Project}.id`, id)
.join(
TableName.Environment,
`${TableName.Environment}.projectId`,
`${TableName.Project}.id`
)
.join(
TableName.Project,
`${TableName.ProjectMembership}.projectId`,
`${TableName.Project}.id`
)
.select(
db.ref("id").withSchema(TableName.Project),
db.ref("name").withSchema(TableName.Project),
db.ref("autoCapitalization").withSchema(TableName.Project),
db.ref("orgId").withSchema(TableName.Project),
db.ref("createdAt").withSchema(TableName.Project),
db.ref("updatedAt").withSchema(TableName.Project),
db.ref("id").withSchema(TableName.Environment).as("envId"),
db.ref("slug").withSchema(TableName.Environment).as("envSlug"),
db.ref("name").withSchema(TableName.Environment).as("envName")
);
const [doc] = mergeOneToManyRelation(
workspaces,
"id",
({ envId, envSlug, envName, ...data }) => data,
({ envName, envSlug, envId }) => ({ id: envId, slug: envSlug, name: envName }),
"environments"
);
return doc;
} catch (error) {
throw new DatabaseError({ error, name: "Find all projects" });
}
};
const findAllProjectUserPubKeys = async (projectId: string) => {
try {
const pubKeys = await db(TableName.ProjectMembership)
.where({ projectId })
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
.join(
TableName.UserEncryptionKey,
`${TableName.Users}.id`,
`${TableName.UserEncryptionKey}.userId`
)
.select("userId", "publicKey");
return pubKeys;
} catch (error) {
throw new DatabaseError({ error, name: "Find all workspace pub keys" });
}
};
return {
...projectOrm,
findAllProjects,
findProjectById,
findAllProjectUserPubKeys
};
};

View File

@@ -0,0 +1,113 @@
import { ProjectMembershipRole } from "@app/db/schemas";
import {
OrgPermissionActions,
OrgPermissionSubjects
} from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import {
ProjectPermissionActions,
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { TProjectEnvDalFactory } from "../project-env/project-env-dal";
import { TProjectMembershipDalFactory } from "../project-membership/project-membership-dal";
import { TProjectDalFactory } from "./project-dal";
import { TCreateProjectDTO, TDeleteProjectDTO, TGetProjectDTO } from "./project-types";
import { ForbiddenError } from "@casl/ability";
const DEFAULT_PROJECT_ENVS = [
{ name: "Development", slug: "dev" },
{ name: "Staging", slug: "staging" },
{ name: "Production", slug: "prod" }
];
type TProjectServiceFactoryDep = {
projectDal: TProjectDalFactory;
projectEnvDal: Pick<TProjectEnvDalFactory, "insertMany">;
projectMembershipDal: Pick<TProjectMembershipDalFactory, "create">;
permissionService: TPermissionServiceFactory;
};
export type TProjectServiceFactory = ReturnType<typeof projectServiceFactory>;
export const projectServiceFactory = ({
projectDal,
permissionService,
projectMembershipDal,
projectEnvDal
}: TProjectServiceFactoryDep) => {
/*
* Create workspace. Make user the admin
* */
const createProject = async ({ orgId, actor, actorId, workspaceName }: TCreateProjectDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Create,
OrgPermissionSubjects.Workspace
);
// TODO(backend-pg): licence server
const newProject = projectDal.transaction(async (tx) => {
const project = await projectDal.create({ name: workspaceName, orgId }, tx);
await projectMembershipDal.create({
userId: actorId,
role: ProjectMembershipRole.Admin,
projectId: project.id
});
const envs = await projectEnvDal.insertMany(
DEFAULT_PROJECT_ENVS.map((el) => ({ ...el, projectId: project.id })),
tx
);
return { ...project, environments: envs };
});
return newProject;
};
const deleteProject = async ({ actor, actorId, projectId }: TDeleteProjectDTO) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.Project
);
// TODO(backend-pg): licence server
const deletedProject = await projectDal.deleteById(projectId);
return deletedProject;
};
const getProjects = async (actorId: string) => {
const workspaces = await projectDal.findAllProjects(actorId);
return workspaces;
};
const getAProject = async ({ actorId, projectId, actor }: TGetProjectDTO) => {
await permissionService.getProjectPermission(actor, actorId, projectId);
return projectDal.findProjectById(projectId);
};
const toggleAutoCapitalization = async ({
projectId,
actor,
actorId,
autoCapitalization
}: TGetProjectDTO & { autoCapitalization: boolean }) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Settings
);
const updatedProject = await projectDal.updateById(projectId, { autoCapitalization });
return updatedProject;
};
return {
createProject,
deleteProject,
getProjects,
getAProject,
toggleAutoCapitalization
};
};

View File

@@ -0,0 +1,20 @@
import { ActorType } from "../auth/auth-type";
export type TCreateProjectDTO = {
actor: ActorType;
actorId: string;
orgId: string;
workspaceName: string;
};
export type TDeleteProjectDTO = {
actor: ActorType;
actorId: string;
projectId: string;
};
export type TGetProjectDTO = {
actor: ActorType;
actorId: string;
projectId: string;
};