mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 01:27:41 +00:00
feat: added backend logic for project access, search project endpoint, send mail for org admin project access direct
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { TableName } from "../schemas";
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
export async function up(knex: Knex): Promise<void> {
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
|||||||
@@ -233,3 +233,8 @@ export enum ActionProjectType {
|
|||||||
// project operations that happen on all types
|
// project operations that happen on all types
|
||||||
Any = "any"
|
Any = "any"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum SortDirection {
|
||||||
|
ASC = "asc",
|
||||||
|
DESC = "desc"
|
||||||
|
}
|
||||||
|
|||||||
@@ -285,7 +285,9 @@ export enum EventType {
|
|||||||
KMIP_OPERATION_ACTIVATE = "kmip-operation-activate",
|
KMIP_OPERATION_ACTIVATE = "kmip-operation-activate",
|
||||||
KMIP_OPERATION_REVOKE = "kmip-operation-revoke",
|
KMIP_OPERATION_REVOKE = "kmip-operation-revoke",
|
||||||
KMIP_OPERATION_LOCATE = "kmip-operation-locate",
|
KMIP_OPERATION_LOCATE = "kmip-operation-locate",
|
||||||
KMIP_OPERATION_REGISTER = "kmip-operation-register"
|
KMIP_OPERATION_REGISTER = "kmip-operation-register",
|
||||||
|
|
||||||
|
PROJECT_ACCESS_REQUEST = "project-access-request"
|
||||||
}
|
}
|
||||||
|
|
||||||
export const filterableSecretEvents: EventType[] = [
|
export const filterableSecretEvents: EventType[] = [
|
||||||
@@ -2277,6 +2279,15 @@ interface KmipOperationRegisterEvent {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ProjectAccessRequestEvent {
|
||||||
|
type: EventType.PROJECT_ACCESS_REQUEST;
|
||||||
|
metadata: {
|
||||||
|
projectId: string;
|
||||||
|
requesterId: string;
|
||||||
|
requesterEmail: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface SetupKmipEvent {
|
interface SetupKmipEvent {
|
||||||
type: EventType.SETUP_KMIP;
|
type: EventType.SETUP_KMIP;
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -2511,5 +2522,6 @@ export type Event =
|
|||||||
| KmipOperationRevokeEvent
|
| KmipOperationRevokeEvent
|
||||||
| KmipOperationLocateEvent
|
| KmipOperationLocateEvent
|
||||||
| KmipOperationRegisterEvent
|
| KmipOperationRegisterEvent
|
||||||
|
| ProjectAccessRequestEvent
|
||||||
| CreateSecretRequestEvent
|
| CreateSecretRequestEvent
|
||||||
| SecretApprovalRequestReview;
|
| SecretApprovalRequestReview;
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ export enum CharacterType {
|
|||||||
DoubleQuote = "doubleQuote", // "
|
DoubleQuote = "doubleQuote", // "
|
||||||
Comma = "comma", // ,
|
Comma = "comma", // ,
|
||||||
Semicolon = "semicolon", // ;
|
Semicolon = "semicolon", // ;
|
||||||
Exclamation = "exclamation" // !
|
Exclamation = "exclamation", // !
|
||||||
|
Fullstop = "fullStop" // !
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -81,7 +82,8 @@ export const characterValidator = (allowedCharacters: CharacterType[]) => {
|
|||||||
[CharacterType.DoubleQuote]: '\\"',
|
[CharacterType.DoubleQuote]: '\\"',
|
||||||
[CharacterType.Comma]: ",",
|
[CharacterType.Comma]: ",",
|
||||||
[CharacterType.Semicolon]: ";",
|
[CharacterType.Semicolon]: ";",
|
||||||
[CharacterType.Exclamation]: "!"
|
[CharacterType.Exclamation]: "!",
|
||||||
|
[CharacterType.Fullstop]: "."
|
||||||
};
|
};
|
||||||
|
|
||||||
// Combine patterns from allowed characters
|
// Combine patterns from allowed characters
|
||||||
|
|||||||
@@ -662,6 +662,7 @@ export const registerRoutes = async (
|
|||||||
});
|
});
|
||||||
|
|
||||||
const orgAdminService = orgAdminServiceFactory({
|
const orgAdminService = orgAdminServiceFactory({
|
||||||
|
smtpService,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
projectUserMembershipRoleDAL,
|
projectUserMembershipRoleDAL,
|
||||||
@@ -964,7 +965,8 @@ export const registerRoutes = async (
|
|||||||
projectSlackConfigDAL,
|
projectSlackConfigDAL,
|
||||||
slackIntegrationDAL,
|
slackIntegrationDAL,
|
||||||
projectTemplateService,
|
projectTemplateService,
|
||||||
groupProjectDAL
|
groupProjectDAL,
|
||||||
|
smtpService
|
||||||
});
|
});
|
||||||
|
|
||||||
const projectEnvService = projectEnvServiceFactory({
|
const projectEnvService = projectEnvServiceFactory({
|
||||||
|
|||||||
@@ -8,15 +8,17 @@ import {
|
|||||||
ProjectSlackConfigsSchema,
|
ProjectSlackConfigsSchema,
|
||||||
ProjectType,
|
ProjectType,
|
||||||
SecretFoldersSchema,
|
SecretFoldersSchema,
|
||||||
|
SortDirection,
|
||||||
UserEncryptionKeysSchema,
|
UserEncryptionKeysSchema,
|
||||||
UsersSchema
|
UsersSchema
|
||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { PROJECTS } from "@app/lib/api-docs";
|
import { PROJECTS } from "@app/lib/api-docs";
|
||||||
|
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
|
||||||
import { ProjectFilterType } from "@app/services/project/project-types";
|
import { ProjectFilterType, SearchProjectSortBy } from "@app/services/project/project-types";
|
||||||
import { validateSlackChannelsField } from "@app/services/slack/slack-auth-validators";
|
import { validateSlackChannelsField } from "@app/services/slack/slack-auth-validators";
|
||||||
|
|
||||||
import { integrationAuthPubSchema, SanitizedProjectSchema } from "../sanitizedSchemas";
|
import { integrationAuthPubSchema, SanitizedProjectSchema } from "../sanitizedSchemas";
|
||||||
@@ -704,4 +706,105 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
return environmentsFolders;
|
return environmentsFolders;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/search",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
limit: z.number().default(100),
|
||||||
|
offset: z.number().default(0),
|
||||||
|
type: z.nativeEnum(ProjectType).optional(),
|
||||||
|
orderBy: z.nativeEnum(SearchProjectSortBy).optional().default(SearchProjectSortBy.NAME),
|
||||||
|
orderDirection: z.nativeEnum(SortDirection).optional().default(SortDirection.ASC),
|
||||||
|
name: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.refine((val) => characterValidator([CharacterType.AlphaNumeric, CharacterType.Hyphen])(val), {
|
||||||
|
message: "Invalid pattern: only alphanumeric characters, spaces, - are allowed."
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
projects: SanitizedProjectSchema.extend({ isMember: z.boolean() }).array(),
|
||||||
|
totalCount: z.number()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { docs: projects, totalCount } = await server.services.project.searchProjects({
|
||||||
|
permission: req.permission,
|
||||||
|
...req.body
|
||||||
|
});
|
||||||
|
|
||||||
|
return { projects, totalCount };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/:workspaceId/project-access",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
workspaceId: z.string().trim()
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
comment: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.refine(
|
||||||
|
(val) =>
|
||||||
|
characterValidator([
|
||||||
|
CharacterType.AlphaNumeric,
|
||||||
|
CharacterType.Hyphen,
|
||||||
|
CharacterType.Comma,
|
||||||
|
CharacterType.Fullstop,
|
||||||
|
CharacterType.Spaces,
|
||||||
|
CharacterType.Exclamation
|
||||||
|
])(val),
|
||||||
|
{
|
||||||
|
message: "Invalid pattern: only alphanumeric characters, spaces, - are allowed."
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
message: z.string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
await server.services.project.requestProjectAccess({
|
||||||
|
permission: req.permission,
|
||||||
|
comment: req.body.comment,
|
||||||
|
projectId: req.params.workspaceId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (req.auth.actor === ActorType.USER) {
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: req.params.workspaceId,
|
||||||
|
event: {
|
||||||
|
type: EventType.PROJECT_ACCESS_REQUEST,
|
||||||
|
metadata: {
|
||||||
|
projectId: req.params.workspaceId,
|
||||||
|
requesterEmail: req.auth.user.email || req.auth.user.username,
|
||||||
|
requesterId: req.auth.userId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { message: "Project access request has been send to project admins" };
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,17 +12,22 @@ import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
|||||||
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
||||||
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||||
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
||||||
|
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||||
import { TUserDALFactory } from "../user/user-dal";
|
import { TUserDALFactory } from "../user/user-dal";
|
||||||
import { TAccessProjectDTO, TListOrgProjectsDTO } from "./org-admin-types";
|
import { TAccessProjectDTO, TListOrgProjectsDTO } from "./org-admin-types";
|
||||||
|
|
||||||
type TOrgAdminServiceFactoryDep = {
|
type TOrgAdminServiceFactoryDep = {
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||||
projectDAL: Pick<TProjectDALFactory, "find" | "findById" | "findProjectGhostUser">;
|
projectDAL: Pick<TProjectDALFactory, "find" | "findById" | "findProjectGhostUser">;
|
||||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findOne" | "create" | "transaction" | "delete">;
|
projectMembershipDAL: Pick<
|
||||||
|
TProjectMembershipDALFactory,
|
||||||
|
"findOne" | "create" | "transaction" | "delete" | "findAllProjectMembers"
|
||||||
|
>;
|
||||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "create">;
|
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "create">;
|
||||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
||||||
userDAL: Pick<TUserDALFactory, "findUserEncKeyByUserId">;
|
userDAL: Pick<TUserDALFactory, "findUserEncKeyByUserId">;
|
||||||
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create" | "delete">;
|
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create" | "delete">;
|
||||||
|
smtpService: Pick<TSmtpService, "sendMail">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TOrgAdminServiceFactory = ReturnType<typeof orgAdminServiceFactory>;
|
export type TOrgAdminServiceFactory = ReturnType<typeof orgAdminServiceFactory>;
|
||||||
@@ -34,7 +39,8 @@ export const orgAdminServiceFactory = ({
|
|||||||
projectKeyDAL,
|
projectKeyDAL,
|
||||||
projectBotDAL,
|
projectBotDAL,
|
||||||
userDAL,
|
userDAL,
|
||||||
projectUserMembershipRoleDAL
|
projectUserMembershipRoleDAL,
|
||||||
|
smtpService
|
||||||
}: TOrgAdminServiceFactoryDep) => {
|
}: TOrgAdminServiceFactoryDep) => {
|
||||||
const listOrgProjects = async ({
|
const listOrgProjects = async ({
|
||||||
actor,
|
actor,
|
||||||
@@ -184,6 +190,23 @@ export const orgAdminServiceFactory = ({
|
|||||||
);
|
);
|
||||||
return newProjectMembership;
|
return newProjectMembership;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId);
|
||||||
|
const filteredProjectMembers = projectMembers
|
||||||
|
.filter(
|
||||||
|
(member) => member.roles.some((role) => role.role === ProjectMembershipRole.Admin) && member.userId !== actorId
|
||||||
|
)
|
||||||
|
.map((el) => el.user.email!);
|
||||||
|
|
||||||
|
await smtpService.sendMail({
|
||||||
|
template: SmtpTemplates.OrgAdminProjectDirectAccess,
|
||||||
|
recipients: filteredProjectMembers,
|
||||||
|
subjectLine: "Organization Admin Project Direct Access Issued",
|
||||||
|
substitutions: {
|
||||||
|
projectName: project.name,
|
||||||
|
email: projectMembers.find((el) => el.userId === actorId)?.user?.username
|
||||||
|
}
|
||||||
|
});
|
||||||
return { isExistingMember: false, membership: updatedMembership };
|
return { isExistingMember: false, membership: updatedMembership };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -231,7 +231,7 @@ export const orgServiceFactory = ({
|
|||||||
|
|
||||||
const findAllWorkspaces = async ({ actor, actorId, orgId, type }: TFindAllWorkspacesDTO) => {
|
const findAllWorkspaces = async ({ actor, actorId, orgId, type }: TFindAllWorkspacesDTO) => {
|
||||||
if (actor === ActorType.USER) {
|
if (actor === ActorType.USER) {
|
||||||
const workspaces = await projectDAL.findAllProjects(actorId, orgId, type || "all");
|
const workspaces = await projectDAL.findUserProjects(actorId, orgId, type || "all");
|
||||||
return workspaces;
|
return workspaces;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,20 +6,23 @@ import {
|
|||||||
ProjectType,
|
ProjectType,
|
||||||
ProjectUpgradeStatus,
|
ProjectUpgradeStatus,
|
||||||
ProjectVersion,
|
ProjectVersion,
|
||||||
|
SortDirection,
|
||||||
TableName,
|
TableName,
|
||||||
|
TProjects,
|
||||||
TProjectsUpdate
|
TProjectsUpdate
|
||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
import { BadRequestError, DatabaseError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
import { BadRequestError, DatabaseError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||||
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
|
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
|
||||||
|
|
||||||
import { Filter, ProjectFilterType } from "./project-types";
|
import { ActorType } from "../auth/auth-type";
|
||||||
|
import { Filter, ProjectFilterType, SearchProjectSortBy } from "./project-types";
|
||||||
|
|
||||||
export type TProjectDALFactory = ReturnType<typeof projectDALFactory>;
|
export type TProjectDALFactory = ReturnType<typeof projectDALFactory>;
|
||||||
|
|
||||||
export const projectDALFactory = (db: TDbClient) => {
|
export const projectDALFactory = (db: TDbClient) => {
|
||||||
const projectOrm = ormify(db, TableName.Project);
|
const projectOrm = ormify(db, TableName.Project);
|
||||||
|
|
||||||
const findAllProjects = async (userId: string, orgId: string, projectType: ProjectType | "all") => {
|
const findUserProjects = async (userId: string, orgId: string, projectType: ProjectType | "all") => {
|
||||||
try {
|
try {
|
||||||
const workspaces = await db
|
const workspaces = await db
|
||||||
.replicaNode()(TableName.ProjectMembership)
|
.replicaNode()(TableName.ProjectMembership)
|
||||||
@@ -352,9 +355,79 @@ export const projectDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const searchProjects = async (dto: {
|
||||||
|
orgId: string;
|
||||||
|
actor: ActorType;
|
||||||
|
actorId: string;
|
||||||
|
type?: ProjectType;
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
name?: string;
|
||||||
|
sortBy?: SearchProjectSortBy;
|
||||||
|
sortDir?: SortDirection;
|
||||||
|
}) => {
|
||||||
|
const { limit = 20, offset = 0, sortBy = SearchProjectSortBy.NAME, sortDir = SortDirection.ASC } = dto;
|
||||||
|
|
||||||
|
const userMembershipSubquery = db(TableName.ProjectMembership).where({ userId: dto.actorId }).select("projectId");
|
||||||
|
const groups = db(TableName.UserGroupMembership).where({ userId: dto.actorId }).select("groupId");
|
||||||
|
const groupMembershipSubquery = db(TableName.GroupProjectMembership).whereIn("groupId", groups).select("projectId");
|
||||||
|
|
||||||
|
const identityMembershipSubQuery = db(TableName.IdentityProjectMembership)
|
||||||
|
.where({ identityId: dto.actorId })
|
||||||
|
.select("projectId");
|
||||||
|
|
||||||
|
// Get the SQL strings for the subqueries
|
||||||
|
const userMembershipSql = userMembershipSubquery.toQuery();
|
||||||
|
const groupMembershipSql = groupMembershipSubquery.toQuery();
|
||||||
|
const identityMembershipSql = identityMembershipSubQuery.toQuery();
|
||||||
|
|
||||||
|
const query = db
|
||||||
|
.replicaNode()(TableName.Project)
|
||||||
|
.where(`${TableName.Project}.orgId`, dto.orgId)
|
||||||
|
.select(selectAllTableCols(TableName.Project))
|
||||||
|
.select(db.raw("COUNT(*) OVER() AS count"))
|
||||||
|
.select<(TProjects & { isMember: boolean; count: number })[]>(
|
||||||
|
dto.actor === ActorType.USER
|
||||||
|
? db.raw(
|
||||||
|
`
|
||||||
|
CASE
|
||||||
|
WHEN ${TableName.Project}.id IN (?) THEN TRUE
|
||||||
|
WHEN ${TableName.Project}.id IN (?) THEN TRUE
|
||||||
|
ELSE FALSE
|
||||||
|
END as "isMember"
|
||||||
|
`,
|
||||||
|
[db.raw(userMembershipSql), db.raw(groupMembershipSql)]
|
||||||
|
)
|
||||||
|
: db.raw(
|
||||||
|
`
|
||||||
|
CASE
|
||||||
|
WHEN ${TableName.Project}.id IN (?) THEN TRUE
|
||||||
|
ELSE FALSE
|
||||||
|
END as "isMember"
|
||||||
|
`,
|
||||||
|
[db.raw(identityMembershipSql)]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(limit)
|
||||||
|
.offset(offset);
|
||||||
|
if (sortBy === SearchProjectSortBy.NAME) {
|
||||||
|
void query.orderBy([{ column: `${TableName.Project}.name`, order: sortDir }]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dto.type) {
|
||||||
|
void query.where(`${TableName.Project}.type`, dto.type);
|
||||||
|
}
|
||||||
|
if (dto.name) {
|
||||||
|
void query.whereILike(`${TableName.Project}.name`, `%${dto.name}%`);
|
||||||
|
}
|
||||||
|
const docs = await query;
|
||||||
|
|
||||||
|
return { docs, totalCount: Number(docs?.[0]?.count ?? 0) };
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...projectOrm,
|
...projectOrm,
|
||||||
findAllProjects,
|
findUserProjects,
|
||||||
setProjectUpgradeStatus,
|
setProjectUpgradeStatus,
|
||||||
findAllProjectsByIdentity,
|
findAllProjectsByIdentity,
|
||||||
findProjectGhostUser,
|
findProjectGhostUser,
|
||||||
@@ -363,6 +436,7 @@ export const projectDALFactory = (db: TDbClient) => {
|
|||||||
findProjectBySlug,
|
findProjectBySlug,
|
||||||
findProjectWithOrg,
|
findProjectWithOrg,
|
||||||
checkProjectUpgradeStatus,
|
checkProjectUpgradeStatus,
|
||||||
getProjectFromSplitId
|
getProjectFromSplitId,
|
||||||
|
searchProjects
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { TSshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-cer
|
|||||||
import { TSshCertificateDALFactory } from "@app/ee/services/ssh-certificate/ssh-certificate-dal";
|
import { TSshCertificateDALFactory } from "@app/ee/services/ssh-certificate/ssh-certificate-dal";
|
||||||
import { TSshCertificateTemplateDALFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-dal";
|
import { TSshCertificateTemplateDALFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-dal";
|
||||||
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { groupBy } from "@app/lib/fn";
|
import { groupBy } from "@app/lib/fn";
|
||||||
@@ -57,6 +58,7 @@ import { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secr
|
|||||||
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
|
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
|
||||||
import { TProjectSlackConfigDALFactory } from "../slack/project-slack-config-dal";
|
import { TProjectSlackConfigDALFactory } from "../slack/project-slack-config-dal";
|
||||||
import { TSlackIntegrationDALFactory } from "../slack/slack-integration-dal";
|
import { TSlackIntegrationDALFactory } from "../slack/slack-integration-dal";
|
||||||
|
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||||
import { TUserDALFactory } from "../user/user-dal";
|
import { TUserDALFactory } from "../user/user-dal";
|
||||||
import { TProjectDALFactory } from "./project-dal";
|
import { TProjectDALFactory } from "./project-dal";
|
||||||
import { assignWorkspaceKeysToMembers, createProjectKey } from "./project-fns";
|
import { assignWorkspaceKeysToMembers, createProjectKey } from "./project-fns";
|
||||||
@@ -76,6 +78,8 @@ import {
|
|||||||
TListProjectSshCertificatesDTO,
|
TListProjectSshCertificatesDTO,
|
||||||
TListProjectSshCertificateTemplatesDTO,
|
TListProjectSshCertificateTemplatesDTO,
|
||||||
TLoadProjectKmsBackupDTO,
|
TLoadProjectKmsBackupDTO,
|
||||||
|
TProjectAccessRequestDTO,
|
||||||
|
TSearchProjectsDTO,
|
||||||
TToggleProjectAutoCapitalizationDTO,
|
TToggleProjectAutoCapitalizationDTO,
|
||||||
TUpdateAuditLogsRetentionDTO,
|
TUpdateAuditLogsRetentionDTO,
|
||||||
TUpdateProjectDTO,
|
TUpdateProjectDTO,
|
||||||
@@ -106,7 +110,10 @@ type TProjectServiceFactoryDep = {
|
|||||||
identityProjectDAL: TIdentityProjectDALFactory;
|
identityProjectDAL: TIdentityProjectDALFactory;
|
||||||
identityProjectMembershipRoleDAL: Pick<TIdentityProjectMembershipRoleDALFactory, "create">;
|
identityProjectMembershipRoleDAL: Pick<TIdentityProjectMembershipRoleDALFactory, "create">;
|
||||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "create" | "findLatestProjectKey" | "delete" | "find" | "insertMany">;
|
projectKeyDAL: Pick<TProjectKeyDALFactory, "create" | "findLatestProjectKey" | "delete" | "find" | "insertMany">;
|
||||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "create" | "findProjectGhostUser" | "findOne" | "delete">;
|
projectMembershipDAL: Pick<
|
||||||
|
TProjectMembershipDALFactory,
|
||||||
|
"create" | "findProjectGhostUser" | "findOne" | "delete" | "findAllProjectMembers"
|
||||||
|
>;
|
||||||
groupProjectDAL: Pick<TGroupProjectDALFactory, "delete">;
|
groupProjectDAL: Pick<TGroupProjectDALFactory, "delete">;
|
||||||
projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "findOne" | "transaction" | "updateById" | "create">;
|
projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "findOne" | "transaction" | "updateById" | "create">;
|
||||||
slackIntegrationDAL: Pick<TSlackIntegrationDALFactory, "findById" | "findByIdWithWorkflowIntegrationDetails">;
|
slackIntegrationDAL: Pick<TSlackIntegrationDALFactory, "findById" | "findByIdWithWorkflowIntegrationDetails">;
|
||||||
@@ -123,6 +130,7 @@ type TProjectServiceFactoryDep = {
|
|||||||
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
|
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
queueService: Pick<TQueueServiceFactory, "stopRepeatableJob">;
|
queueService: Pick<TQueueServiceFactory, "stopRepeatableJob">;
|
||||||
|
smtpService: Pick<TSmtpService, "sendMail">;
|
||||||
|
|
||||||
orgDAL: Pick<TOrgDALFactory, "findOne">;
|
orgDAL: Pick<TOrgDALFactory, "findOne">;
|
||||||
keyStore: Pick<TKeyStoreFactory, "deleteItem">;
|
keyStore: Pick<TKeyStoreFactory, "deleteItem">;
|
||||||
@@ -177,7 +185,8 @@ export const projectServiceFactory = ({
|
|||||||
projectSlackConfigDAL,
|
projectSlackConfigDAL,
|
||||||
slackIntegrationDAL,
|
slackIntegrationDAL,
|
||||||
projectTemplateService,
|
projectTemplateService,
|
||||||
groupProjectDAL
|
groupProjectDAL,
|
||||||
|
smtpService
|
||||||
}: TProjectServiceFactoryDep) => {
|
}: TProjectServiceFactoryDep) => {
|
||||||
/*
|
/*
|
||||||
* Create workspace. Make user the admin
|
* Create workspace. Make user the admin
|
||||||
@@ -506,7 +515,7 @@ export const projectServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
type = ProjectType.SecretManager
|
type = ProjectType.SecretManager
|
||||||
}: TListProjectsDTO) => {
|
}: TListProjectsDTO) => {
|
||||||
const workspaces = await projectDAL.findAllProjects(actorId, actorOrgId, type);
|
const workspaces = await projectDAL.findUserProjects(actorId, actorOrgId, type);
|
||||||
|
|
||||||
if (includeRoles) {
|
if (includeRoles) {
|
||||||
const { permission } = await permissionService.getUserOrgPermission(
|
const { permission } = await permissionService.getUserOrgPermission(
|
||||||
@@ -1339,6 +1348,85 @@ export const projectServiceFactory = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const searchProjects = async ({
|
||||||
|
name,
|
||||||
|
offset,
|
||||||
|
permission,
|
||||||
|
limit,
|
||||||
|
type,
|
||||||
|
orderBy,
|
||||||
|
orderDirection
|
||||||
|
}: TSearchProjectsDTO) => {
|
||||||
|
// check user belong to org
|
||||||
|
await permissionService.getOrgPermission(
|
||||||
|
permission.type,
|
||||||
|
permission.id,
|
||||||
|
permission.orgId,
|
||||||
|
permission.authMethod,
|
||||||
|
permission.orgId
|
||||||
|
);
|
||||||
|
|
||||||
|
return projectDAL.searchProjects({
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
orgId: permission.orgId,
|
||||||
|
actor: permission.type,
|
||||||
|
actorId: permission.id,
|
||||||
|
sortBy: orderBy,
|
||||||
|
sortDir: orderDirection
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestProjectAccess = async ({ permission, comment, projectId }: TProjectAccessRequestDTO) => {
|
||||||
|
// check user belong to org
|
||||||
|
await permissionService.getOrgPermission(
|
||||||
|
permission.type,
|
||||||
|
permission.id,
|
||||||
|
permission.orgId,
|
||||||
|
permission.authMethod,
|
||||||
|
permission.orgId
|
||||||
|
);
|
||||||
|
|
||||||
|
const projectMember = await permissionService
|
||||||
|
.getProjectPermission({
|
||||||
|
actor: permission.type,
|
||||||
|
actorId: permission.id,
|
||||||
|
projectId,
|
||||||
|
actionProjectType: ActionProjectType.Any,
|
||||||
|
actorAuthMethod: permission.authMethod,
|
||||||
|
actorOrgId: permission.orgId
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
if (projectMember) throw new BadRequestError({ message: "User already has access to the project" });
|
||||||
|
|
||||||
|
const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId);
|
||||||
|
const filteredProjectMembers = projectMembers
|
||||||
|
.filter((member) => member.roles.some((role) => role.role === ProjectMembershipRole.Admin))
|
||||||
|
.map((el) => el.user.email!);
|
||||||
|
const org = await orgDAL.findOne({ id: permission.orgId });
|
||||||
|
const project = await projectDAL.findById(projectId);
|
||||||
|
const userDetails = await userDAL.findById(permission.id);
|
||||||
|
const appCfg = getConfig();
|
||||||
|
|
||||||
|
await smtpService.sendMail({
|
||||||
|
template: SmtpTemplates.ProjectAccessRequest,
|
||||||
|
recipients: filteredProjectMembers,
|
||||||
|
subjectLine: "Project Access Request",
|
||||||
|
substitutions: {
|
||||||
|
requesterName: `${userDetails.firstName} ${userDetails.lastName}`,
|
||||||
|
requesterEmail: userDetails.email,
|
||||||
|
projectName: project?.name,
|
||||||
|
orgName: org?.name,
|
||||||
|
note: comment,
|
||||||
|
callback_url: `${appCfg.SITE_URL}/${project.type}/${project.id}/access-management?selectedTab=members&requesterEmail=${userDetails.email}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
createProject,
|
createProject,
|
||||||
deleteProject,
|
deleteProject,
|
||||||
@@ -1364,6 +1452,8 @@ export const projectServiceFactory = ({
|
|||||||
loadProjectKmsBackup,
|
loadProjectKmsBackup,
|
||||||
getProjectKmsKeys,
|
getProjectKmsKeys,
|
||||||
getProjectSlackConfig,
|
getProjectSlackConfig,
|
||||||
updateProjectSlackConfig
|
updateProjectSlackConfig,
|
||||||
|
requestProjectAccess,
|
||||||
|
searchProjects
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { ProjectType, TProjectKeys } from "@app/db/schemas";
|
import { ProjectType, SortDirection, TProjectKeys } from "@app/db/schemas";
|
||||||
import { TProjectPermission } from "@app/lib/types";
|
import { OrgServiceActor, TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
||||||
|
|
||||||
@@ -158,3 +158,23 @@ export type TUpdateProjectSlackConfig = {
|
|||||||
isSecretRequestNotificationEnabled: boolean;
|
isSecretRequestNotificationEnabled: boolean;
|
||||||
secretRequestChannels: string;
|
secretRequestChannels: string;
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|
||||||
|
export enum SearchProjectSortBy {
|
||||||
|
NAME = "name"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TSearchProjectsDTO = {
|
||||||
|
permission: OrgServiceActor;
|
||||||
|
name?: string;
|
||||||
|
type?: ProjectType;
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
orderBy?: SearchProjectSortBy;
|
||||||
|
orderDirection?: SortDirection;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TProjectAccessRequestDTO = {
|
||||||
|
permission: OrgServiceActor;
|
||||||
|
projectId: string;
|
||||||
|
comment: string;
|
||||||
|
};
|
||||||
|
|||||||
@@ -40,7 +40,9 @@ export enum SmtpTemplates {
|
|||||||
ExternalImportSuccessful = "externalImportSuccessful.handlebars",
|
ExternalImportSuccessful = "externalImportSuccessful.handlebars",
|
||||||
ExternalImportFailed = "externalImportFailed.handlebars",
|
ExternalImportFailed = "externalImportFailed.handlebars",
|
||||||
ExternalImportStarted = "externalImportStarted.handlebars",
|
ExternalImportStarted = "externalImportStarted.handlebars",
|
||||||
SecretRequestCompleted = "secretRequestCompleted.handlebars"
|
SecretRequestCompleted = "secretRequestCompleted.handlebars",
|
||||||
|
ProjectAccessRequest = "projectAccess.handlebars",
|
||||||
|
OrgAdminProjectDirectAccess = "orgAdminProjectGrantAccess.handlebars"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SmtpHost {
|
export enum SmtpHost {
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||||
|
<title>Organization admin issued direct access to project</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h2>Infisical</h2>
|
||||||
|
<p>The organization admin {{email}} has granted direct access to the project "{{projectName}}".</p>
|
||||||
|
|
||||||
|
{{emailFooter}}
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||||
|
<title>Project Access Request</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h2>Infisical</h2>
|
||||||
|
<h2>You have a new project access request!</h2>
|
||||||
|
<ul>
|
||||||
|
<li>Requester Name: "{{requesterName}}"</li>
|
||||||
|
<li>Requester Email: "{{requesterEmail}}"</li>
|
||||||
|
<li>Project Name: "{{projectName}}"</li>
|
||||||
|
<li>Organization Name: "{{orgName}}"</li>
|
||||||
|
<li>User Note: "{{note}}"</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
Please click on the link below to grant access
|
||||||
|
</p>
|
||||||
|
<a href="{{callback_url}}">Grant Access</a>
|
||||||
|
{{emailFooter}}
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user