mirror of
https://github.com/Infisical/infisical.git
synced 2025-08-24 20:43:19 +00:00
Compare commits
22 Commits
misc/add-s
...
doc/added-
Author | SHA1 | Date | |
---|---|---|---|
|
f0ec8c883f | ||
|
b30706607f | ||
|
2a3d19dcb2 | ||
|
b4ff620b44 | ||
|
23f1888123 | ||
|
7764f63299 | ||
|
cb3365afd4 | ||
|
58705ffc3f | ||
|
67e57d8993 | ||
|
90ff13a6b5 | ||
|
36145a15c1 | ||
|
4f64ed6b42 | ||
|
d47959ca83 | ||
|
3b2953ca58 | ||
|
1daa503e0e | ||
|
d69e8d2a8d | ||
|
7c7af347fc | ||
|
7e9743b4c2 | ||
|
34cf544b3a | ||
|
12fd063cd5 | ||
|
8fb6063686 | ||
|
459b262865 |
@@ -1,91 +0,0 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasEncryptedGithubAppConnectionClientIdColumn = await knex.schema.hasColumn(
|
||||
TableName.SuperAdmin,
|
||||
"encryptedGitHubAppConnectionClientId"
|
||||
);
|
||||
const hasEncryptedGithubAppConnectionClientSecretColumn = await knex.schema.hasColumn(
|
||||
TableName.SuperAdmin,
|
||||
"encryptedGitHubAppConnectionClientSecret"
|
||||
);
|
||||
|
||||
const hasEncryptedGithubAppConnectionSlugColumn = await knex.schema.hasColumn(
|
||||
TableName.SuperAdmin,
|
||||
"encryptedGitHubAppConnectionSlug"
|
||||
);
|
||||
|
||||
const hasEncryptedGithubAppConnectionAppIdColumn = await knex.schema.hasColumn(
|
||||
TableName.SuperAdmin,
|
||||
"encryptedGitHubAppConnectionId"
|
||||
);
|
||||
|
||||
const hasEncryptedGithubAppConnectionAppPrivateKeyColumn = await knex.schema.hasColumn(
|
||||
TableName.SuperAdmin,
|
||||
"encryptedGitHubAppConnectionPrivateKey"
|
||||
);
|
||||
|
||||
await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
|
||||
if (!hasEncryptedGithubAppConnectionClientIdColumn) {
|
||||
t.binary("encryptedGitHubAppConnectionClientId").nullable();
|
||||
}
|
||||
if (!hasEncryptedGithubAppConnectionClientSecretColumn) {
|
||||
t.binary("encryptedGitHubAppConnectionClientSecret").nullable();
|
||||
}
|
||||
if (!hasEncryptedGithubAppConnectionSlugColumn) {
|
||||
t.binary("encryptedGitHubAppConnectionSlug").nullable();
|
||||
}
|
||||
if (!hasEncryptedGithubAppConnectionAppIdColumn) {
|
||||
t.binary("encryptedGitHubAppConnectionId").nullable();
|
||||
}
|
||||
if (!hasEncryptedGithubAppConnectionAppPrivateKeyColumn) {
|
||||
t.binary("encryptedGitHubAppConnectionPrivateKey").nullable();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasEncryptedGithubAppConnectionClientIdColumn = await knex.schema.hasColumn(
|
||||
TableName.SuperAdmin,
|
||||
"encryptedGitHubAppConnectionClientId"
|
||||
);
|
||||
const hasEncryptedGithubAppConnectionClientSecretColumn = await knex.schema.hasColumn(
|
||||
TableName.SuperAdmin,
|
||||
"encryptedGitHubAppConnectionClientSecret"
|
||||
);
|
||||
|
||||
const hasEncryptedGithubAppConnectionSlugColumn = await knex.schema.hasColumn(
|
||||
TableName.SuperAdmin,
|
||||
"encryptedGitHubAppConnectionSlug"
|
||||
);
|
||||
|
||||
const hasEncryptedGithubAppConnectionAppIdColumn = await knex.schema.hasColumn(
|
||||
TableName.SuperAdmin,
|
||||
"encryptedGitHubAppConnectionId"
|
||||
);
|
||||
|
||||
const hasEncryptedGithubAppConnectionAppPrivateKeyColumn = await knex.schema.hasColumn(
|
||||
TableName.SuperAdmin,
|
||||
"encryptedGitHubAppConnectionPrivateKey"
|
||||
);
|
||||
|
||||
await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
|
||||
if (hasEncryptedGithubAppConnectionClientIdColumn) {
|
||||
t.dropColumn("encryptedGitHubAppConnectionClientId");
|
||||
}
|
||||
if (hasEncryptedGithubAppConnectionClientSecretColumn) {
|
||||
t.dropColumn("encryptedGitHubAppConnectionClientSecret");
|
||||
}
|
||||
if (hasEncryptedGithubAppConnectionSlugColumn) {
|
||||
t.dropColumn("encryptedGitHubAppConnectionSlug");
|
||||
}
|
||||
if (hasEncryptedGithubAppConnectionAppIdColumn) {
|
||||
t.dropColumn("encryptedGitHubAppConnectionId");
|
||||
}
|
||||
if (hasEncryptedGithubAppConnectionAppPrivateKeyColumn) {
|
||||
t.dropColumn("encryptedGitHubAppConnectionPrivateKey");
|
||||
}
|
||||
});
|
||||
}
|
@@ -29,12 +29,7 @@ export const SuperAdminSchema = z.object({
|
||||
adminIdentityIds: z.string().array().nullable().optional(),
|
||||
encryptedMicrosoftTeamsAppId: zodBuffer.nullable().optional(),
|
||||
encryptedMicrosoftTeamsClientSecret: zodBuffer.nullable().optional(),
|
||||
encryptedMicrosoftTeamsBotId: zodBuffer.nullable().optional(),
|
||||
encryptedGitHubAppConnectionClientId: zodBuffer.nullable().optional(),
|
||||
encryptedGitHubAppConnectionClientSecret: zodBuffer.nullable().optional(),
|
||||
encryptedGitHubAppConnectionSlug: zodBuffer.nullable().optional(),
|
||||
encryptedGitHubAppConnectionId: zodBuffer.nullable().optional(),
|
||||
encryptedGitHubAppConnectionPrivateKey: zodBuffer.nullable().optional()
|
||||
encryptedMicrosoftTeamsBotId: zodBuffer.nullable().optional()
|
||||
});
|
||||
|
||||
export type TSuperAdmin = z.infer<typeof SuperAdminSchema>;
|
||||
|
@@ -89,7 +89,7 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
projectSlug: z.string().trim(),
|
||||
authorProjectMembershipId: z.string().trim().optional(),
|
||||
authorUserId: z.string().trim().optional(),
|
||||
envSlug: z.string().trim().optional()
|
||||
}),
|
||||
response: {
|
||||
@@ -143,7 +143,7 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
|
||||
handler: async (req) => {
|
||||
const { requests } = await server.services.accessApprovalRequest.listApprovalRequests({
|
||||
projectSlug: req.query.projectSlug,
|
||||
authorProjectMembershipId: req.query.authorProjectMembershipId,
|
||||
authorUserId: req.query.authorUserId,
|
||||
envSlug: req.query.envSlug,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
|
@@ -30,6 +30,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
||||
workspaceId: z.string().trim(),
|
||||
environment: z.string().trim().optional(),
|
||||
committer: z.string().trim().optional(),
|
||||
search: z.string().trim().optional(),
|
||||
status: z.nativeEnum(RequestState).optional(),
|
||||
limit: z.coerce.number().default(20),
|
||||
offset: z.coerce.number().default(0)
|
||||
@@ -66,13 +67,14 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
||||
userId: z.string().nullable().optional()
|
||||
})
|
||||
.array()
|
||||
}).array()
|
||||
}).array(),
|
||||
totalCount: z.number()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const approvals = await server.services.secretApprovalRequest.getSecretApprovals({
|
||||
const { approvals, totalCount } = await server.services.secretApprovalRequest.getSecretApprovals({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
@@ -80,7 +82,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
||||
...req.query,
|
||||
projectId: req.query.workspaceId
|
||||
});
|
||||
return { approvals };
|
||||
return { approvals, totalCount };
|
||||
}
|
||||
});
|
||||
|
||||
|
@@ -725,16 +725,17 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR
|
||||
)
|
||||
|
||||
.where(`${TableName.Environment}.projectId`, projectId)
|
||||
.where(`${TableName.AccessApprovalPolicy}.deletedAt`, null)
|
||||
.select(selectAllTableCols(TableName.AccessApprovalRequest))
|
||||
.select(db.ref("status").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerStatus"))
|
||||
.select(db.ref("reviewerUserId").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerUserId"));
|
||||
.select(db.ref("reviewerUserId").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerUserId"))
|
||||
.select(db.ref("deletedAt").withSchema(TableName.AccessApprovalPolicy).as("policyDeletedAt"));
|
||||
|
||||
const formattedRequests = sqlNestRelationships({
|
||||
data: accessRequests,
|
||||
key: "id",
|
||||
parentMapper: (doc) => ({
|
||||
...AccessApprovalRequestsSchema.parse(doc)
|
||||
...AccessApprovalRequestsSchema.parse(doc),
|
||||
isPolicyDeleted: Boolean(doc.policyDeletedAt)
|
||||
}),
|
||||
childrenMapper: [
|
||||
{
|
||||
@@ -751,7 +752,8 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR
|
||||
(req) =>
|
||||
!req.privilegeId &&
|
||||
!req.reviewers.some((r) => r.status === ApprovalStatus.REJECTED) &&
|
||||
req.status === ApprovalStatus.PENDING
|
||||
req.status === ApprovalStatus.PENDING &&
|
||||
!req.isPolicyDeleted
|
||||
);
|
||||
|
||||
// an approval is finalized if there are any rejections, a privilege ID is set or the number of approvals is equal to the number of approvals required.
|
||||
@@ -759,7 +761,8 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR
|
||||
(req) =>
|
||||
req.privilegeId ||
|
||||
req.reviewers.some((r) => r.status === ApprovalStatus.REJECTED) ||
|
||||
req.status !== ApprovalStatus.PENDING
|
||||
req.status !== ApprovalStatus.PENDING ||
|
||||
req.isPolicyDeleted
|
||||
);
|
||||
|
||||
return { pendingCount: pendingApprovals.length, finalizedCount: finalizedApprovals.length };
|
||||
|
@@ -275,7 +275,7 @@ export const accessApprovalRequestServiceFactory = ({
|
||||
|
||||
const listApprovalRequests: TAccessApprovalRequestServiceFactory["listApprovalRequests"] = async ({
|
||||
projectSlug,
|
||||
authorProjectMembershipId,
|
||||
authorUserId,
|
||||
envSlug,
|
||||
actor,
|
||||
actorOrgId,
|
||||
@@ -300,8 +300,8 @@ export const accessApprovalRequestServiceFactory = ({
|
||||
const policies = await accessApprovalPolicyDAL.find({ projectId: project.id });
|
||||
let requests = await accessApprovalRequestDAL.findRequestsWithPrivilegeByPolicyIds(policies.map((p) => p.id));
|
||||
|
||||
if (authorProjectMembershipId) {
|
||||
requests = requests.filter((request) => request.requestedByUserId === actorId);
|
||||
if (authorUserId) {
|
||||
requests = requests.filter((request) => request.requestedByUserId === authorUserId);
|
||||
}
|
||||
|
||||
if (envSlug) {
|
||||
|
@@ -31,7 +31,7 @@ export type TCreateAccessApprovalRequestDTO = {
|
||||
|
||||
export type TListApprovalRequestsDTO = {
|
||||
projectSlug: string;
|
||||
authorProjectMembershipId?: string;
|
||||
authorUserId?: string;
|
||||
envSlug?: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
|
@@ -24,6 +24,7 @@ type TFindQueryFilter = {
|
||||
committer?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
search?: string;
|
||||
};
|
||||
|
||||
export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
@@ -314,7 +315,6 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
.where(`${TableName.SecretApprovalPolicyApprover}.approverUserId`, userId)
|
||||
.orWhere(`${TableName.SecretApprovalRequest}.committerUserId`, userId)
|
||||
)
|
||||
.andWhere((bd) => void bd.where(`${TableName.SecretApprovalPolicy}.deletedAt`, null))
|
||||
.select("status", `${TableName.SecretApprovalRequest}.id`)
|
||||
.groupBy(`${TableName.SecretApprovalRequest}.id`, "status")
|
||||
.count("status")
|
||||
@@ -340,13 +340,13 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
};
|
||||
|
||||
const findByProjectId = async (
|
||||
{ status, limit = 20, offset = 0, projectId, committer, environment, userId }: TFindQueryFilter,
|
||||
{ status, limit = 20, offset = 0, projectId, committer, environment, userId, search }: TFindQueryFilter,
|
||||
tx?: Knex
|
||||
) => {
|
||||
try {
|
||||
// akhilmhdh: If ever u wanted a 1 to so many relationship connected with pagination
|
||||
// this is the place u wanna look at.
|
||||
const query = (tx || db.replicaNode())(TableName.SecretApprovalRequest)
|
||||
const innerQuery = (tx || db.replicaNode())(TableName.SecretApprovalRequest)
|
||||
.join(TableName.SecretFolder, `${TableName.SecretApprovalRequest}.folderId`, `${TableName.SecretFolder}.id`)
|
||||
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
|
||||
.join(
|
||||
@@ -435,7 +435,30 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
db.ref("firstName").withSchema("committerUser").as("committerUserFirstName"),
|
||||
db.ref("lastName").withSchema("committerUser").as("committerUserLastName")
|
||||
)
|
||||
.orderBy("createdAt", "desc");
|
||||
.distinctOn(`${TableName.SecretApprovalRequest}.id`)
|
||||
.as("inner");
|
||||
|
||||
const query = (tx || db)
|
||||
.select("*")
|
||||
.select(db.raw("count(*) OVER() as total_count"))
|
||||
.from(innerQuery)
|
||||
.orderBy("createdAt", "desc") as typeof innerQuery;
|
||||
|
||||
if (search) {
|
||||
void query.where((qb) => {
|
||||
void qb
|
||||
.whereRaw(`CONCAT_WS(' ', ??, ??) ilike ?`, [
|
||||
db.ref("firstName").withSchema("committerUser"),
|
||||
db.ref("lastName").withSchema("committerUser"),
|
||||
`%${search}%`
|
||||
])
|
||||
.orWhereRaw(`?? ilike ?`, [db.ref("username").withSchema("committerUser"), `%${search}%`])
|
||||
.orWhereRaw(`?? ilike ?`, [db.ref("email").withSchema("committerUser"), `%${search}%`])
|
||||
.orWhereILike(`${TableName.Environment}.name`, `%${search}%`)
|
||||
.orWhereILike(`${TableName.Environment}.slug`, `%${search}%`)
|
||||
.orWhereILike(`${TableName.SecretApprovalPolicy}.secretPath`, `%${search}%`);
|
||||
});
|
||||
}
|
||||
|
||||
const docs = await (tx || db)
|
||||
.with("w", query)
|
||||
@@ -443,6 +466,10 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
.from<Awaited<typeof query>[number]>("w")
|
||||
.where("w.rank", ">=", offset)
|
||||
.andWhere("w.rank", "<", offset + limit);
|
||||
|
||||
// @ts-expect-error knex does not infer
|
||||
const totalCount = Number(docs[0]?.total_count || 0);
|
||||
|
||||
const formattedDoc = sqlNestRelationships({
|
||||
data: docs,
|
||||
key: "id",
|
||||
@@ -504,23 +531,26 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
]
|
||||
});
|
||||
return formattedDoc.map((el) => ({
|
||||
return {
|
||||
approvals: formattedDoc.map((el) => ({
|
||||
...el,
|
||||
policy: { ...el.policy, approvers: el.approvers, bypassers: el.bypassers }
|
||||
}));
|
||||
})),
|
||||
totalCount
|
||||
};
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindSAR" });
|
||||
}
|
||||
};
|
||||
|
||||
const findByProjectIdBridgeSecretV2 = async (
|
||||
{ status, limit = 20, offset = 0, projectId, committer, environment, userId }: TFindQueryFilter,
|
||||
{ status, limit = 20, offset = 0, projectId, committer, environment, userId, search }: TFindQueryFilter,
|
||||
tx?: Knex
|
||||
) => {
|
||||
try {
|
||||
// akhilmhdh: If ever u wanted a 1 to so many relationship connected with pagination
|
||||
// this is the place u wanna look at.
|
||||
const query = (tx || db.replicaNode())(TableName.SecretApprovalRequest)
|
||||
const innerQuery = (tx || db.replicaNode())(TableName.SecretApprovalRequest)
|
||||
.join(TableName.SecretFolder, `${TableName.SecretApprovalRequest}.folderId`, `${TableName.SecretFolder}.id`)
|
||||
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
|
||||
.join(
|
||||
@@ -609,14 +639,42 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
db.ref("firstName").withSchema("committerUser").as("committerUserFirstName"),
|
||||
db.ref("lastName").withSchema("committerUser").as("committerUserLastName")
|
||||
)
|
||||
.orderBy("createdAt", "desc");
|
||||
.distinctOn(`${TableName.SecretApprovalRequest}.id`)
|
||||
.as("inner");
|
||||
|
||||
const query = (tx || db)
|
||||
.select("*")
|
||||
.select(db.raw("count(*) OVER() as total_count"))
|
||||
.from(innerQuery)
|
||||
.orderBy("createdAt", "desc") as typeof innerQuery;
|
||||
|
||||
if (search) {
|
||||
void query.where((qb) => {
|
||||
void qb
|
||||
.whereRaw(`CONCAT_WS(' ', ??, ??) ilike ?`, [
|
||||
db.ref("firstName").withSchema("committerUser"),
|
||||
db.ref("lastName").withSchema("committerUser"),
|
||||
`%${search}%`
|
||||
])
|
||||
.orWhereRaw(`?? ilike ?`, [db.ref("username").withSchema("committerUser"), `%${search}%`])
|
||||
.orWhereRaw(`?? ilike ?`, [db.ref("email").withSchema("committerUser"), `%${search}%`])
|
||||
.orWhereILike(`${TableName.Environment}.name`, `%${search}%`)
|
||||
.orWhereILike(`${TableName.Environment}.slug`, `%${search}%`)
|
||||
.orWhereILike(`${TableName.SecretApprovalPolicy}.secretPath`, `%${search}%`);
|
||||
});
|
||||
}
|
||||
|
||||
const rankOffset = offset + 1;
|
||||
const docs = await (tx || db)
|
||||
.with("w", query)
|
||||
.select("*")
|
||||
.from<Awaited<typeof query>[number]>("w")
|
||||
.where("w.rank", ">=", offset)
|
||||
.andWhere("w.rank", "<", offset + limit);
|
||||
.where("w.rank", ">=", rankOffset)
|
||||
.andWhere("w.rank", "<", rankOffset + limit);
|
||||
|
||||
// @ts-expect-error knex does not infer
|
||||
const totalCount = Number(docs[0]?.total_count || 0);
|
||||
|
||||
const formattedDoc = sqlNestRelationships({
|
||||
data: docs,
|
||||
key: "id",
|
||||
@@ -682,10 +740,13 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
]
|
||||
});
|
||||
return formattedDoc.map((el) => ({
|
||||
return {
|
||||
approvals: formattedDoc.map((el) => ({
|
||||
...el,
|
||||
policy: { ...el.policy, approvers: el.approvers, bypassers: el.bypassers }
|
||||
}));
|
||||
})),
|
||||
totalCount
|
||||
};
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindSAR" });
|
||||
}
|
||||
|
@@ -194,7 +194,8 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
environment,
|
||||
committer,
|
||||
limit,
|
||||
offset
|
||||
offset,
|
||||
search
|
||||
}: TListApprovalsDTO) => {
|
||||
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
|
||||
|
||||
@@ -208,6 +209,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
});
|
||||
|
||||
const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
|
||||
|
||||
if (shouldUseSecretV2Bridge) {
|
||||
return secretApprovalRequestDAL.findByProjectIdBridgeSecretV2({
|
||||
projectId,
|
||||
@@ -216,19 +218,21 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
status,
|
||||
userId: actorId,
|
||||
limit,
|
||||
offset
|
||||
offset,
|
||||
search
|
||||
});
|
||||
}
|
||||
const approvals = await secretApprovalRequestDAL.findByProjectId({
|
||||
|
||||
return secretApprovalRequestDAL.findByProjectId({
|
||||
projectId,
|
||||
committer,
|
||||
environment,
|
||||
status,
|
||||
userId: actorId,
|
||||
limit,
|
||||
offset
|
||||
offset,
|
||||
search
|
||||
});
|
||||
return approvals;
|
||||
};
|
||||
|
||||
const getSecretApprovalDetails = async ({
|
||||
|
@@ -93,6 +93,7 @@ export type TListApprovalsDTO = {
|
||||
committer?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
search?: string;
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TSecretApprovalDetailsDTO = {
|
||||
|
@@ -2020,16 +2020,10 @@ export const registerRoutes = async (
|
||||
if (licenseSyncJob) {
|
||||
cronJobs.push(licenseSyncJob);
|
||||
}
|
||||
|
||||
const microsoftTeamsSyncJob = await microsoftTeamsService.initializeBackgroundSync();
|
||||
if (microsoftTeamsSyncJob) {
|
||||
cronJobs.push(microsoftTeamsSyncJob);
|
||||
}
|
||||
|
||||
const adminIntegrationsSyncJob = await superAdminService.initializeAdminIntegrationConfigSync();
|
||||
if (adminIntegrationsSyncJob) {
|
||||
cronJobs.push(adminIntegrationsSyncJob);
|
||||
}
|
||||
}
|
||||
|
||||
server.decorate<FastifyZodProvider["store"]>("store", {
|
||||
|
@@ -37,12 +37,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
||||
encryptedSlackClientSecret: true,
|
||||
encryptedMicrosoftTeamsAppId: true,
|
||||
encryptedMicrosoftTeamsClientSecret: true,
|
||||
encryptedMicrosoftTeamsBotId: true,
|
||||
encryptedGitHubAppConnectionClientId: true,
|
||||
encryptedGitHubAppConnectionClientSecret: true,
|
||||
encryptedGitHubAppConnectionSlug: true,
|
||||
encryptedGitHubAppConnectionId: true,
|
||||
encryptedGitHubAppConnectionPrivateKey: true
|
||||
encryptedMicrosoftTeamsBotId: true
|
||||
}).extend({
|
||||
isMigrationModeOn: z.boolean(),
|
||||
defaultAuthOrgSlug: z.string().nullable(),
|
||||
@@ -92,11 +87,6 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
||||
microsoftTeamsAppId: z.string().optional(),
|
||||
microsoftTeamsClientSecret: z.string().optional(),
|
||||
microsoftTeamsBotId: z.string().optional(),
|
||||
gitHubAppConnectionClientId: z.string().optional(),
|
||||
gitHubAppConnectionClientSecret: z.string().optional(),
|
||||
gitHubAppConnectionSlug: z.string().optional(),
|
||||
gitHubAppConnectionId: z.string().optional(),
|
||||
gitHubAppConnectionPrivateKey: z.string().optional(),
|
||||
authConsentContent: z
|
||||
.string()
|
||||
.trim()
|
||||
@@ -358,13 +348,6 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
||||
appId: z.string(),
|
||||
clientSecret: z.string(),
|
||||
botId: z.string()
|
||||
}),
|
||||
gitHubAppConnection: z.object({
|
||||
clientId: z.string(),
|
||||
clientSecret: z.string(),
|
||||
appSlug: z.string(),
|
||||
appId: z.string(),
|
||||
privateKey: z.string()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@@ -7,7 +7,6 @@ import { request } from "@app/lib/config/request";
|
||||
import { BadRequestError, ForbiddenRequestError, InternalServerError } from "@app/lib/errors";
|
||||
import { getAppConnectionMethodName } from "@app/services/app-connection/app-connection-fns";
|
||||
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
||||
import { getInstanceIntegrationsConfig } from "@app/services/super-admin/super-admin-service";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import { GitHubConnectionMethod } from "./github-connection-enums";
|
||||
@@ -15,14 +14,13 @@ import { TGitHubConnection, TGitHubConnectionConfig } from "./github-connection-
|
||||
|
||||
export const getGitHubConnectionListItem = () => {
|
||||
const { INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID, INF_APP_CONNECTION_GITHUB_APP_SLUG } = getConfig();
|
||||
const { gitHubAppConnection } = getInstanceIntegrationsConfig();
|
||||
|
||||
return {
|
||||
name: "GitHub" as const,
|
||||
app: AppConnection.GitHub as const,
|
||||
methods: Object.values(GitHubConnectionMethod) as [GitHubConnectionMethod.App, GitHubConnectionMethod.OAuth],
|
||||
oauthClientId: INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID,
|
||||
appClientSlug: gitHubAppConnection.appSlug || INF_APP_CONNECTION_GITHUB_APP_SLUG
|
||||
appClientSlug: INF_APP_CONNECTION_GITHUB_APP_SLUG
|
||||
};
|
||||
};
|
||||
|
||||
@@ -32,24 +30,23 @@ export const getGitHubClient = (appConnection: TGitHubConnection) => {
|
||||
const { method, credentials } = appConnection;
|
||||
|
||||
let client: Octokit;
|
||||
const { gitHubAppConnection } = getInstanceIntegrationsConfig();
|
||||
|
||||
const appId = gitHubAppConnection.appId || appCfg.INF_APP_CONNECTION_GITHUB_APP_ID;
|
||||
const appPrivateKey = gitHubAppConnection.privateKey || appCfg.INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY;
|
||||
|
||||
switch (method) {
|
||||
case GitHubConnectionMethod.App:
|
||||
if (!appId || !appPrivateKey) {
|
||||
if (!appCfg.INF_APP_CONNECTION_GITHUB_APP_ID || !appCfg.INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY) {
|
||||
throw new InternalServerError({
|
||||
message: `GitHub ${getAppConnectionMethodName(method).replace("GitHub", "")} has not been configured`
|
||||
message: `GitHub ${getAppConnectionMethodName(method).replace(
|
||||
"GitHub",
|
||||
""
|
||||
)} environment variables have not been configured`
|
||||
});
|
||||
}
|
||||
|
||||
client = new Octokit({
|
||||
authStrategy: createAppAuth,
|
||||
auth: {
|
||||
appId,
|
||||
privateKey: appPrivateKey,
|
||||
appId: appCfg.INF_APP_CONNECTION_GITHUB_APP_ID,
|
||||
privateKey: appCfg.INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY,
|
||||
installationId: credentials.installationId
|
||||
}
|
||||
});
|
||||
@@ -157,8 +154,6 @@ type TokenRespData = {
|
||||
export const validateGitHubConnectionCredentials = async (config: TGitHubConnectionConfig) => {
|
||||
const { credentials, method } = config;
|
||||
|
||||
const { gitHubAppConnection } = getInstanceIntegrationsConfig();
|
||||
|
||||
const {
|
||||
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID,
|
||||
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_SECRET,
|
||||
@@ -170,8 +165,8 @@ export const validateGitHubConnectionCredentials = async (config: TGitHubConnect
|
||||
const { clientId, clientSecret } =
|
||||
method === GitHubConnectionMethod.App
|
||||
? {
|
||||
clientId: gitHubAppConnection.clientId || INF_APP_CONNECTION_GITHUB_APP_CLIENT_ID,
|
||||
clientSecret: gitHubAppConnection.clientSecret || INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET
|
||||
clientId: INF_APP_CONNECTION_GITHUB_APP_CLIENT_ID,
|
||||
clientSecret: INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET
|
||||
}
|
||||
: // oauth
|
||||
{
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import bcrypt from "bcrypt";
|
||||
import { CronJob } from "cron";
|
||||
import jwt from "jsonwebtoken";
|
||||
|
||||
import { IdentityAuthMethod, OrgMembershipRole, TSuperAdmin, TSuperAdminUpdate } from "@app/db/schemas";
|
||||
@@ -9,7 +8,6 @@ import { getConfig } from "@app/lib/config/env";
|
||||
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||
import { generateUserSrpKeys, getUserPrivateKey } from "@app/lib/crypto/srp";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { TIdentityDALFactory } from "@app/services/identity/identity-dal";
|
||||
|
||||
import { TAuthLoginFactory } from "../auth/auth-login-service";
|
||||
@@ -37,7 +35,6 @@ import {
|
||||
TAdminBootstrapInstanceDTO,
|
||||
TAdminGetIdentitiesDTO,
|
||||
TAdminGetUsersDTO,
|
||||
TAdminIntegrationConfig,
|
||||
TAdminSignUpDTO,
|
||||
TGetOrganizationsDTO
|
||||
} from "./super-admin-types";
|
||||
@@ -73,31 +70,6 @@ export let getServerCfg: () => Promise<
|
||||
}
|
||||
>;
|
||||
|
||||
let adminIntegrationsConfig: TAdminIntegrationConfig = {
|
||||
slack: {
|
||||
clientSecret: "",
|
||||
clientId: ""
|
||||
},
|
||||
microsoftTeams: {
|
||||
appId: "",
|
||||
clientSecret: "",
|
||||
botId: ""
|
||||
},
|
||||
gitHubAppConnection: {
|
||||
clientId: "",
|
||||
clientSecret: "",
|
||||
appSlug: "",
|
||||
appId: "",
|
||||
privateKey: ""
|
||||
}
|
||||
};
|
||||
|
||||
Object.freeze(adminIntegrationsConfig);
|
||||
|
||||
export const getInstanceIntegrationsConfig = () => {
|
||||
return adminIntegrationsConfig;
|
||||
};
|
||||
|
||||
const ADMIN_CONFIG_KEY = "infisical-admin-cfg";
|
||||
const ADMIN_CONFIG_KEY_EXP = 60; // 60s
|
||||
export const ADMIN_CONFIG_DB_UUID = "00000000-0000-0000-0000-000000000000";
|
||||
@@ -166,74 +138,6 @@ export const superAdminServiceFactory = ({
|
||||
return serverCfg;
|
||||
};
|
||||
|
||||
const getAdminIntegrationsConfig = async () => {
|
||||
const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
|
||||
|
||||
if (!serverCfg) {
|
||||
throw new NotFoundError({ name: "AdminConfig", message: "Admin config not found" });
|
||||
}
|
||||
|
||||
const decrypt = kmsService.decryptWithRootKey();
|
||||
|
||||
const slackClientId = serverCfg.encryptedSlackClientId ? decrypt(serverCfg.encryptedSlackClientId).toString() : "";
|
||||
const slackClientSecret = serverCfg.encryptedSlackClientSecret
|
||||
? decrypt(serverCfg.encryptedSlackClientSecret).toString()
|
||||
: "";
|
||||
|
||||
const microsoftAppId = serverCfg.encryptedMicrosoftTeamsAppId
|
||||
? decrypt(serverCfg.encryptedMicrosoftTeamsAppId).toString()
|
||||
: "";
|
||||
const microsoftClientSecret = serverCfg.encryptedMicrosoftTeamsClientSecret
|
||||
? decrypt(serverCfg.encryptedMicrosoftTeamsClientSecret).toString()
|
||||
: "";
|
||||
const microsoftBotId = serverCfg.encryptedMicrosoftTeamsBotId
|
||||
? decrypt(serverCfg.encryptedMicrosoftTeamsBotId).toString()
|
||||
: "";
|
||||
|
||||
const gitHubAppConnectionClientId = serverCfg.encryptedGitHubAppConnectionClientId
|
||||
? decrypt(serverCfg.encryptedGitHubAppConnectionClientId).toString()
|
||||
: "";
|
||||
const gitHubAppConnectionClientSecret = serverCfg.encryptedGitHubAppConnectionClientSecret
|
||||
? decrypt(serverCfg.encryptedGitHubAppConnectionClientSecret).toString()
|
||||
: "";
|
||||
|
||||
const gitHubAppConnectionAppSlug = serverCfg.encryptedGitHubAppConnectionSlug
|
||||
? decrypt(serverCfg.encryptedGitHubAppConnectionSlug).toString()
|
||||
: "";
|
||||
|
||||
const gitHubAppConnectionAppId = serverCfg.encryptedGitHubAppConnectionId
|
||||
? decrypt(serverCfg.encryptedGitHubAppConnectionId).toString()
|
||||
: "";
|
||||
const gitHubAppConnectionAppPrivateKey = serverCfg.encryptedGitHubAppConnectionPrivateKey
|
||||
? decrypt(serverCfg.encryptedGitHubAppConnectionPrivateKey).toString()
|
||||
: "";
|
||||
|
||||
return {
|
||||
slack: {
|
||||
clientSecret: slackClientSecret,
|
||||
clientId: slackClientId
|
||||
},
|
||||
microsoftTeams: {
|
||||
appId: microsoftAppId,
|
||||
clientSecret: microsoftClientSecret,
|
||||
botId: microsoftBotId
|
||||
},
|
||||
gitHubAppConnection: {
|
||||
clientId: gitHubAppConnectionClientId,
|
||||
clientSecret: gitHubAppConnectionClientSecret,
|
||||
appSlug: gitHubAppConnectionAppSlug,
|
||||
appId: gitHubAppConnectionAppId,
|
||||
privateKey: gitHubAppConnectionAppPrivateKey
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const $syncAdminIntegrationConfig = async () => {
|
||||
const config = await getAdminIntegrationsConfig();
|
||||
Object.freeze(config);
|
||||
adminIntegrationsConfig = config;
|
||||
};
|
||||
|
||||
const updateServerCfg = async (
|
||||
data: TSuperAdminUpdate & {
|
||||
slackClientId?: string;
|
||||
@@ -241,11 +145,6 @@ export const superAdminServiceFactory = ({
|
||||
microsoftTeamsAppId?: string;
|
||||
microsoftTeamsClientSecret?: string;
|
||||
microsoftTeamsBotId?: string;
|
||||
gitHubAppConnectionClientId?: string;
|
||||
gitHubAppConnectionClientSecret?: string;
|
||||
gitHubAppConnectionSlug?: string;
|
||||
gitHubAppConnectionId?: string;
|
||||
gitHubAppConnectionPrivateKey?: string;
|
||||
},
|
||||
userId: string
|
||||
) => {
|
||||
@@ -337,51 +236,10 @@ export const superAdminServiceFactory = ({
|
||||
updatedData.microsoftTeamsBotId = undefined;
|
||||
microsoftTeamsSettingsUpdated = true;
|
||||
}
|
||||
|
||||
let gitHubAppConnectionSettingsUpdated = false;
|
||||
if (data.gitHubAppConnectionClientId !== undefined) {
|
||||
const encryptedClientId = encryptWithRoot(Buffer.from(data.gitHubAppConnectionClientId));
|
||||
updatedData.encryptedGitHubAppConnectionClientId = encryptedClientId;
|
||||
updatedData.gitHubAppConnectionClientId = undefined;
|
||||
gitHubAppConnectionSettingsUpdated = true;
|
||||
}
|
||||
|
||||
if (data.gitHubAppConnectionClientSecret !== undefined) {
|
||||
const encryptedClientSecret = encryptWithRoot(Buffer.from(data.gitHubAppConnectionClientSecret));
|
||||
updatedData.encryptedGitHubAppConnectionClientSecret = encryptedClientSecret;
|
||||
updatedData.gitHubAppConnectionClientSecret = undefined;
|
||||
gitHubAppConnectionSettingsUpdated = true;
|
||||
}
|
||||
|
||||
if (data.gitHubAppConnectionSlug !== undefined) {
|
||||
const encryptedAppSlug = encryptWithRoot(Buffer.from(data.gitHubAppConnectionSlug));
|
||||
updatedData.encryptedGitHubAppConnectionSlug = encryptedAppSlug;
|
||||
updatedData.gitHubAppConnectionSlug = undefined;
|
||||
gitHubAppConnectionSettingsUpdated = true;
|
||||
}
|
||||
|
||||
if (data.gitHubAppConnectionId !== undefined) {
|
||||
const encryptedAppId = encryptWithRoot(Buffer.from(data.gitHubAppConnectionId));
|
||||
updatedData.encryptedGitHubAppConnectionId = encryptedAppId;
|
||||
updatedData.gitHubAppConnectionId = undefined;
|
||||
gitHubAppConnectionSettingsUpdated = true;
|
||||
}
|
||||
|
||||
if (data.gitHubAppConnectionPrivateKey !== undefined) {
|
||||
const encryptedAppPrivateKey = encryptWithRoot(Buffer.from(data.gitHubAppConnectionPrivateKey));
|
||||
updatedData.encryptedGitHubAppConnectionPrivateKey = encryptedAppPrivateKey;
|
||||
updatedData.gitHubAppConnectionPrivateKey = undefined;
|
||||
gitHubAppConnectionSettingsUpdated = true;
|
||||
}
|
||||
|
||||
const updatedServerCfg = await serverCfgDAL.updateById(ADMIN_CONFIG_DB_UUID, updatedData);
|
||||
|
||||
await keyStore.setItemWithExpiry(ADMIN_CONFIG_KEY, ADMIN_CONFIG_KEY_EXP, JSON.stringify(updatedServerCfg));
|
||||
|
||||
if (gitHubAppConnectionSettingsUpdated) {
|
||||
await $syncAdminIntegrationConfig();
|
||||
}
|
||||
|
||||
if (
|
||||
updatedServerCfg.encryptedMicrosoftTeamsAppId &&
|
||||
updatedServerCfg.encryptedMicrosoftTeamsClientSecret &&
|
||||
@@ -735,6 +593,43 @@ export const superAdminServiceFactory = ({
|
||||
await userDAL.updateById(userId, { superAdmin: true });
|
||||
};
|
||||
|
||||
const getAdminIntegrationsConfig = async () => {
|
||||
const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
|
||||
|
||||
if (!serverCfg) {
|
||||
throw new NotFoundError({ name: "AdminConfig", message: "Admin config not found" });
|
||||
}
|
||||
|
||||
const decrypt = kmsService.decryptWithRootKey();
|
||||
|
||||
const slackClientId = serverCfg.encryptedSlackClientId ? decrypt(serverCfg.encryptedSlackClientId).toString() : "";
|
||||
const slackClientSecret = serverCfg.encryptedSlackClientSecret
|
||||
? decrypt(serverCfg.encryptedSlackClientSecret).toString()
|
||||
: "";
|
||||
|
||||
const microsoftAppId = serverCfg.encryptedMicrosoftTeamsAppId
|
||||
? decrypt(serverCfg.encryptedMicrosoftTeamsAppId).toString()
|
||||
: "";
|
||||
const microsoftClientSecret = serverCfg.encryptedMicrosoftTeamsClientSecret
|
||||
? decrypt(serverCfg.encryptedMicrosoftTeamsClientSecret).toString()
|
||||
: "";
|
||||
const microsoftBotId = serverCfg.encryptedMicrosoftTeamsBotId
|
||||
? decrypt(serverCfg.encryptedMicrosoftTeamsBotId).toString()
|
||||
: "";
|
||||
|
||||
return {
|
||||
slack: {
|
||||
clientSecret: slackClientSecret,
|
||||
clientId: slackClientId
|
||||
},
|
||||
microsoftTeams: {
|
||||
appId: microsoftAppId,
|
||||
clientSecret: microsoftClientSecret,
|
||||
botId: microsoftBotId
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const getConfiguredEncryptionStrategies = async () => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
@@ -801,19 +696,6 @@ export const superAdminServiceFactory = ({
|
||||
return (await keyStore.getItem("invalidating-cache")) !== null;
|
||||
};
|
||||
|
||||
const initializeAdminIntegrationConfigSync = async () => {
|
||||
logger.info("Setting up background sync process for admin integrations config");
|
||||
|
||||
// initial sync upon startup
|
||||
await $syncAdminIntegrationConfig();
|
||||
|
||||
// sync admin integrations config every 5 minutes
|
||||
const job = new CronJob("*/5 * * * *", $syncAdminIntegrationConfig);
|
||||
job.start();
|
||||
|
||||
return job;
|
||||
};
|
||||
|
||||
return {
|
||||
initServerCfg,
|
||||
updateServerCfg,
|
||||
@@ -832,7 +714,6 @@ export const superAdminServiceFactory = ({
|
||||
checkIfInvalidatingCache,
|
||||
getOrganizations,
|
||||
deleteOrganization,
|
||||
deleteOrganizationMembership,
|
||||
initializeAdminIntegrationConfigSync
|
||||
deleteOrganizationMembership
|
||||
};
|
||||
};
|
||||
|
@@ -55,22 +55,3 @@ export enum CacheType {
|
||||
ALL = "all",
|
||||
SECRETS = "secrets"
|
||||
}
|
||||
|
||||
export type TAdminIntegrationConfig = {
|
||||
slack: {
|
||||
clientSecret: string;
|
||||
clientId: string;
|
||||
};
|
||||
microsoftTeams: {
|
||||
appId: string;
|
||||
clientSecret: string;
|
||||
botId: string;
|
||||
};
|
||||
gitHubAppConnection: {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
appSlug: string;
|
||||
appId: string;
|
||||
privateKey: string;
|
||||
};
|
||||
};
|
||||
|
2230
docs/docs.json
Normal file
2230
docs/docs.json
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Before Width: | Height: | Size: 610 KiB |
@@ -53,22 +53,7 @@ Infisical supports two methods for connecting to GitHub.
|
||||
Obtain the necessary Github application credentials. This would be the application slug, client ID, app ID, client secret, and private key.
|
||||

|
||||
|
||||
Back in your Infisical instance, you can configure the GitHub App credentials in one of two ways:
|
||||
|
||||
**Option 1: Server Admin Panel (Recommended)**
|
||||
|
||||
Navigate to the server admin panel > **Integrations** > **GitHub App** and enter the GitHub application credentials:
|
||||

|
||||
|
||||
- **Client ID**: The Client ID of your GitHub application
|
||||
- **Client Secret**: The Client Secret of your GitHub application
|
||||
- **App Slug**: The Slug of your GitHub application (found in the URL)
|
||||
- **App ID**: The App ID of your GitHub application
|
||||
- **Private Key**: The Private Key of your GitHub application
|
||||
|
||||
**Option 2: Environment Variables**
|
||||
|
||||
Alternatively, you can add the new environment variables for the credentials of your GitHub application:
|
||||
Back in your Infisical instance, add the five new environment variables for the credentials of your GitHub application:
|
||||
|
||||
- `INF_APP_CONNECTION_GITHUB_APP_CLIENT_ID`: The **Client ID** of your GitHub application.
|
||||
- `INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET`: The **Client Secret** of your GitHub application.
|
||||
@@ -76,7 +61,7 @@ Infisical supports two methods for connecting to GitHub.
|
||||
- `INF_APP_CONNECTION_GITHUB_APP_ID`: The **App ID** of your GitHub application.
|
||||
- `INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY`: The **Private Key** of your GitHub application.
|
||||
|
||||
Once configured, you can use the GitHub integration via app authentication. If you configured the credentials using environment variables, restart your Infisical instance for the changes to take effect. If you configured them through the server admin panel, allow approximately 5 minutes for the changes to propagate.
|
||||
Once added, restart your Infisical instance and use the GitHub integration via app authentication.
|
||||
</Step>
|
||||
</Steps>
|
||||
</Accordion>
|
||||
@@ -173,5 +158,4 @@ Infisical supports two methods for connecting to GitHub.
|
||||
</Step>
|
||||
</Steps>
|
||||
</Tab>
|
||||
|
||||
</Tabs>
|
||||
|
128
docs/internals/architecture/cloud.mdx
Normal file
128
docs/internals/architecture/cloud.mdx
Normal file
@@ -0,0 +1,128 @@
|
||||
---
|
||||
title: "Infisical Cloud"
|
||||
description: "Architecture overview of Infisical's US and EU cloud deployments"
|
||||
---
|
||||
|
||||
This document provides an overview of Infisical's cloud architecture for our US and EU deployments, detailing the core components and how they interact to provide security and infrastructure services.
|
||||
|
||||
## Overview
|
||||
|
||||
Infisical Cloud operates on AWS infrastructure using containerized services deployed via Amazon ECS (Elastic Container Service). Our US and EU deployments use identical architectural patterns to ensure consistency and reliability across regions.
|
||||
|
||||

|
||||
|
||||
## Components
|
||||
|
||||
A typical Infisical Cloud deployment consists of the following components:
|
||||
|
||||
### Application Services
|
||||
|
||||
- **Infisical Core**: Main application server running the Infisical backend API
|
||||
- **License API**: Dedicated API service for license management with separate RDS instance (shared between US/EU)
|
||||
- **Application Load Balancer**: Routes incoming traffic to application containers with SSL termination and host-based routing
|
||||
|
||||
### Data Layer
|
||||
|
||||
- **Amazon RDS (PostgreSQL)**:
|
||||
- **Main RDS Instance**: Primary database for secrets, users, and metadata (Multi-AZ, encryption enabled)
|
||||
- **License API RDS Instance**: Dedicated database for license management services
|
||||
- **Amazon ElastiCache (Redis)**:
|
||||
- **Main Redis Cluster**: Multi-AZ replication group for core application caching and queuing
|
||||
- **License API Redis**: Dedicated cache for license services
|
||||
- Redis 7 engine with CloudWatch logging and snapshot backups
|
||||
|
||||
### Infrastructure
|
||||
|
||||
- **ECS Fargate**: Serverless container platform running application services
|
||||
- **AWS Global Accelerator**: Global traffic routing and performance optimization
|
||||
- **Cloudflare**: DNS management and routing
|
||||
- **AWS SSM Parameter Store**: Stores application configuration and secrets
|
||||
- **CloudWatch**: Centralized logging and monitoring
|
||||
|
||||
## System Layout
|
||||
|
||||
### Service Architecture
|
||||
|
||||
The Infisical application runs as multiple containerized services on ECS:
|
||||
|
||||
- **Main Server**: Auto-scaling containerized application services
|
||||
- **License API**: Dedicated service with separate infrastructure (shared globally)
|
||||
- **Monitoring**: AWS OTel Collector and Datadog Agent sidecars
|
||||
|
||||
Container images are pulled from Docker Hub and managed via GitHub Actions for deployments.
|
||||
|
||||
### Network Configuration
|
||||
|
||||
Services are deployed in private subnets with the following connectivity:
|
||||
|
||||
- External traffic → Application Load Balancer → ECS Services
|
||||
- Main server exposes port 8080
|
||||
- License API exposes port 4000 (portal.infisical.com, license.infisical.com)
|
||||
- Service-to-service communication via AWS Service Connect
|
||||
|
||||
### Data Flow
|
||||
|
||||
1. **DNS resolution** via Cloudflare routes traffic to AWS Global Accelerator
|
||||
2. **Global Accelerator** optimizes routing to the nearest AWS region
|
||||
3. **Client requests** are routed through the Application Load Balancer to ECS containers
|
||||
4. **Application logic** processes requests in the Infisical Core service
|
||||
5. **Data persistence** occurs via encrypted connections to RDS
|
||||
6. **Caching** utilizes ElastiCache for performance optimization
|
||||
7. **Configuration** is retrieved from AWS SSM Parameter Store
|
||||
|
||||
## Regional Deployments
|
||||
|
||||
Each region operates in a separate AWS account, providing strong isolation boundaries for security, compliance, and operational independence.
|
||||
|
||||
### US Cloud (us.infisical.com or app.infisical.com)
|
||||
|
||||
- **AWS Account**: Dedicated US AWS account
|
||||
- **Infrastructure**: ECS-based containerized deployment
|
||||
- **Monitoring**: Integrated with Datadog for observability and security monitoring
|
||||
|
||||
### EU Cloud (eu.infisical.com)
|
||||
|
||||
- **AWS Account**: Dedicated EU AWS account
|
||||
- **Infrastructure**: ECS-based containerized deployment
|
||||
- **Monitoring**: Integrated with Datadog for observability and security monitoring
|
||||
|
||||
## Configuration Management
|
||||
|
||||
Application configuration and secrets are managed through AWS SSM Parameter Store, with deployment automation handled via GitHub Actions.
|
||||
|
||||
## Monitoring and Observability
|
||||
|
||||
### Logging
|
||||
|
||||
- **CloudWatch**: 365-day retention for application logs
|
||||
- **Health Checks**: HTTP endpoint monitoring for service health
|
||||
|
||||
### Metrics
|
||||
|
||||
- **AWS OTel Collector**: Prometheus metrics collection
|
||||
- **Datadog Agent**: Application performance monitoring and infrastructure metrics
|
||||
|
||||
## Container Management
|
||||
|
||||
- **Images**: `infisical/staging_infisical` and `infisical/license-api` from Docker Hub
|
||||
- **Deployment**: Automated via GitHub Actions updating SSM parameter for image tags
|
||||
- **Registry Access**: Docker Hub credentials stored in AWS Secrets Manager
|
||||
- **Platform**: ECS Fargate serverless container platform
|
||||
|
||||
## Security Overview
|
||||
|
||||
### Data Protection
|
||||
|
||||
- **Encryption**: All secrets encrypted at rest and in transit
|
||||
- **Network Isolation**: Services deployed in private subnets with controlled access
|
||||
- **Authentication**: API tokens and service accounts for secure access
|
||||
- **Audit Logging**: Comprehensive audit trails for all secret operations
|
||||
|
||||
### Network Architecture
|
||||
|
||||
- **VPC Design**: Dedicated VPC with public and private subnets across multiple Availability Zones
|
||||
- **NAT Gateway**: Controlled outbound connectivity from private subnets
|
||||
- **Load Balancing**: Application Load Balancer with SSL termination and health checks
|
||||
- **Security Groups**: Restrictive firewall rules and controlled network access
|
||||
- **High Availability**: Multi-AZ deployment with automatic failover
|
||||
- **Network Monitoring**: VPC Flow Logs with 365-day retention for traffic analysis
|
2215
docs/mint.json
2215
docs/mint.json
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,7 @@
|
||||
* {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
#navbar .max-w-8xl {
|
||||
max-width: 100%;
|
||||
border-bottom: 1px solid #ebebeb;
|
||||
@@ -26,24 +30,20 @@
|
||||
}
|
||||
|
||||
#sidebar li > div.mt-2 {
|
||||
border-radius: 0;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
#sidebar li > a.text-primary {
|
||||
border-radius: 0;
|
||||
background-color: #FBFFCC;
|
||||
border-left: 4px solid #EFFF33;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
#sidebar li > a.mt-2 {
|
||||
border-radius: 0;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
#sidebar li > a.leading-6 {
|
||||
border-radius: 0;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
@@ -68,65 +68,26 @@
|
||||
}
|
||||
|
||||
#content-area .mt-8 .block{
|
||||
border-radius: 0;
|
||||
border-width: 1px;
|
||||
background-color: #FCFBFA;
|
||||
border-color: #ebebeb;
|
||||
}
|
||||
|
||||
/* #content-area:hover .mt-8 .block:hover{
|
||||
border-radius: 0;
|
||||
|
||||
border-width: 1px;
|
||||
background-color: #FDFFE5;
|
||||
border-color: #EFFF33;
|
||||
} */
|
||||
|
||||
#content-area .mt-8 .rounded-xl{
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
#content-area .mt-8 .rounded-lg{
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
#content-area .mt-6 .rounded-xl{
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
#content-area .mt-6 .rounded-lg{
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
#content-area .mt-6 .rounded-md{
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
#content-area .mt-8 .rounded-md{
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
#content-area div.my-4{
|
||||
border-radius: 0;
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
#content-area div.flex-1 {
|
||||
/* text-transform: uppercase; */
|
||||
/* #content-area div.flex-1 {
|
||||
opacity: 0.8;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
#content-area button {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
#content-area a {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
#content-area .not-prose {
|
||||
border-radius: 0;
|
||||
}
|
||||
} */
|
||||
|
||||
/* .eyebrow {
|
||||
text-transform: uppercase;
|
||||
|
@@ -94,7 +94,7 @@ export const DropdownMenuItem = <T extends ElementType = "button">({
|
||||
className={twMerge(
|
||||
"block cursor-pointer rounded-sm px-4 py-2 font-inter text-xs text-mineshaft-200 outline-none data-[highlighted]:bg-mineshaft-700",
|
||||
className,
|
||||
isDisabled ? "pointer-events-none opacity-50" : ""
|
||||
isDisabled ? "pointer-events-none cursor-not-allowed opacity-50" : ""
|
||||
)}
|
||||
>
|
||||
<Item type="button" role="menuitem" className="flex w-full items-center" ref={inputRef}>
|
||||
|
@@ -16,12 +16,6 @@ export const ROUTE_PATHS = Object.freeze({
|
||||
PasswordResetPage: setRoute("/password-reset", "/_restrict-login-signup/password-reset"),
|
||||
PasswordSetupPage: setRoute("/password-setup", "/_authenticate/password-setup")
|
||||
},
|
||||
Admin: {
|
||||
IntegrationsPage: setRoute(
|
||||
"/admin/integrations",
|
||||
"/_authenticate/_inject-org-details/admin/_admin-layout/integrations"
|
||||
)
|
||||
},
|
||||
Organization: {
|
||||
Settings: {
|
||||
OauthCallbackPage: setRoute(
|
||||
|
@@ -1,12 +1,20 @@
|
||||
import { IconDefinition } from "@fortawesome/free-brands-svg-icons";
|
||||
import { faArrowRightToBracket, faEdit } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import { PolicyType } from "@app/hooks/api/policies/enums";
|
||||
|
||||
export const policyDetails: Record<PolicyType, { name: string; className: string }> = {
|
||||
export const policyDetails: Record<
|
||||
PolicyType,
|
||||
{ name: string; className: string; icon: IconDefinition }
|
||||
> = {
|
||||
[PolicyType.AccessPolicy]: {
|
||||
className: "bg-lime-900 text-lime-100",
|
||||
name: "Access Policy"
|
||||
className: "bg-green/20 text-green",
|
||||
name: "Access Policy",
|
||||
icon: faArrowRightToBracket
|
||||
},
|
||||
[PolicyType.ChangePolicy]: {
|
||||
className: "bg-indigo-900 text-indigo-100",
|
||||
name: "Change Policy"
|
||||
className: "bg-yellow/20 text-yellow",
|
||||
name: "Change Policy",
|
||||
icon: faEdit
|
||||
}
|
||||
};
|
||||
|
@@ -65,11 +65,11 @@ const fetchApprovalPolicies = async ({ projectSlug }: TGetAccessApprovalRequests
|
||||
const fetchApprovalRequests = async ({
|
||||
projectSlug,
|
||||
envSlug,
|
||||
authorProjectMembershipId
|
||||
authorUserId
|
||||
}: TGetAccessApprovalRequestsDTO) => {
|
||||
const { data } = await apiRequest.get<{ requests: TAccessApprovalRequest[] }>(
|
||||
"/api/v1/access-approvals/requests",
|
||||
{ params: { projectSlug, envSlug, authorProjectMembershipId } }
|
||||
{ params: { projectSlug, envSlug, authorUserId } }
|
||||
);
|
||||
|
||||
return data.requests.map((request) => ({
|
||||
@@ -109,12 +109,12 @@ export const useGetAccessRequestsCount = ({
|
||||
export const useGetAccessApprovalPolicies = ({
|
||||
projectSlug,
|
||||
envSlug,
|
||||
authorProjectMembershipId,
|
||||
authorUserId,
|
||||
options = {}
|
||||
}: TGetAccessApprovalRequestsDTO & TReactQueryOptions) =>
|
||||
useQuery({
|
||||
queryKey: accessApprovalKeys.getAccessApprovalPolicies(projectSlug),
|
||||
queryFn: () => fetchApprovalPolicies({ projectSlug, envSlug, authorProjectMembershipId }),
|
||||
queryFn: () => fetchApprovalPolicies({ projectSlug, envSlug, authorUserId }),
|
||||
...options,
|
||||
enabled: Boolean(projectSlug) && (options?.enabled ?? true)
|
||||
});
|
||||
@@ -122,16 +122,13 @@ export const useGetAccessApprovalPolicies = ({
|
||||
export const useGetAccessApprovalRequests = ({
|
||||
projectSlug,
|
||||
envSlug,
|
||||
authorProjectMembershipId,
|
||||
authorUserId,
|
||||
options = {}
|
||||
}: TGetAccessApprovalRequestsDTO & TReactQueryOptions) =>
|
||||
useQuery({
|
||||
queryKey: accessApprovalKeys.getAccessApprovalRequests(
|
||||
projectSlug,
|
||||
envSlug,
|
||||
authorProjectMembershipId
|
||||
),
|
||||
queryFn: () => fetchApprovalRequests({ projectSlug, envSlug, authorProjectMembershipId }),
|
||||
queryKey: accessApprovalKeys.getAccessApprovalRequests(projectSlug, envSlug, authorUserId),
|
||||
queryFn: () => fetchApprovalRequests({ projectSlug, envSlug, authorUserId }),
|
||||
...options,
|
||||
enabled: Boolean(projectSlug) && (options?.enabled ?? true)
|
||||
enabled: Boolean(projectSlug) && (options?.enabled ?? true),
|
||||
placeholderData: (previousData) => previousData
|
||||
});
|
||||
|
@@ -148,7 +148,7 @@ export type TCreateAccessRequestDTO = {
|
||||
export type TGetAccessApprovalRequestsDTO = {
|
||||
projectSlug: string;
|
||||
envSlug?: string;
|
||||
authorProjectMembershipId?: string;
|
||||
authorUserId?: string;
|
||||
};
|
||||
|
||||
export type TGetAccessPolicyApprovalCountDTO = {
|
||||
|
@@ -56,11 +56,6 @@ export type TUpdateServerConfigDTO = {
|
||||
microsoftTeamsAppId?: string;
|
||||
microsoftTeamsClientSecret?: string;
|
||||
microsoftTeamsBotId?: string;
|
||||
gitHubAppConnectionClientId?: string;
|
||||
gitHubAppConnectionClientSecret?: string;
|
||||
gitHubAppConnectionSlug?: string;
|
||||
gitHubAppConnectionId?: string;
|
||||
gitHubAppConnectionPrivateKey?: string;
|
||||
} & Partial<TServerConfig>;
|
||||
|
||||
export type TCreateAdminUserDTO = {
|
||||
@@ -105,13 +100,6 @@ export type AdminIntegrationsConfig = {
|
||||
clientSecret: string;
|
||||
botId: string;
|
||||
};
|
||||
gitHubAppConnection: {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
appSlug: string;
|
||||
appId: string;
|
||||
privateKey: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type TGetServerRootKmsEncryptionDetails = {
|
||||
|
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import { useInfiniteQuery, useQuery, UseQueryOptions } from "@tanstack/react-query";
|
||||
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
|
||||
|
||||
import {
|
||||
decryptAssymmetric,
|
||||
@@ -25,10 +25,11 @@ export const secretApprovalRequestKeys = {
|
||||
status,
|
||||
committer,
|
||||
offset,
|
||||
limit
|
||||
limit,
|
||||
search
|
||||
}: TGetSecretApprovalRequestList) =>
|
||||
[
|
||||
{ workspaceId, environment, status, committer, offset, limit },
|
||||
{ workspaceId, environment, status, committer, offset, limit, search },
|
||||
"secret-approval-requests"
|
||||
] as const,
|
||||
detail: ({ id }: Omit<TGetSecretApprovalRequestDetails, "decryptKey">) =>
|
||||
@@ -118,23 +119,25 @@ const fetchSecretApprovalRequestList = async ({
|
||||
committer,
|
||||
status = "open",
|
||||
limit = 20,
|
||||
offset
|
||||
offset = 0,
|
||||
search = ""
|
||||
}: TGetSecretApprovalRequestList) => {
|
||||
const { data } = await apiRequest.get<{ approvals: TSecretApprovalRequest[] }>(
|
||||
"/api/v1/secret-approval-requests",
|
||||
{
|
||||
const { data } = await apiRequest.get<{
|
||||
approvals: TSecretApprovalRequest[];
|
||||
totalCount: number;
|
||||
}>("/api/v1/secret-approval-requests", {
|
||||
params: {
|
||||
workspaceId,
|
||||
environment,
|
||||
committer,
|
||||
status,
|
||||
limit,
|
||||
offset
|
||||
offset,
|
||||
search
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
return data.approvals;
|
||||
return data;
|
||||
};
|
||||
|
||||
export const useGetSecretApprovalRequests = ({
|
||||
@@ -143,31 +146,32 @@ export const useGetSecretApprovalRequests = ({
|
||||
options = {},
|
||||
status,
|
||||
limit = 20,
|
||||
offset = 0,
|
||||
search,
|
||||
committer
|
||||
}: TGetSecretApprovalRequestList & TReactQueryOptions) =>
|
||||
useInfiniteQuery({
|
||||
initialPageParam: 0,
|
||||
useQuery({
|
||||
queryKey: secretApprovalRequestKeys.list({
|
||||
workspaceId,
|
||||
environment,
|
||||
committer,
|
||||
status
|
||||
status,
|
||||
limit,
|
||||
search,
|
||||
offset
|
||||
}),
|
||||
queryFn: ({ pageParam }) =>
|
||||
queryFn: () =>
|
||||
fetchSecretApprovalRequestList({
|
||||
workspaceId,
|
||||
environment,
|
||||
status,
|
||||
committer,
|
||||
limit,
|
||||
offset: pageParam
|
||||
offset,
|
||||
search
|
||||
}),
|
||||
enabled: Boolean(workspaceId) && (options?.enabled ?? true),
|
||||
getNextPageParam: (lastPage, pages) => {
|
||||
if (lastPage.length && lastPage.length < limit) return undefined;
|
||||
|
||||
return lastPage?.length !== 0 ? pages.length * limit : undefined;
|
||||
}
|
||||
placeholderData: (previousData) => previousData
|
||||
});
|
||||
|
||||
const fetchSecretApprovalRequestDetails = async ({
|
||||
|
@@ -113,6 +113,7 @@ export type TGetSecretApprovalRequestList = {
|
||||
committer?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
search?: string;
|
||||
};
|
||||
|
||||
export type TGetSecretApprovalRequestCount = {
|
||||
|
@@ -352,9 +352,9 @@ export const ProjectLayout = () => {
|
||||
secretApprovalReqCount?.open ||
|
||||
accessApprovalRequestCount?.pendingCount
|
||||
) && (
|
||||
<span className="ml-2 rounded border border-primary-400 bg-primary-600 px-1 py-0.5 text-xs font-semibold text-black">
|
||||
<Badge variant="primary" className="ml-1.5">
|
||||
{pendingRequestsCount}
|
||||
</span>
|
||||
</Badge>
|
||||
)}
|
||||
</MenuItem>
|
||||
)}
|
||||
|
@@ -1,222 +0,0 @@
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { FaGithub } from "react-icons/fa";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
Button,
|
||||
FormControl,
|
||||
Input,
|
||||
TextArea
|
||||
} from "@app/components/v2";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { useUpdateServerConfig } from "@app/hooks/api";
|
||||
import { AdminIntegrationsConfig } from "@app/hooks/api/admin/types";
|
||||
|
||||
const gitHubAppFormSchema = z.object({
|
||||
clientId: z.string(),
|
||||
clientSecret: z.string(),
|
||||
appSlug: z.string(),
|
||||
appId: z.string(),
|
||||
privateKey: z.string()
|
||||
});
|
||||
|
||||
type TGitHubAppConnectionForm = z.infer<typeof gitHubAppFormSchema>;
|
||||
|
||||
type Props = {
|
||||
adminIntegrationsConfig?: AdminIntegrationsConfig;
|
||||
};
|
||||
|
||||
export const GitHubAppConnectionForm = ({ adminIntegrationsConfig }: Props) => {
|
||||
const { mutateAsync: updateAdminServerConfig } = useUpdateServerConfig();
|
||||
const [isGitHubAppClientSecretFocused, setIsGitHubAppClientSecretFocused] = useToggle();
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
formState: { isSubmitting, isDirty }
|
||||
} = useForm<TGitHubAppConnectionForm>({
|
||||
resolver: zodResolver(gitHubAppFormSchema)
|
||||
});
|
||||
|
||||
const onSubmit = async (data: TGitHubAppConnectionForm) => {
|
||||
await updateAdminServerConfig({
|
||||
gitHubAppConnectionClientId: data.clientId,
|
||||
gitHubAppConnectionClientSecret: data.clientSecret,
|
||||
gitHubAppConnectionSlug: data.appSlug,
|
||||
gitHubAppConnectionId: data.appId,
|
||||
gitHubAppConnectionPrivateKey: data.privateKey
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Updated GitHub app connection configuration. It can take up to 5 minutes to take effect.",
|
||||
type: "success"
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (adminIntegrationsConfig) {
|
||||
setValue("clientId", adminIntegrationsConfig.gitHubAppConnection.clientId);
|
||||
setValue("clientSecret", adminIntegrationsConfig.gitHubAppConnection.clientSecret);
|
||||
setValue("appSlug", adminIntegrationsConfig.gitHubAppConnection.appSlug);
|
||||
setValue("appId", adminIntegrationsConfig.gitHubAppConnection.appId);
|
||||
setValue("privateKey", adminIntegrationsConfig.gitHubAppConnection.privateKey);
|
||||
}
|
||||
}, [adminIntegrationsConfig]);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Accordion type="single" collapsible className="w-full">
|
||||
<AccordionItem value="github-app-integration" className="data-[state=open]:border-none">
|
||||
<AccordionTrigger className="flex h-fit w-full justify-start rounded-md border border-mineshaft-500 bg-mineshaft-700 px-4 py-6 text-sm transition-colors data-[state=open]:rounded-b-none">
|
||||
<div className="text-md group order-1 ml-3 flex items-center gap-2">
|
||||
<FaGithub className="text-lg group-hover:text-primary-400" />
|
||||
<div className="text-[15px] font-semibold">GitHub App</div>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent childrenClassName="px-0 py-0">
|
||||
<div className="flex w-full flex-col justify-start rounded-md rounded-t-none border border-t-0 border-mineshaft-500 bg-mineshaft-700 px-4 py-4">
|
||||
<div className="mb-2 max-w-lg text-sm text-mineshaft-300">
|
||||
Step 1: Create and configure GitHub App. Please refer to the documentation below for
|
||||
more information.
|
||||
</div>
|
||||
<div className="mb-6">
|
||||
<a
|
||||
href="https://infisical.com/docs/integrations/app-connections/github#self-hosted-instance"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Button colorSchema="secondary">Documentation</Button>
|
||||
</a>
|
||||
</div>
|
||||
<div className="mb-4 max-w-lg text-sm text-mineshaft-300">
|
||||
Step 2: Configure your instance-wide settings to enable GitHub App connections. Copy
|
||||
the credentials from your GitHub App's settings page.
|
||||
</div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="clientId"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Client ID"
|
||||
className="w-96"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value || ""}
|
||||
type="text"
|
||||
onChange={(e) => field.onChange(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="clientSecret"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Client Secret"
|
||||
tooltipText="You can find your Client Secret in the GitHub App's settings under 'Client secrets'."
|
||||
className="w-96"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value || ""}
|
||||
type={isGitHubAppClientSecretFocused ? "text" : "password"}
|
||||
onFocus={() => setIsGitHubAppClientSecretFocused.on()}
|
||||
onBlur={() => setIsGitHubAppClientSecretFocused.off()}
|
||||
onChange={(e) => field.onChange(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="appSlug"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="App Slug"
|
||||
tooltipText="The GitHub App slug from the app's URL (e.g., 'my-app' from github.com/apps/my-app)."
|
||||
className="w-96"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value || ""}
|
||||
type="text"
|
||||
onChange={(e) => field.onChange(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="appId"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="App ID"
|
||||
tooltipText="The numeric App ID found in your GitHub App's settings."
|
||||
className="w-96"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value || ""}
|
||||
type="text"
|
||||
onChange={(e) => field.onChange(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="privateKey"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Private Key"
|
||||
tooltipText="The private key generated for your GitHub App (PEM format)."
|
||||
className="w-96"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<TextArea
|
||||
{...field}
|
||||
value={field.value || ""}
|
||||
className="min-h-32"
|
||||
onChange={(e) => field.onChange(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div>
|
||||
<Button
|
||||
className="mt-2"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting || !isDirty}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</form>
|
||||
);
|
||||
};
|
@@ -1,94 +1,24 @@
|
||||
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||
|
||||
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||
import { ROUTE_PATHS } from "@app/const/routes";
|
||||
import { useGetAdminIntegrationsConfig } from "@app/hooks/api";
|
||||
import { AdminIntegrationsConfig } from "@app/hooks/api/admin/types";
|
||||
|
||||
import { GitHubAppConnectionForm } from "./GitHubAppConnectionForm";
|
||||
import { MicrosoftTeamsIntegrationForm } from "./MicrosoftTeamsIntegrationForm";
|
||||
import { SlackIntegrationForm } from "./SlackIntegrationForm";
|
||||
|
||||
enum IntegrationTabSections {
|
||||
Workflow = "workflow",
|
||||
AppConnections = "app-connections"
|
||||
}
|
||||
|
||||
interface WorkflowTabProps {
|
||||
adminIntegrationsConfig: AdminIntegrationsConfig;
|
||||
}
|
||||
|
||||
interface AppConnectionsTabProps {
|
||||
adminIntegrationsConfig: AdminIntegrationsConfig;
|
||||
}
|
||||
|
||||
const WorkflowTab = ({ adminIntegrationsConfig }: WorkflowTabProps) => (
|
||||
<div className="flex flex-col gap-2">
|
||||
<SlackIntegrationForm adminIntegrationsConfig={adminIntegrationsConfig} />
|
||||
<MicrosoftTeamsIntegrationForm adminIntegrationsConfig={adminIntegrationsConfig} />
|
||||
</div>
|
||||
);
|
||||
|
||||
const AppConnectionsTab = ({ adminIntegrationsConfig }: AppConnectionsTabProps) => (
|
||||
<div className="flex flex-col gap-2">
|
||||
<GitHubAppConnectionForm adminIntegrationsConfig={adminIntegrationsConfig} />
|
||||
</div>
|
||||
);
|
||||
|
||||
export const IntegrationsPageForm = () => {
|
||||
const { data: adminIntegrationsConfig } = useGetAdminIntegrationsConfig();
|
||||
|
||||
const navigate = useNavigate({
|
||||
from: ROUTE_PATHS.Admin.IntegrationsPage.path
|
||||
});
|
||||
|
||||
const selectedTab = useSearch({
|
||||
from: ROUTE_PATHS.Admin.IntegrationsPage.id,
|
||||
select: (el: { selectedTab?: string }) => el.selectedTab || IntegrationTabSections.Workflow,
|
||||
structuralSharing: true
|
||||
});
|
||||
|
||||
const updateSelectedTab = (tab: string) => {
|
||||
navigate({
|
||||
search: { selectedTab: tab }
|
||||
});
|
||||
};
|
||||
|
||||
const tabSections = [
|
||||
{
|
||||
key: IntegrationTabSections.Workflow,
|
||||
label: "Workflows",
|
||||
component: WorkflowTab
|
||||
},
|
||||
{
|
||||
key: IntegrationTabSections.AppConnections,
|
||||
label: "App Connections",
|
||||
component: AppConnectionsTab
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="mb-6 min-h-64 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4">
|
||||
<div className="text-xl font-semibold text-mineshaft-100">Integrations</div>
|
||||
<div className="text-sm text-mineshaft-300">
|
||||
Configure your instance-wide settings to enable integration with third-party services.
|
||||
Configure your instance-wide settings to enable integration with Slack and Microsoft
|
||||
Teams.
|
||||
</div>
|
||||
</div>
|
||||
<Tabs value={selectedTab} onValueChange={updateSelectedTab}>
|
||||
<TabList>
|
||||
{tabSections.map((section) => (
|
||||
<Tab value={section.key} key={`integration-tab-${section.key}`}>
|
||||
{section.label}
|
||||
</Tab>
|
||||
))}
|
||||
</TabList>
|
||||
{tabSections.map(({ key, component: Component }) => (
|
||||
<TabPanel value={key} key={`integration-tab-panel-${key}`}>
|
||||
<Component adminIntegrationsConfig={adminIntegrationsConfig!} />
|
||||
</TabPanel>
|
||||
))}
|
||||
</Tabs>
|
||||
<div className="flex flex-col gap-2">
|
||||
<SlackIntegrationForm adminIntegrationsConfig={adminIntegrationsConfig} />
|
||||
<MicrosoftTeamsIntegrationForm adminIntegrationsConfig={adminIntegrationsConfig} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@@ -75,7 +75,7 @@ export const MicrosoftTeamsIntegrationForm = ({ adminIntegrationsConfig }: Props
|
||||
<AccordionTrigger className="flex h-fit w-full justify-start rounded-md border border-mineshaft-500 bg-mineshaft-700 px-4 py-6 text-sm transition-colors data-[state=open]:rounded-b-none">
|
||||
<div className="text-md group order-1 ml-3 flex items-center gap-2">
|
||||
<BsMicrosoftTeams className="text-lg group-hover:text-primary-400" />
|
||||
<div className="text-[15px] font-semibold">Microsoft Teams</div>
|
||||
<div className="text-[15px] font-semibold">Microsoft Teams Integration</div>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent childrenClassName="px-0 py-0">
|
||||
|
@@ -108,7 +108,7 @@ export const SlackIntegrationForm = ({ adminIntegrationsConfig }: Props) => {
|
||||
<AccordionTrigger className="flex h-fit w-full justify-start rounded-md border border-mineshaft-500 bg-mineshaft-700 px-4 py-6 text-sm transition-colors data-[state=open]:rounded-b-none">
|
||||
<div className="text-md group order-1 ml-3 flex items-center gap-2">
|
||||
<BsSlack className="text-lg group-hover:text-primary-400" />
|
||||
<div className="text-[15px] font-semibold">Slack</div>
|
||||
<div className="text-[15px] font-semibold">Slack Integration</div>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent childrenClassName="px-0 py-0">
|
||||
|
@@ -111,10 +111,10 @@ export const GitHubConnectionForm = ({ appConnection }: Props) => {
|
||||
}. This field cannot be changed after creation.`}
|
||||
errorText={
|
||||
!isLoading && isMissingConfig
|
||||
? `Credentials have not been configured. ${
|
||||
? `Environment variables have not been configured. ${
|
||||
isInfisicalCloud()
|
||||
? "Please contact Infisical."
|
||||
: `See Docs to configure Github ${methodDetails.name} Connections.`
|
||||
: `See Docs to configure GitHub ${methodDetails.name} Connections.`
|
||||
}`
|
||||
: error?.message
|
||||
}
|
||||
|
@@ -1,7 +1,5 @@
|
||||
import { Helmet } from "react-helmet";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { faArrowUpRightFromSquare } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v2/Badge";
|
||||
@@ -45,21 +43,7 @@ export const SecretApprovalsPage = () => {
|
||||
<PageHeader
|
||||
title="Approval Workflows"
|
||||
description="Create approval policies for any modifications to secrets in sensitive environments and folders."
|
||||
>
|
||||
<a
|
||||
href="https://infisical.com/docs/documentation/platform/pr-workflows"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<span className="flex w-max cursor-pointer items-center rounded-md border border-mineshaft-500 bg-mineshaft-600 px-4 py-2 text-mineshaft-200 duration-200 hover:border-primary/40 hover:bg-primary/10 hover:text-white">
|
||||
Documentation
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="mb-[0.06rem] ml-1 text-xs"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
</PageHeader>
|
||||
<Tabs defaultValue={defaultTab}>
|
||||
<TabList>
|
||||
<Tab value={TabSection.SecretApprovalRequests}>
|
||||
|
@@ -2,15 +2,25 @@
|
||||
/* eslint-disable react/jsx-no-useless-fragment */
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import {
|
||||
faArrowUpRightFromSquare,
|
||||
faBan,
|
||||
faBookOpen,
|
||||
faCheck,
|
||||
faCheckCircle,
|
||||
faChevronDown,
|
||||
faClipboardCheck,
|
||||
faLock,
|
||||
faPlus
|
||||
faMagnifyingGlass,
|
||||
faPlus,
|
||||
faSearch,
|
||||
faStopwatch,
|
||||
faUser,
|
||||
IconDefinition
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { formatDistance } from "date-fns";
|
||||
import { format, formatDistance } from "date-fns";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
import {
|
||||
@@ -21,6 +31,8 @@ import {
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuTrigger,
|
||||
EmptyState,
|
||||
Input,
|
||||
Pagination,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v2/Badge";
|
||||
@@ -32,7 +44,12 @@ import {
|
||||
useUser,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import {
|
||||
getUserTablePreference,
|
||||
PreferenceKey,
|
||||
setUserTablePreference
|
||||
} from "@app/helpers/userTablePreferences";
|
||||
import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
|
||||
import { useGetWorkspaceUsers } from "@app/hooks/api";
|
||||
import {
|
||||
accessApprovalKeys,
|
||||
@@ -48,28 +65,21 @@ import { ApprovalStatus, TWorkspaceUser } from "@app/hooks/api/types";
|
||||
import { RequestAccessModal } from "./components/RequestAccessModal";
|
||||
import { ReviewAccessRequestModal } from "./components/ReviewAccessModal";
|
||||
|
||||
const generateRequestText = (request: TAccessApprovalRequest, userId: string) => {
|
||||
const generateRequestText = (request: TAccessApprovalRequest) => {
|
||||
const { isTemporary } = request;
|
||||
|
||||
return (
|
||||
<div className="flex w-full items-center justify-between text-sm">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<div>
|
||||
Requested {isTemporary ? "temporary" : "permanent"} access to{" "}
|
||||
<code className="mx-1 rounded-sm bg-primary-500/20 px-1.5 py-0.5 font-mono text-xs text-primary">
|
||||
<code className="mx-1 rounded bg-mineshaft-600 px-1.5 py-0.5 font-mono text-[13px] text-mineshaft-200">
|
||||
{request.policy.secretPath}
|
||||
</code>
|
||||
in
|
||||
<code className="mx-1 rounded-sm bg-primary-500/20 px-1.5 py-0.5 font-mono text-xs text-primary">
|
||||
</code>{" "}
|
||||
in{" "}
|
||||
<code className="mx-1 rounded bg-mineshaft-600 px-1.5 py-0.5 font-mono text-[13px] text-mineshaft-200">
|
||||
{request.environmentName}
|
||||
</code>
|
||||
</div>
|
||||
<div>
|
||||
{request.requestedByUserId === userId && (
|
||||
<span className="text-xs text-gray-500">
|
||||
<Badge className="ml-1">Requested By You</Badge>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -120,30 +130,64 @@ export const AccessApprovalRequest = ({
|
||||
projectSlug
|
||||
});
|
||||
|
||||
const { data: requests, refetch: refetchRequests } = useGetAccessApprovalRequests({
|
||||
const {
|
||||
data: requests,
|
||||
refetch: refetchRequests,
|
||||
isPending: areRequestsPending
|
||||
} = useGetAccessApprovalRequests({
|
||||
projectSlug,
|
||||
authorProjectMembershipId: requestedByFilter,
|
||||
authorUserId: requestedByFilter,
|
||||
envSlug: envFilter
|
||||
});
|
||||
|
||||
const { search, setSearch, setPage, page, perPage, setPerPage, offset } = usePagination("", {
|
||||
initPerPage: getUserTablePreference("accessRequestsTable", PreferenceKey.PerPage, 20)
|
||||
});
|
||||
|
||||
const handlePerPageChange = (newPerPage: number) => {
|
||||
setPerPage(newPerPage);
|
||||
setUserTablePreference("accessRequestsTable", PreferenceKey.PerPage, newPerPage);
|
||||
};
|
||||
|
||||
const filteredRequests = useMemo(() => {
|
||||
let accessRequests: typeof requests;
|
||||
|
||||
if (statusFilter === "open")
|
||||
return requests?.filter(
|
||||
accessRequests = requests?.filter(
|
||||
(request) =>
|
||||
!request.policy.deletedAt &&
|
||||
!request.isApproved &&
|
||||
!request.reviewers.some((reviewer) => reviewer.status === ApprovalStatus.REJECTED)
|
||||
);
|
||||
if (statusFilter === "close")
|
||||
return requests?.filter(
|
||||
accessRequests = requests?.filter(
|
||||
(request) =>
|
||||
request.policy.deletedAt ||
|
||||
request.isApproved ||
|
||||
request.reviewers.some((reviewer) => reviewer.status === ApprovalStatus.REJECTED)
|
||||
);
|
||||
|
||||
return requests;
|
||||
}, [requests, statusFilter, requestedByFilter, envFilter]);
|
||||
return (
|
||||
accessRequests?.filter((request) => {
|
||||
const { environmentName, requestedByUser } = request;
|
||||
|
||||
const searchValue = search.trim().toLowerCase();
|
||||
|
||||
return (
|
||||
environmentName?.toLowerCase().includes(searchValue) ||
|
||||
`${requestedByUser?.email ?? ""} ${requestedByUser?.firstName ?? ""} ${requestedByUser?.lastName ?? ""}`
|
||||
.toLowerCase()
|
||||
.includes(searchValue)
|
||||
);
|
||||
}) ?? []
|
||||
);
|
||||
}, [requests, statusFilter, requestedByFilter, envFilter, search]);
|
||||
|
||||
useResetPageHelper({
|
||||
totalCount: filteredRequests.length,
|
||||
offset,
|
||||
setPage
|
||||
});
|
||||
|
||||
const generateRequestDetails = useCallback(
|
||||
(request: TAccessApprovalRequest) => {
|
||||
@@ -162,9 +206,15 @@ export const AccessApprovalRequest = ({
|
||||
const canBypass =
|
||||
!request.policy.bypassers.length || request.policy.bypassers.includes(user.id);
|
||||
|
||||
let displayData: { label: string; type: "primary" | "danger" | "success" } = {
|
||||
let displayData: {
|
||||
label: string;
|
||||
type: "primary" | "danger" | "success";
|
||||
tooltipContent?: string;
|
||||
icon: IconDefinition | null;
|
||||
} = {
|
||||
label: "",
|
||||
type: "primary"
|
||||
type: "primary",
|
||||
icon: null
|
||||
};
|
||||
|
||||
const isExpired =
|
||||
@@ -172,20 +222,42 @@ export const AccessApprovalRequest = ({
|
||||
request.isApproved &&
|
||||
new Date() > new Date(request.privilege.temporaryAccessEndTime || ("" as string));
|
||||
|
||||
if (isExpired) displayData = { label: "Access Expired", type: "danger" };
|
||||
else if (isAccepted) displayData = { label: "Access Granted", type: "success" };
|
||||
else if (isRejectedByAnyone) displayData = { label: "Rejected", type: "danger" };
|
||||
if (isExpired)
|
||||
displayData = {
|
||||
label: "Access Expired",
|
||||
type: "danger",
|
||||
icon: faStopwatch,
|
||||
tooltipContent: request.privilege?.temporaryAccessEndTime
|
||||
? `Expired ${format(request.privilege.temporaryAccessEndTime, "M/d/yyyy h:mm aa")}`
|
||||
: undefined
|
||||
};
|
||||
else if (isAccepted)
|
||||
displayData = {
|
||||
label: "Access Granted",
|
||||
type: "success",
|
||||
icon: faCheck,
|
||||
tooltipContent: `Granted ${format(request.updatedAt, "M/d/yyyy h:mm aa")}`
|
||||
};
|
||||
else if (isRejectedByAnyone)
|
||||
displayData = {
|
||||
label: "Rejected",
|
||||
type: "danger",
|
||||
icon: faBan,
|
||||
tooltipContent: `Rejected ${format(request.updatedAt, "M/d/yyyy h:mm aa")}`
|
||||
};
|
||||
else if (userReviewStatus === ApprovalStatus.APPROVED) {
|
||||
displayData = {
|
||||
label: `Pending ${request.policy.approvals - request.reviewers.length} review${
|
||||
request.policy.approvals - request.reviewers.length > 1 ? "s" : ""
|
||||
}`,
|
||||
type: "primary"
|
||||
type: "primary",
|
||||
icon: faClipboardCheck
|
||||
};
|
||||
} else if (!isReviewedByUser)
|
||||
displayData = {
|
||||
label: "Review Required",
|
||||
type: "primary"
|
||||
type: "primary",
|
||||
icon: faClipboardCheck
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -225,16 +297,42 @@ export const AccessApprovalRequest = ({
|
||||
[generateRequestDetails, membersGroupById, user, setSelectedRequest, handlePopUpOpen]
|
||||
);
|
||||
|
||||
const isFiltered = Boolean(search || envFilter || requestedByFilter);
|
||||
|
||||
return (
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key="approval-changes-list"
|
||||
transition={{ duration: 0.1 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: 30 }}
|
||||
className="rounded-md text-gray-300"
|
||||
>
|
||||
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div>
|
||||
<div className="mb-6 flex items-end justify-between">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xl font-semibold text-mineshaft-100">Access Requests</span>
|
||||
<div className="mt-2 text-sm text-bunker-300">
|
||||
Request access to secrets in sensitive environments and folders.
|
||||
<div className="flex items-start gap-1">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">Access Requests</p>
|
||||
<a
|
||||
href="https://infisical.com/docs/documentation/platform/access-controls/access-requests"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="ml-1 mt-[0.32rem] inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||
<span>Docs</span>
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="mb-[0.07rem] ml-1.5 text-[10px]"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<p className="text-sm text-bunker-300">
|
||||
Request and review access to secrets in sensitive environments and folders
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Tooltip
|
||||
content="To submit Access Requests, your project needs to create Access Request policies first."
|
||||
isDisabled={policiesLoading || !!policies?.length}
|
||||
@@ -247,25 +345,23 @@ export const AccessApprovalRequest = ({
|
||||
}
|
||||
handlePopUpOpen("requestAccess");
|
||||
}}
|
||||
colorSchema="secondary"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
isDisabled={policiesLoading || !policies?.length}
|
||||
>
|
||||
Request access
|
||||
Request Access
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AnimatePresence>
|
||||
<motion.div
|
||||
key="approval-changes-list"
|
||||
transition={{ duration: 0.1 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: 30 }}
|
||||
className="rounded-md text-gray-300"
|
||||
>
|
||||
<div className="flex items-center space-x-8 rounded-t-md border-x border-t border-mineshaft-600 bg-mineshaft-800 p-4 px-8">
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search approval requests by requesting user or environment..."
|
||||
className="flex-1"
|
||||
containerClassName="mb-4"
|
||||
/>
|
||||
<div className="flex items-center space-x-8 rounded-t-md border-x border-t border-mineshaft-600 bg-mineshaft-800 px-8 py-3 text-sm">
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
@@ -273,17 +369,19 @@ export const AccessApprovalRequest = ({
|
||||
onKeyDown={(evt) => {
|
||||
if (evt.key === "Enter") setStatusFilter("open");
|
||||
}}
|
||||
className={
|
||||
statusFilter === "close" ? "text-gray-500 duration-100 hover:text-gray-400" : ""
|
||||
}
|
||||
className={twMerge(
|
||||
"font-medium",
|
||||
statusFilter === "close" && "text-gray-500 duration-100 hover:text-gray-400"
|
||||
)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faLock} className="mr-2" />
|
||||
{!!requestCount && requestCount?.pendingCount} Pending
|
||||
</div>
|
||||
<div
|
||||
className={
|
||||
statusFilter === "open" ? "text-gray-500 duration-100 hover:text-gray-400" : ""
|
||||
}
|
||||
className={twMerge(
|
||||
"font-medium",
|
||||
statusFilter === "open" && "text-gray-500 duration-100 hover:text-gray-400"
|
||||
)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => setStatusFilter("close")}
|
||||
@@ -292,7 +390,7 @@ export const AccessApprovalRequest = ({
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faCheck} className="mr-2" />
|
||||
{!!requestCount && requestCount.finalizedCount} Completed
|
||||
{!!requestCount && requestCount.finalizedCount} Closed
|
||||
</div>
|
||||
<div className="flex flex-grow justify-end space-x-8">
|
||||
<DropdownMenu>
|
||||
@@ -300,14 +398,20 @@ export const AccessApprovalRequest = ({
|
||||
<Button
|
||||
variant="plain"
|
||||
colorSchema="secondary"
|
||||
className="text-bunker-300"
|
||||
className={envFilter ? "text-white" : "text-bunker-300"}
|
||||
rightIcon={<FontAwesomeIcon icon={faChevronDown} size="sm" className="ml-2" />}
|
||||
>
|
||||
Environments
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuLabel>Select an environment</DropdownMenuLabel>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
sideOffset={1}
|
||||
className="thin-scrollbar max-h-[20rem] overflow-y-auto"
|
||||
>
|
||||
<DropdownMenuLabel className="sticky top-0 bg-mineshaft-900">
|
||||
Select an Environment
|
||||
</DropdownMenuLabel>
|
||||
{currentWorkspace?.environments.map(({ slug, name }) => (
|
||||
<DropdownMenuItem
|
||||
onClick={() => setEnvFilter((state) => (state === slug ? undefined : slug))}
|
||||
@@ -337,15 +441,27 @@ export const AccessApprovalRequest = ({
|
||||
Requested By
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Select an author</DropdownMenuLabel>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
sideOffset={1}
|
||||
className="thin-scrollbar max-h-[20rem] overflow-y-auto"
|
||||
>
|
||||
<DropdownMenuLabel className="sticky top-0 bg-mineshaft-900">
|
||||
Select Requesting User
|
||||
</DropdownMenuLabel>
|
||||
{members?.map(({ user: membershipUser, id }) => (
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
setRequestedByFilter((state) => (state === id ? undefined : id))
|
||||
setRequestedByFilter((state) =>
|
||||
state === membershipUser.id ? undefined : membershipUser.id
|
||||
)
|
||||
}
|
||||
key={`request-filter-member-${id}`}
|
||||
icon={requestedByFilter === id && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||
icon={
|
||||
requestedByFilter === membershipUser.id && (
|
||||
<FontAwesomeIcon icon={faCheckCircle} />
|
||||
)
|
||||
}
|
||||
iconPos="right"
|
||||
>
|
||||
{membershipUser.username}
|
||||
@@ -357,19 +473,26 @@ export const AccessApprovalRequest = ({
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col rounded-b-md border-x border-b border-t border-mineshaft-600 bg-mineshaft-800">
|
||||
{filteredRequests?.length === 0 && (
|
||||
{filteredRequests?.length === 0 && !isFiltered && (
|
||||
<div className="py-12">
|
||||
<EmptyState title="No more access requests pending." />
|
||||
<EmptyState
|
||||
title={`No ${statusFilter === "open" ? "Pending" : "Closed"} Access Requests`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{Boolean(!filteredRequests?.length && isFiltered && !areRequestsPending) && (
|
||||
<div className="py-12">
|
||||
<EmptyState title="No Requests Match Filters" icon={faSearch} />
|
||||
</div>
|
||||
)}
|
||||
{!!filteredRequests?.length &&
|
||||
filteredRequests?.map((request) => {
|
||||
filteredRequests?.slice(offset, perPage * page).map((request) => {
|
||||
const details = generateRequestDetails(request);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={request.id}
|
||||
className="flex w-full cursor-pointer px-8 py-4 hover:bg-mineshaft-700 aria-disabled:opacity-80"
|
||||
className="flex w-full cursor-pointer border-b border-mineshaft-600 px-8 py-3 last:border-b-0 hover:bg-mineshaft-700 aria-disabled:opacity-80"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => handleSelectRequest(request)}
|
||||
@@ -379,14 +502,18 @@ export const AccessApprovalRequest = ({
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="w-full">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="flex w-full flex-col justify-between">
|
||||
<div className="mb-1 flex w-full items-center">
|
||||
<FontAwesomeIcon icon={faLock} className="mr-2" />
|
||||
{generateRequestText(request, user.id)}
|
||||
<FontAwesomeIcon
|
||||
icon={faLock}
|
||||
size="xs"
|
||||
className="mr-1.5 text-mineshaft-300"
|
||||
/>
|
||||
{generateRequestText(request)}
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-xs text-gray-500">
|
||||
<div className="text-xs leading-3 text-gray-500">
|
||||
{membersGroupById?.[request.requestedByUserId]?.user && (
|
||||
<>
|
||||
Requested {formatDistance(new Date(request.createdAt), new Date())}{" "}
|
||||
@@ -397,21 +524,45 @@ export const AccessApprovalRequest = ({
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{request.requestedByUserId === user.id && (
|
||||
<div className="flex items-center gap-1.5 whitespace-nowrap text-xs text-bunker-300">
|
||||
<FontAwesomeIcon icon={faUser} size="sm" />
|
||||
<span>Requested By You</span>
|
||||
</div>
|
||||
)}
|
||||
<Tooltip content={details.displayData.tooltipContent}>
|
||||
<div>
|
||||
<Badge variant={details.displayData.type}>
|
||||
{details.displayData.label}
|
||||
<Badge
|
||||
className="flex items-center gap-1.5 whitespace-nowrap"
|
||||
variant={details.displayData.type}
|
||||
>
|
||||
{details.displayData.icon && (
|
||||
<FontAwesomeIcon icon={details.displayData.icon} />
|
||||
)}
|
||||
<span>{details.displayData.label}</span>
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{Boolean(filteredRequests.length) && (
|
||||
<Pagination
|
||||
className="border-none"
|
||||
count={filteredRequests.length}
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={setPage}
|
||||
onChangePerPage={handlePerPageChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
|
||||
{!!policies && (
|
||||
<RequestAccessModal
|
||||
policies={policies}
|
||||
@@ -452,6 +603,7 @@ export const AccessApprovalRequest = ({
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={() => handlePopUpClose("upgradePlan")}
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
||||
|
@@ -1,11 +1,19 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import {
|
||||
faArrowDown,
|
||||
faArrowUp,
|
||||
faArrowUpRightFromSquare,
|
||||
faBookOpen,
|
||||
faCheckCircle,
|
||||
faChevronDown,
|
||||
faFileShield,
|
||||
faPlus
|
||||
faFilter,
|
||||
faMagnifyingGlass,
|
||||
faPlus,
|
||||
faSearch
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
@@ -19,8 +27,9 @@ import {
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuTrigger,
|
||||
EmptyState,
|
||||
Modal,
|
||||
ModalContent,
|
||||
IconButton,
|
||||
Input,
|
||||
Pagination,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
@@ -38,7 +47,12 @@ import {
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { ProjectPermissionActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import {
|
||||
getUserTablePreference,
|
||||
PreferenceKey,
|
||||
setUserTablePreference
|
||||
} from "@app/helpers/userTablePreferences";
|
||||
import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
|
||||
import {
|
||||
useDeleteAccessApprovalPolicy,
|
||||
useDeleteSecretApprovalPolicy,
|
||||
@@ -47,6 +61,7 @@ import {
|
||||
useListWorkspaceGroups
|
||||
} from "@app/hooks/api";
|
||||
import { useGetAccessApprovalPolicies } from "@app/hooks/api/accessApproval/queries";
|
||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
import { PolicyType } from "@app/hooks/api/policies/enums";
|
||||
import { TAccessApprovalPolicy, Workspace } from "@app/hooks/api/types";
|
||||
|
||||
@@ -57,6 +72,18 @@ interface IProps {
|
||||
workspaceId: string;
|
||||
}
|
||||
|
||||
enum PolicyOrderBy {
|
||||
Name = "name",
|
||||
Environment = "environment",
|
||||
SecretPath = "secret-path",
|
||||
Type = "type"
|
||||
}
|
||||
|
||||
type PolicyFilters = {
|
||||
type: null | PolicyType;
|
||||
environmentIds: string[];
|
||||
};
|
||||
|
||||
const useApprovalPolicies = (permission: TProjectPermission, currentWorkspace?: Workspace) => {
|
||||
const { data: accessPolicies, isPending: isAccessPoliciesLoading } = useGetAccessApprovalPolicies(
|
||||
{
|
||||
@@ -112,11 +139,79 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
|
||||
currentWorkspace
|
||||
);
|
||||
|
||||
const [filterType, setFilterType] = useState<string | null>(null);
|
||||
const [filters, setFilters] = useState<PolicyFilters>({
|
||||
type: null,
|
||||
environmentIds: []
|
||||
});
|
||||
|
||||
const filteredPolicies = useMemo(() => {
|
||||
return filterType ? policies.filter((policy) => policy.policyType === filterType) : policies;
|
||||
}, [policies, filterType]);
|
||||
const {
|
||||
search,
|
||||
setSearch,
|
||||
setPage,
|
||||
page,
|
||||
perPage,
|
||||
setPerPage,
|
||||
offset,
|
||||
orderDirection,
|
||||
orderBy,
|
||||
setOrderBy,
|
||||
setOrderDirection,
|
||||
toggleOrderDirection
|
||||
} = usePagination<PolicyOrderBy>(PolicyOrderBy.Name, {
|
||||
initPerPage: getUserTablePreference("approvalPoliciesTable", PreferenceKey.PerPage, 20)
|
||||
});
|
||||
|
||||
const handlePerPageChange = (newPerPage: number) => {
|
||||
setPerPage(newPerPage);
|
||||
setUserTablePreference("approvalPoliciesTable", PreferenceKey.PerPage, newPerPage);
|
||||
};
|
||||
|
||||
const filteredPolicies = useMemo(
|
||||
() =>
|
||||
policies
|
||||
.filter(({ policyType, environment, name, secretPath }) => {
|
||||
if (filters.type && policyType !== filters.type) return false;
|
||||
|
||||
if (filters.environmentIds.length && !filters.environmentIds.includes(environment.id))
|
||||
return false;
|
||||
|
||||
const searchValue = search.trim().toLowerCase();
|
||||
|
||||
return (
|
||||
name.toLowerCase().includes(searchValue) ||
|
||||
environment.name.toLowerCase().includes(searchValue) ||
|
||||
(secretPath ?? "*").toLowerCase().includes(searchValue)
|
||||
);
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const [policyOne, policyTwo] = orderDirection === OrderByDirection.ASC ? [a, b] : [b, a];
|
||||
|
||||
switch (orderBy) {
|
||||
case PolicyOrderBy.Type:
|
||||
return policyOne.policyType
|
||||
.toLowerCase()
|
||||
.localeCompare(policyTwo.policyType.toLowerCase());
|
||||
case PolicyOrderBy.Environment:
|
||||
return policyOne.environment.name
|
||||
.toLowerCase()
|
||||
.localeCompare(policyTwo.environment.name.toLowerCase());
|
||||
case PolicyOrderBy.SecretPath:
|
||||
return (policyOne.secretPath ?? "*")
|
||||
.toLowerCase()
|
||||
.localeCompare((policyTwo.secretPath ?? "*").toLowerCase());
|
||||
case PolicyOrderBy.Name:
|
||||
default:
|
||||
return policyOne.name.toLowerCase().localeCompare(policyTwo.name.toLowerCase());
|
||||
}
|
||||
}),
|
||||
[policies, filters, search, orderBy, orderDirection]
|
||||
);
|
||||
|
||||
useResetPageHelper({
|
||||
totalCount: filteredPolicies.length,
|
||||
offset,
|
||||
setPage
|
||||
});
|
||||
|
||||
const { mutateAsync: deleteSecretApprovalPolicy } = useDeleteSecretApprovalPolicy();
|
||||
const { mutateAsync: deleteAccessApprovalPolicy } = useDeleteAccessApprovalPolicy();
|
||||
@@ -151,16 +246,57 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
|
||||
}
|
||||
};
|
||||
|
||||
const isTableFiltered = filters.type !== null || Boolean(filters.environmentIds.length);
|
||||
|
||||
const handleSort = (column: PolicyOrderBy) => {
|
||||
if (column === orderBy) {
|
||||
toggleOrderDirection();
|
||||
return;
|
||||
}
|
||||
|
||||
setOrderBy(column);
|
||||
setOrderDirection(OrderByDirection.ASC);
|
||||
};
|
||||
|
||||
const getClassName = (col: PolicyOrderBy) => twMerge("ml-2", orderBy === col ? "" : "opacity-30");
|
||||
|
||||
const getColSortIcon = (col: PolicyOrderBy) =>
|
||||
orderDirection === OrderByDirection.DESC && orderBy === col ? faArrowUp : faArrowDown;
|
||||
|
||||
return (
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key="approval-changes-list"
|
||||
transition={{ duration: 0.1 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: 30 }}
|
||||
className="rounded-md text-gray-300"
|
||||
>
|
||||
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div>
|
||||
<div className="mb-6 flex items-end justify-between">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xl font-semibold text-mineshaft-100">Policies</span>
|
||||
<div className="mt-2 text-sm text-bunker-300">
|
||||
Implement granular policies for access requests and secrets management.
|
||||
<div className="flex items-start gap-1">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">Policies</p>
|
||||
<a
|
||||
href="https://infisical.com/docs/documentation/platform/pr-workflows"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="ml-1 mt-[0.32rem] inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||
<span>Docs</span>
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="mb-[0.07rem] ml-1.5 text-[10px]"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<p className="text-sm text-bunker-300">
|
||||
Implement granular policies for access requests and secrets management
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={ProjectPermissionSub.SecretApproval}
|
||||
@@ -174,6 +310,7 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
|
||||
}
|
||||
handlePopUpOpen("policyForm");
|
||||
}}
|
||||
colorSchema="secondary"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
@@ -182,41 +319,54 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</div>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Name</Th>
|
||||
<Th>Environment</Th>
|
||||
<Th>Secret Path</Th>
|
||||
<Th>
|
||||
<div className="mb-4 flex items-center gap-2">
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search policies by name, type, environment or secret path..."
|
||||
className="flex-1"
|
||||
/>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Button
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
ariaLabel="Filter findings"
|
||||
variant="plain"
|
||||
colorSchema="secondary"
|
||||
className="text-xs font-semibold uppercase text-bunker-300"
|
||||
rightIcon={
|
||||
<FontAwesomeIcon icon={faChevronDown} size="sm" className="ml-2" />
|
||||
}
|
||||
size="sm"
|
||||
className={twMerge(
|
||||
"flex h-10 w-11 items-center justify-center overflow-hidden border border-mineshaft-600 bg-mineshaft-800 p-0 transition-all hover:border-primary/60 hover:bg-primary/10",
|
||||
isTableFiltered && "border-primary/50 text-primary"
|
||||
)}
|
||||
>
|
||||
Type
|
||||
</Button>
|
||||
<FontAwesomeIcon icon={faFilter} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuLabel>Select a type</DropdownMenuLabel>
|
||||
<DropdownMenuContent
|
||||
className="thin-scrollbar max-h-[70vh] overflow-y-auto"
|
||||
align="end"
|
||||
>
|
||||
<DropdownMenuLabel>Policy Type</DropdownMenuLabel>
|
||||
<DropdownMenuItem
|
||||
onClick={() => setFilterType(null)}
|
||||
icon={!filterType && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||
onClick={() =>
|
||||
setFilters((prev) => ({
|
||||
...prev,
|
||||
type: null
|
||||
}))
|
||||
}
|
||||
icon={!filters && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||
iconPos="right"
|
||||
>
|
||||
All
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => setFilterType(PolicyType.AccessPolicy)}
|
||||
onClick={() =>
|
||||
setFilters((prev) => ({
|
||||
...prev,
|
||||
type: PolicyType.AccessPolicy
|
||||
}))
|
||||
}
|
||||
icon={
|
||||
filterType === PolicyType.AccessPolicy && (
|
||||
filters.type === PolicyType.AccessPolicy && (
|
||||
<FontAwesomeIcon icon={faCheckCircle} />
|
||||
)
|
||||
}
|
||||
@@ -225,9 +375,14 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
|
||||
Access Policy
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => setFilterType(PolicyType.ChangePolicy)}
|
||||
onClick={() =>
|
||||
setFilters((prev) => ({
|
||||
...prev,
|
||||
type: PolicyType.ChangePolicy
|
||||
}))
|
||||
}
|
||||
icon={
|
||||
filterType === PolicyType.ChangePolicy && (
|
||||
filters.type === PolicyType.ChangePolicy && (
|
||||
<FontAwesomeIcon icon={faCheckCircle} />
|
||||
)
|
||||
}
|
||||
@@ -235,25 +390,110 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
|
||||
>
|
||||
Change Policy
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuLabel>Environment</DropdownMenuLabel>
|
||||
{currentWorkspace.environments.map((env) => (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setFilters((prev) => ({
|
||||
...prev,
|
||||
environmentIds: prev.environmentIds.includes(env.id)
|
||||
? prev.environmentIds.filter((i) => i !== env.id)
|
||||
: [...prev.environmentIds, env.id]
|
||||
}));
|
||||
}}
|
||||
key={env.id}
|
||||
icon={
|
||||
filters.environmentIds.includes(env.id) && (
|
||||
<FontAwesomeIcon className="text-primary" icon={faCheckCircle} />
|
||||
)
|
||||
}
|
||||
iconPos="right"
|
||||
>
|
||||
<span className="capitalize">{env.name}</span>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>
|
||||
<div className="flex items-center">
|
||||
Name
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={getClassName(PolicyOrderBy.Name)}
|
||||
ariaLabel="sort"
|
||||
onClick={() => handleSort(PolicyOrderBy.Name)}
|
||||
>
|
||||
<FontAwesomeIcon icon={getColSortIcon(PolicyOrderBy.Name)} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th />
|
||||
<Th>
|
||||
<div className="flex items-center">
|
||||
Environment
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={getClassName(PolicyOrderBy.Environment)}
|
||||
ariaLabel="sort"
|
||||
onClick={() => handleSort(PolicyOrderBy.Environment)}
|
||||
>
|
||||
<FontAwesomeIcon icon={getColSortIcon(PolicyOrderBy.Environment)} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th>
|
||||
<div className="flex items-center">
|
||||
Secret Path
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={getClassName(PolicyOrderBy.SecretPath)}
|
||||
ariaLabel="sort"
|
||||
onClick={() => handleSort(PolicyOrderBy.SecretPath)}
|
||||
>
|
||||
<FontAwesomeIcon icon={getColSortIcon(PolicyOrderBy.SecretPath)} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th>
|
||||
<div className="flex items-center">
|
||||
Type
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={getClassName(PolicyOrderBy.Type)}
|
||||
ariaLabel="sort"
|
||||
onClick={() => handleSort(PolicyOrderBy.Type)}
|
||||
>
|
||||
<FontAwesomeIcon icon={getColSortIcon(PolicyOrderBy.Type)} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isPoliciesLoading && (
|
||||
<TableSkeleton columns={6} innerKey="secret-policies" className="bg-mineshaft-700" />
|
||||
<TableSkeleton
|
||||
columns={5}
|
||||
innerKey="secret-policies"
|
||||
className="bg-mineshaft-700"
|
||||
/>
|
||||
)}
|
||||
{!isPoliciesLoading && !filteredPolicies?.length && (
|
||||
{!isPoliciesLoading && !policies?.length && (
|
||||
<Tr>
|
||||
<Td colSpan={6}>
|
||||
<EmptyState title="No policies found" icon={faFileShield} />
|
||||
<Td colSpan={5}>
|
||||
<EmptyState title="No Policies Found" icon={faFileShield} />
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
{!!currentWorkspace &&
|
||||
filteredPolicies?.map((policy) => (
|
||||
filteredPolicies
|
||||
?.slice(offset, perPage * page)
|
||||
.map((policy) => (
|
||||
<ApprovalPolicyRow
|
||||
policy={policy}
|
||||
key={policy.id}
|
||||
@@ -265,20 +505,21 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
|
||||
))}
|
||||
</TBody>
|
||||
</Table>
|
||||
{Boolean(!filteredPolicies.length && policies.length && !isPoliciesLoading) && (
|
||||
<EmptyState title="No Policies Match Search" icon={faSearch} />
|
||||
)}
|
||||
{Boolean(filteredPolicies.length) && (
|
||||
<Pagination
|
||||
count={filteredPolicies.length}
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={setPage}
|
||||
onChangePerPage={handlePerPageChange}
|
||||
/>
|
||||
)}
|
||||
</TableContainer>
|
||||
<Modal
|
||||
isOpen={popUp.policyForm.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("policyForm", isOpen)}
|
||||
>
|
||||
<ModalContent
|
||||
className="max-w-3xl"
|
||||
title={
|
||||
popUp.policyForm.data
|
||||
? `Edit ${popUp?.policyForm?.data?.name || "Policy"}`
|
||||
: "Create Policy"
|
||||
}
|
||||
id="policy-form"
|
||||
>
|
||||
</div>
|
||||
</motion.div>
|
||||
<AccessPolicyForm
|
||||
projectId={currentWorkspace.id}
|
||||
projectSlug={currentWorkspace.slug}
|
||||
@@ -287,8 +528,6 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
|
||||
members={members}
|
||||
editValues={popUp.policyForm.data as TAccessApprovalPolicy}
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deletePolicy.isOpen}
|
||||
deleteKey="remove"
|
||||
@@ -301,6 +540,6 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text="You can add secret approval policy if you switch to Infisical's Enterprise plan."
|
||||
/>
|
||||
</div>
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { RefObject, useMemo, useRef, useState } from "react";
|
||||
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||
import { faGripVertical, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
@@ -13,6 +13,8 @@ import {
|
||||
FormControl,
|
||||
IconButton,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Select,
|
||||
SelectItem,
|
||||
Switch,
|
||||
@@ -110,20 +112,20 @@ const formSchema = z
|
||||
|
||||
type TFormSchema = z.infer<typeof formSchema>;
|
||||
|
||||
export const AccessPolicyForm = ({
|
||||
isOpen,
|
||||
const Form = ({
|
||||
onToggle,
|
||||
members = [],
|
||||
projectId,
|
||||
projectSlug,
|
||||
editValues
|
||||
}: Props) => {
|
||||
editValues,
|
||||
modalContainer,
|
||||
isEditMode
|
||||
}: Props & { modalContainer: RefObject<HTMLDivElement>; isEditMode: boolean }) => {
|
||||
const [draggedItem, setDraggedItem] = useState<number | null>(null);
|
||||
const [dragOverItem, setDragOverItem] = useState<number | null>(null);
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
watch,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<TFormSchema>({
|
||||
@@ -188,13 +190,8 @@ export const AccessPolicyForm = ({
|
||||
const { data: groups } = useListWorkspaceGroups(projectId);
|
||||
|
||||
const environments = currentWorkspace?.environments || [];
|
||||
const isEditMode = Boolean(editValues);
|
||||
const isAccessPolicyType = watch("policyType") === PolicyType.AccessPolicy;
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen || !isEditMode) reset({});
|
||||
}, [isOpen, isEditMode]);
|
||||
|
||||
const { mutateAsync: createAccessApprovalPolicy } = useCreateAccessApprovalPolicy();
|
||||
const { mutateAsync: updateAccessApprovalPolicy } = useUpdateAccessApprovalPolicy();
|
||||
|
||||
@@ -387,6 +384,7 @@ export const AccessPolicyForm = ({
|
||||
setDraggedItem(null);
|
||||
setDragOverItem(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col space-y-3">
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||
@@ -572,7 +570,7 @@ export const AccessPolicyForm = ({
|
||||
className="flex-grow"
|
||||
>
|
||||
<FilterableSelect
|
||||
menuPortalTarget={document.getElementById("policy-form")}
|
||||
menuPortalTarget={modalContainer.current}
|
||||
menuPlacement="top"
|
||||
isMulti
|
||||
placeholder="Select members..."
|
||||
@@ -602,7 +600,7 @@ export const AccessPolicyForm = ({
|
||||
className="flex-grow"
|
||||
>
|
||||
<FilterableSelect
|
||||
menuPortalTarget={document.getElementById("policy-form")}
|
||||
menuPortalTarget={modalContainer.current}
|
||||
menuPlacement="top"
|
||||
isMulti
|
||||
placeholder="Select groups..."
|
||||
@@ -813,3 +811,27 @@ export const AccessPolicyForm = ({
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const AccessPolicyForm = ({ isOpen, onToggle, editValues, ...props }: Props) => {
|
||||
const modalContainer = useRef<HTMLDivElement>(null);
|
||||
const isEditMode = Boolean(editValues);
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onToggle}>
|
||||
<ModalContent
|
||||
className="max-w-3xl"
|
||||
ref={modalContainer}
|
||||
title={isEditMode ? "Edit Policy" : "Create Policy"}
|
||||
>
|
||||
<Form
|
||||
{...props}
|
||||
isOpen={isOpen}
|
||||
onToggle={onToggle}
|
||||
editValues={editValues}
|
||||
modalContainer={modalContainer}
|
||||
isEditMode={isEditMode}
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { useMemo } from "react";
|
||||
import { faEllipsis } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faEdit, faEllipsisV, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
GenericFieldLabel,
|
||||
IconButton,
|
||||
Td,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
@@ -80,11 +82,11 @@ export const ApprovalPolicyRow = ({
|
||||
userLabels: members
|
||||
?.filter((member) => el.user.find((i) => i.id === member.user.id))
|
||||
.map((member) => getMemberLabel(member))
|
||||
.join(","),
|
||||
.join(", "),
|
||||
groupLabels: groups
|
||||
?.filter(({ group }) => el.group.find((i) => i.id === group.id))
|
||||
.map(({ group }) => group.name)
|
||||
.join(","),
|
||||
.join(", "),
|
||||
approvals: el.approvals
|
||||
};
|
||||
});
|
||||
@@ -102,36 +104,47 @@ export const ApprovalPolicyRow = ({
|
||||
}}
|
||||
onClick={() => setIsExpanded.toggle()}
|
||||
>
|
||||
<Td>{policy.name}</Td>
|
||||
<Td>{policy.environment.slug}</Td>
|
||||
<Td>{policy.name || <span className="text-mineshaft-400">Unnamed Policy</span>}</Td>
|
||||
<Td>{policy.environment.name}</Td>
|
||||
<Td>{policy.secretPath || "*"}</Td>
|
||||
<Td>
|
||||
<Badge className={policyDetails[policy.policyType].className}>
|
||||
{policyDetails[policy.policyType].name}
|
||||
<Badge
|
||||
className={twMerge(
|
||||
policyDetails[policy.policyType].className,
|
||||
"flex w-min items-center gap-1.5 whitespace-nowrap"
|
||||
)}
|
||||
>
|
||||
<FontAwesomeIcon icon={policyDetails[policy.policyType].icon} />
|
||||
<span>{policyDetails[policy.policyType].name}</span>
|
||||
</Badge>
|
||||
</Td>
|
||||
<Td>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="cursor-pointer rounded-lg">
|
||||
<div className="flex items-center justify-center transition-transform duration-300 ease-in-out hover:scale-125 hover:text-primary-400 data-[state=open]:scale-125 data-[state=open]:text-primary-400">
|
||||
<FontAwesomeIcon size="sm" icon={faEllipsis} />
|
||||
</div>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
ariaLabel="Options"
|
||||
colorSchema="secondary"
|
||||
className="w-6"
|
||||
variant="plain"
|
||||
>
|
||||
<FontAwesomeIcon icon={faEllipsisV} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="center" className="min-w-[100%] p-1">
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent sideOffset={2} align="end" className="min-w-[12rem] p-1">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.SecretApproval}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onEdit();
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
isDisabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faEdit} />}
|
||||
>
|
||||
Edit Policy
|
||||
</DropdownMenuItem>
|
||||
@@ -143,16 +156,12 @@ export const ApprovalPolicyRow = ({
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
isAllowed
|
||||
? "hover:!bg-red-500 hover:!text-white"
|
||||
: "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDelete();
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
isDisabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||
>
|
||||
Delete Policy
|
||||
</DropdownMenuItem>
|
||||
@@ -162,45 +171,41 @@ export const ApprovalPolicyRow = ({
|
||||
</DropdownMenu>
|
||||
</Td>
|
||||
</Tr>
|
||||
{isExpanded && (
|
||||
<Tr>
|
||||
<Td colSpan={5} className="rounded bg-mineshaft-900">
|
||||
<div className="mb-4 border-b-2 border-mineshaft-500 py-2 text-lg">Approvers</div>
|
||||
<Td colSpan={6} className="!border-none p-0">
|
||||
<div
|
||||
className={`w-full overflow-hidden bg-mineshaft-900/75 transition-all duration-500 ease-in-out ${
|
||||
isExpanded ? "thin-scrollbar max-h-[26rem] !overflow-y-auto opacity-100" : "max-h-0"
|
||||
}`}
|
||||
>
|
||||
<div className="p-4">
|
||||
<div className="mb-4 border-b-2 border-mineshaft-500 pb-2">Approvers</div>
|
||||
{labels?.map((el, index) => (
|
||||
<div
|
||||
key={`approval-list-${index + 1}`}
|
||||
className="relative mb-2 flex rounded border border-mineshaft-500 bg-mineshaft-700 p-4"
|
||||
className="relative mb-2 flex rounded border border-mineshaft-500 bg-mineshaft-800 p-4"
|
||||
>
|
||||
<div>
|
||||
<div className="mr-8 flex h-8 w-8 items-center justify-center border border-bunker-300 bg-bunker-800 text-white">
|
||||
<div className="text-lg">{index + 1}</div>
|
||||
<div className="my-auto mr-8 flex h-8 w-8 items-center justify-center rounded border border-mineshaft-400 bg-bunker-500/50 text-white">
|
||||
<div>{index + 1}</div>
|
||||
</div>
|
||||
{index !== labels.length - 1 && (
|
||||
<div className="absolute bottom-0 left-8 h-6 border-r border-gray-400" />
|
||||
<div className="absolute bottom-0 left-8 h-[1.25rem] border-r border-mineshaft-400" />
|
||||
)}
|
||||
{index !== 0 && (
|
||||
<div className="absolute left-8 top-0 h-4 border-r border-gray-400" />
|
||||
<div className="absolute left-8 top-0 h-[1.25rem] border-r border-mineshaft-400" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid flex-grow grid-cols-3">
|
||||
<div>
|
||||
<div className="mb-1 text-xs font-semibold uppercase">Users</div>
|
||||
<div>{el.userLabels || "-"}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-1 text-xs font-semibold uppercase">Groups</div>
|
||||
<div>{el.groupLabels || "-"}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-1 text-xs font-semibold uppercase">Approvals Required</div>
|
||||
<div>{el.approvals || "-"}</div>
|
||||
</div>
|
||||
<GenericFieldLabel label="Users">{el.userLabels}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Groups">{el.groupLabels}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Approvals Required">{el.approvals}</GenericFieldLabel>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@@ -1,14 +1,19 @@
|
||||
import { Fragment, useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
faArrowUpRightFromSquare,
|
||||
faBookOpen,
|
||||
faCheck,
|
||||
faCheckCircle,
|
||||
faChevronDown,
|
||||
faCodeBranch
|
||||
faCodeBranch,
|
||||
faMagnifyingGlass,
|
||||
faSearch
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useSearch } from "@tanstack/react-router";
|
||||
import { formatDistance } from "date-fns";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import {
|
||||
Button,
|
||||
@@ -18,6 +23,8 @@ import {
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuTrigger,
|
||||
EmptyState,
|
||||
Input,
|
||||
Pagination,
|
||||
Skeleton
|
||||
} from "@app/components/v2";
|
||||
import { ROUTE_PATHS } from "@app/const/routes";
|
||||
@@ -28,6 +35,12 @@ import {
|
||||
useUser,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import {
|
||||
getUserTablePreference,
|
||||
PreferenceKey,
|
||||
setUserTablePreference
|
||||
} from "@app/helpers/userTablePreferences";
|
||||
import { usePagination } from "@app/hooks";
|
||||
import {
|
||||
useGetSecretApprovalRequestCount,
|
||||
useGetSecretApprovalRequests,
|
||||
@@ -52,18 +65,41 @@ export const SecretApprovalRequest = () => {
|
||||
const [usingUrlRequestId, setUsingUrlRequestId] = useState(false);
|
||||
|
||||
const {
|
||||
data: secretApprovalRequests,
|
||||
isFetchingNextPage: isFetchingNextApprovalRequest,
|
||||
fetchNextPage: fetchNextApprovalRequest,
|
||||
hasNextPage: hasNextApprovalPage,
|
||||
debouncedSearch: debouncedSearchFilter,
|
||||
search: searchFilter,
|
||||
setSearch: setSearchFilter,
|
||||
setPage,
|
||||
page,
|
||||
perPage,
|
||||
setPerPage,
|
||||
offset,
|
||||
limit
|
||||
} = usePagination("", {
|
||||
initPerPage: getUserTablePreference("changeRequestsTable", PreferenceKey.PerPage, 20)
|
||||
});
|
||||
|
||||
const handlePerPageChange = (newPerPage: number) => {
|
||||
setPerPage(newPerPage);
|
||||
setUserTablePreference("changeRequestsTable", PreferenceKey.PerPage, newPerPage);
|
||||
};
|
||||
|
||||
const {
|
||||
data,
|
||||
isPending: isApprovalRequestLoading,
|
||||
refetch
|
||||
} = useGetSecretApprovalRequests({
|
||||
workspaceId,
|
||||
status: statusFilter,
|
||||
environment: envFilter,
|
||||
committer: committerFilter
|
||||
committer: committerFilter,
|
||||
search: debouncedSearchFilter,
|
||||
limit,
|
||||
offset
|
||||
});
|
||||
|
||||
const totalApprovalCount = data?.totalCount ?? 0;
|
||||
const secretApprovalRequests = data?.approvals ?? [];
|
||||
|
||||
const { data: secretApprovalRequestCount, isSuccess: isSecretApprovalReqCountSuccess } =
|
||||
useGetSecretApprovalRequestCount({ workspaceId });
|
||||
const { user: userSession } = useUser();
|
||||
@@ -88,8 +124,9 @@ export const SecretApprovalRequest = () => {
|
||||
refetch();
|
||||
};
|
||||
|
||||
const isRequestListEmpty =
|
||||
!isApprovalRequestLoading && secretApprovalRequests?.pages[0]?.length === 0;
|
||||
const isRequestListEmpty = !isApprovalRequestLoading && secretApprovalRequests?.length === 0;
|
||||
|
||||
const isFiltered = Boolean(searchFilter || envFilter || committerFilter);
|
||||
|
||||
return (
|
||||
<AnimatePresence mode="wait">
|
||||
@@ -116,7 +153,38 @@ export const SecretApprovalRequest = () => {
|
||||
exit={{ opacity: 0, translateX: 30 }}
|
||||
className="rounded-md text-gray-300"
|
||||
>
|
||||
<div className="flex items-center space-x-8 rounded-t-md border-x border-t border-mineshaft-600 bg-mineshaft-800 p-4 px-8">
|
||||
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div>
|
||||
<div className="flex items-start gap-1">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">Change Requests</p>
|
||||
<a
|
||||
href="https://infisical.com/docs/documentation/platform/pr-workflows"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="ml-1 mt-[0.32rem] inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||
<span>Docs</span>
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="mb-[0.07rem] ml-1.5 text-[10px]"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<p className="text-sm text-bunker-300">Review pending and closed change requests</p>
|
||||
</div>
|
||||
</div>
|
||||
<Input
|
||||
value={searchFilter}
|
||||
onChange={(e) => setSearchFilter(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search change requests by author, environment or policy path..."
|
||||
className="flex-1"
|
||||
containerClassName="mb-4"
|
||||
/>
|
||||
<div className="flex items-center space-x-8 rounded-t-md border-x border-t border-mineshaft-600 bg-mineshaft-800 px-8 py-3 text-sm">
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
@@ -124,17 +192,19 @@ export const SecretApprovalRequest = () => {
|
||||
onKeyDown={(evt) => {
|
||||
if (evt.key === "Enter") setStatusFilter("open");
|
||||
}}
|
||||
className={
|
||||
statusFilter === "close" ? "text-gray-500 duration-100 hover:text-gray-400" : ""
|
||||
}
|
||||
className={twMerge(
|
||||
"font-medium",
|
||||
statusFilter === "close" && "text-gray-500 duration-100 hover:text-gray-400"
|
||||
)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faCodeBranch} className="mr-2" />
|
||||
{isSecretApprovalReqCountSuccess && secretApprovalRequestCount?.open} Open
|
||||
</div>
|
||||
<div
|
||||
className={
|
||||
statusFilter === "open" ? "text-gray-500 duration-100 hover:text-gray-400" : ""
|
||||
}
|
||||
className={twMerge(
|
||||
"font-medium",
|
||||
statusFilter === "open" && "text-gray-500 duration-100 hover:text-gray-400"
|
||||
)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => setStatusFilter("close")}
|
||||
@@ -152,13 +222,21 @@ export const SecretApprovalRequest = () => {
|
||||
variant="plain"
|
||||
colorSchema="secondary"
|
||||
className={envFilter ? "text-white" : "text-bunker-300"}
|
||||
rightIcon={<FontAwesomeIcon icon={faChevronDown} size="sm" className="ml-2" />}
|
||||
rightIcon={
|
||||
<FontAwesomeIcon icon={faChevronDown} size="sm" className="ml-2" />
|
||||
}
|
||||
>
|
||||
Environments
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuLabel>Select an environment</DropdownMenuLabel>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
sideOffset={1}
|
||||
className="thin-scrollbar max-h-[20rem] overflow-y-auto"
|
||||
>
|
||||
<DropdownMenuLabel className="sticky top-0 bg-mineshaft-900">
|
||||
Select an Environment
|
||||
</DropdownMenuLabel>
|
||||
{currentWorkspace?.environments.map(({ slug, name }) => (
|
||||
<DropdownMenuItem
|
||||
onClick={() => setEnvFilter((state) => (state === slug ? undefined : slug))}
|
||||
@@ -188,8 +266,14 @@ export const SecretApprovalRequest = () => {
|
||||
Author
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Select an author</DropdownMenuLabel>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
sideOffset={1}
|
||||
className="thin-scrollbar max-h-[20rem] overflow-y-auto"
|
||||
>
|
||||
<DropdownMenuLabel className="sticky top-0 bg-mineshaft-900">
|
||||
Select an Author
|
||||
</DropdownMenuLabel>
|
||||
{members?.map(({ user, id }) => (
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
@@ -210,14 +294,14 @@ export const SecretApprovalRequest = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col rounded-b-md border-x border-b border-t border-mineshaft-600 bg-mineshaft-800">
|
||||
{isRequestListEmpty && (
|
||||
{isRequestListEmpty && !isFiltered && (
|
||||
<div className="py-12">
|
||||
<EmptyState title="No more requests pending." />
|
||||
<EmptyState
|
||||
title={`No ${statusFilter === "open" ? "Open" : "Closed"} Change Requests`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{secretApprovalRequests?.pages?.map((group, i) => (
|
||||
<Fragment key={`secret-approval-request-${i + 1}`}>
|
||||
{group?.map((secretApproval) => {
|
||||
{secretApprovalRequests.map((secretApproval) => {
|
||||
const {
|
||||
id: reqId,
|
||||
commits,
|
||||
@@ -233,7 +317,7 @@ export const SecretApprovalRequest = () => {
|
||||
return (
|
||||
<div
|
||||
key={reqId}
|
||||
className="flex flex-col px-8 py-4 hover:bg-mineshaft-700"
|
||||
className="flex flex-col border-b border-mineshaft-600 px-8 py-3 last:border-b-0 hover:bg-mineshaft-700"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => setSelectedApprovalId(secretApproval.id)}
|
||||
@@ -241,14 +325,18 @@ export const SecretApprovalRequest = () => {
|
||||
if (evt.key === "Enter") setSelectedApprovalId(secretApproval.id);
|
||||
}}
|
||||
>
|
||||
<div className="mb-1">
|
||||
<FontAwesomeIcon icon={faCodeBranch} className="mr-2" />
|
||||
<div className="mb-1 text-sm">
|
||||
<FontAwesomeIcon
|
||||
icon={faCodeBranch}
|
||||
size="sm"
|
||||
className="mr-1.5 text-mineshaft-300"
|
||||
/>
|
||||
{secretApproval.isReplicated
|
||||
? `${commits.length} secret pending import`
|
||||
: generateCommitText(commits)}
|
||||
<span className="text-xs text-bunker-300"> #{secretApproval.slug}</span>
|
||||
</div>
|
||||
<span className="text-xs text-gray-500">
|
||||
<span className="text-xs leading-3 text-gray-500">
|
||||
Opened {formatDistance(new Date(createdAt), new Date())} ago by{" "}
|
||||
{committerUser?.firstName || ""} {committerUser?.lastName || ""} (
|
||||
{committerUser?.email})
|
||||
@@ -257,9 +345,24 @@ export const SecretApprovalRequest = () => {
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Fragment>
|
||||
))}
|
||||
{(isFetchingNextApprovalRequest || isApprovalRequestLoading) && (
|
||||
{Boolean(
|
||||
!secretApprovalRequests.length && isFiltered && !isApprovalRequestLoading
|
||||
) && (
|
||||
<div className="py-12">
|
||||
<EmptyState title="No Requests Match Filters" icon={faSearch} />
|
||||
</div>
|
||||
)}
|
||||
{Boolean(totalApprovalCount) && (
|
||||
<Pagination
|
||||
className="border-none"
|
||||
count={totalApprovalCount}
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={setPage}
|
||||
onChangePerPage={handlePerPageChange}
|
||||
/>
|
||||
)}
|
||||
{isApprovalRequestLoading && (
|
||||
<div>
|
||||
{Array.apply(0, Array(3)).map((_x, index) => (
|
||||
<div
|
||||
@@ -276,18 +379,7 @@ export const SecretApprovalRequest = () => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{hasNextApprovalPage && (
|
||||
<Button
|
||||
className="mt-4 text-sm"
|
||||
isFullWidth
|
||||
variant="star"
|
||||
isLoading={isFetchingNextApprovalRequest}
|
||||
isDisabled={isFetchingNextApprovalRequest || !hasNextApprovalPage}
|
||||
onClick={() => fetchNextApprovalRequest()}
|
||||
>
|
||||
{hasNextApprovalPage ? "Load More" : "End of history"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
@@ -56,27 +56,24 @@ export const generateCommitText = (commits: { op: CommitType }[] = [], isReplica
|
||||
if (score[CommitType.CREATE])
|
||||
text.push(
|
||||
<span key="created-commit">
|
||||
{score[CommitType.CREATE]} secret{score[CommitType.CREATE] !== 1 && "s"}
|
||||
<span style={{ color: "#60DD00" }}> created</span>
|
||||
{score[CommitType.CREATE]} Secret{score[CommitType.CREATE] !== 1 && "s"}
|
||||
<span className="text-green-600"> Created</span>
|
||||
</span>
|
||||
);
|
||||
if (score[CommitType.UPDATE])
|
||||
text.push(
|
||||
<span key="updated-commit">
|
||||
{Boolean(text.length) && ","}
|
||||
{score[CommitType.UPDATE]} secret{score[CommitType.UPDATE] !== 1 && "s"}
|
||||
<span style={{ color: "#F8EB30" }} className="text-orange-600">
|
||||
{" "}
|
||||
updated
|
||||
</span>
|
||||
{Boolean(text.length) && ", "}
|
||||
{score[CommitType.UPDATE]} Secret{score[CommitType.UPDATE] !== 1 && "s"}
|
||||
<span className="text-yellow-600"> Updated</span>
|
||||
</span>
|
||||
);
|
||||
if (score[CommitType.DELETE])
|
||||
text.push(
|
||||
<span className="deleted-commit">
|
||||
{Boolean(text.length) && "and"}
|
||||
{score[CommitType.DELETE]} secret{score[CommitType.UPDATE] !== 1 && "s"}
|
||||
<span style={{ color: "#F83030" }}> deleted</span>
|
||||
{score[CommitType.DELETE]} Secret{score[CommitType.DELETE] !== 1 && "s"}
|
||||
<span className="text-red-600"> Deleted</span>
|
||||
</span>
|
||||
);
|
||||
return text;
|
||||
|
@@ -37,7 +37,10 @@ const formSchema = z.object({
|
||||
policyArns: z.string().trim().optional(),
|
||||
tags: z
|
||||
.array(
|
||||
z.object({ key: z.string().trim().min(1).max(128), value: z.string().trim().min(1).max(256) })
|
||||
z.object({
|
||||
key: z.string().trim().min(1).max(128),
|
||||
value: z.string().trim().min(1).max(256)
|
||||
})
|
||||
)
|
||||
.optional()
|
||||
}),
|
||||
@@ -52,7 +55,10 @@ const formSchema = z.object({
|
||||
policyArns: z.string().trim().optional(),
|
||||
tags: z
|
||||
.array(
|
||||
z.object({ key: z.string().trim().min(1).max(128), value: z.string().trim().min(1).max(256) })
|
||||
z.object({
|
||||
key: z.string().trim().min(1).max(128),
|
||||
value: z.string().trim().min(1).max(256)
|
||||
})
|
||||
)
|
||||
.optional()
|
||||
})
|
||||
|
@@ -26,7 +26,7 @@ const formSchema = z.object({
|
||||
policyArns: z.string().trim().optional(),
|
||||
tags: z
|
||||
.array(z.object({ key: z.string().trim().min(1), value: z.string().trim().min(1) }))
|
||||
.optional(),
|
||||
.optional()
|
||||
}),
|
||||
z.object({
|
||||
method: z.literal(DynamicSecretAwsIamAuth.AssumeRole),
|
||||
@@ -97,7 +97,7 @@ export const EditDynamicSecretAwsIamForm = ({
|
||||
usernameTemplate: dynamicSecret?.usernameTemplate || "{{randomUsername}}",
|
||||
inputs: {
|
||||
...(dynamicSecret.inputs as TForm["inputs"])
|
||||
},
|
||||
}
|
||||
}
|
||||
});
|
||||
const isAccessKeyMethod = watch("inputs.method") === DynamicSecretAwsIamAuth.AccessKey;
|
||||
@@ -125,8 +125,7 @@ export const EditDynamicSecretAwsIamForm = ({
|
||||
defaultTTL,
|
||||
inputs,
|
||||
newName: newName === dynamicSecret.name ? undefined : newName,
|
||||
usernameTemplate:
|
||||
!usernameTemplate || isDefaultUsernameTemplate ? null : usernameTemplate
|
||||
usernameTemplate: !usernameTemplate || isDefaultUsernameTemplate ? null : usernameTemplate
|
||||
}
|
||||
});
|
||||
onClose();
|
||||
|
Reference in New Issue
Block a user