mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat(infisical-pg: added services for project, project key and environment
This commit is contained in:
@@ -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 { };
|
||||
};
|
||||
`
|
||||
);
|
||||
}
|
||||
|
||||
6
backend-pg/src/@types/fastify.d.ts
vendored
6
backend-pg/src/@types/fastify.d.ts
vendored
@@ -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;
|
||||
|
||||
11
backend-pg/src/@types/knex.d.ts
vendored
11
backend-pg/src/@types/knex.d.ts
vendored
@@ -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
|
||||
>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
23
backend-pg/src/db/schemas/project-keys.ts
Normal file
23
backend-pg/src/db/schemas/project-keys.ts
Normal 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>>;
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
286
backend-pg/src/ee/services/permission/project-permission.ts
Normal file
286
backend-pg/src/ee/services/permission/project-permission.ts
Normal 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;
|
||||
};
|
||||
@@ -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" });
|
||||
}
|
||||
|
||||
28
backend-pg/src/lib/knex/join.ts
Normal file
28
backend-pg/src/lib/knex/join.ts
Normal 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;
|
||||
};
|
||||
@@ -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];
|
||||
|
||||
@@ -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:
|
||||
|
||||
13
backend-pg/src/server/plugins/auth/inject-permission.ts
Normal file
13
backend-pg/src/server/plugins/auth/inject-permission.ts
Normal 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 };
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
/*
|
||||
|
||||
10
backend-pg/src/services/project-env/project-env-dal.ts
Normal file
10
backend-pg/src/services/project-env/project-env-dal.ts
Normal 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;
|
||||
};
|
||||
87
backend-pg/src/services/project-env/project-env-service.ts
Normal file
87
backend-pg/src/services/project-env/project-env-service.ts
Normal 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
|
||||
};
|
||||
};
|
||||
16
backend-pg/src/services/project-env/project-env-types.ts
Normal file
16
backend-pg/src/services/project-env/project-env-types.ts
Normal 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;
|
||||
42
backend-pg/src/services/project-key/project-key-dal.ts
Normal file
42
backend-pg/src/services/project-key/project-key-dal.ts
Normal 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
|
||||
};
|
||||
};
|
||||
64
backend-pg/src/services/project-key/project-key-service.ts
Normal file
64
backend-pg/src/services/project-key/project-key-service.ts
Normal 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
|
||||
};
|
||||
};
|
||||
9
backend-pg/src/services/project-key/project-key-types.ts
Normal file
9
backend-pg/src/services/project-key/project-key-types.ts
Normal 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;
|
||||
@@ -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 };
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
7
backend-pg/src/services/project-role/project-role-dal.ts
Normal file
7
backend-pg/src/services/project-role/project-role-dal.ts
Normal 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);
|
||||
158
backend-pg/src/services/project-role/project-role-service.ts
Normal file
158
backend-pg/src/services/project-role/project-role-service.ts
Normal 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 };
|
||||
};
|
||||
116
backend-pg/src/services/project/project-dal.ts
Normal file
116
backend-pg/src/services/project/project-dal.ts
Normal 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
|
||||
};
|
||||
};
|
||||
113
backend-pg/src/services/project/project-service.ts
Normal file
113
backend-pg/src/services/project/project-service.ts
Normal 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
|
||||
};
|
||||
};
|
||||
20
backend-pg/src/services/project/project-types.ts
Normal file
20
backend-pg/src/services/project/project-types.ts
Normal 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;
|
||||
};
|
||||
Reference in New Issue
Block a user