Compare commits

..

7 Commits

47 changed files with 3562 additions and 3569 deletions

View File

@ -0,0 +1,91 @@
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");
}
});
}

View File

@ -29,7 +29,12 @@ export const SuperAdminSchema = z.object({
adminIdentityIds: z.string().array().nullable().optional(), adminIdentityIds: z.string().array().nullable().optional(),
encryptedMicrosoftTeamsAppId: zodBuffer.nullable().optional(), encryptedMicrosoftTeamsAppId: zodBuffer.nullable().optional(),
encryptedMicrosoftTeamsClientSecret: zodBuffer.nullable().optional(), encryptedMicrosoftTeamsClientSecret: zodBuffer.nullable().optional(),
encryptedMicrosoftTeamsBotId: 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()
}); });
export type TSuperAdmin = z.infer<typeof SuperAdminSchema>; export type TSuperAdmin = z.infer<typeof SuperAdminSchema>;

View File

@ -89,7 +89,7 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
schema: { schema: {
querystring: z.object({ querystring: z.object({
projectSlug: z.string().trim(), projectSlug: z.string().trim(),
authorUserId: z.string().trim().optional(), authorProjectMembershipId: z.string().trim().optional(),
envSlug: z.string().trim().optional() envSlug: z.string().trim().optional()
}), }),
response: { response: {
@ -143,7 +143,7 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
handler: async (req) => { handler: async (req) => {
const { requests } = await server.services.accessApprovalRequest.listApprovalRequests({ const { requests } = await server.services.accessApprovalRequest.listApprovalRequests({
projectSlug: req.query.projectSlug, projectSlug: req.query.projectSlug,
authorUserId: req.query.authorUserId, authorProjectMembershipId: req.query.authorProjectMembershipId,
envSlug: req.query.envSlug, envSlug: req.query.envSlug,
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,

View File

@ -30,7 +30,6 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
workspaceId: z.string().trim(), workspaceId: z.string().trim(),
environment: z.string().trim().optional(), environment: z.string().trim().optional(),
committer: z.string().trim().optional(), committer: z.string().trim().optional(),
search: z.string().trim().optional(),
status: z.nativeEnum(RequestState).optional(), status: z.nativeEnum(RequestState).optional(),
limit: z.coerce.number().default(20), limit: z.coerce.number().default(20),
offset: z.coerce.number().default(0) offset: z.coerce.number().default(0)
@ -67,14 +66,13 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
userId: z.string().nullable().optional() userId: z.string().nullable().optional()
}) })
.array() .array()
}).array(), }).array()
totalCount: z.number()
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => { handler: async (req) => {
const { approvals, totalCount } = await server.services.secretApprovalRequest.getSecretApprovals({ const approvals = await server.services.secretApprovalRequest.getSecretApprovals({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
@ -82,7 +80,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
...req.query, ...req.query,
projectId: req.query.workspaceId projectId: req.query.workspaceId
}); });
return { approvals, totalCount }; return { approvals };
} }
}); });

View File

@ -725,17 +725,16 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR
) )
.where(`${TableName.Environment}.projectId`, projectId) .where(`${TableName.Environment}.projectId`, projectId)
.where(`${TableName.AccessApprovalPolicy}.deletedAt`, null)
.select(selectAllTableCols(TableName.AccessApprovalRequest)) .select(selectAllTableCols(TableName.AccessApprovalRequest))
.select(db.ref("status").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerStatus")) .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({ const formattedRequests = sqlNestRelationships({
data: accessRequests, data: accessRequests,
key: "id", key: "id",
parentMapper: (doc) => ({ parentMapper: (doc) => ({
...AccessApprovalRequestsSchema.parse(doc), ...AccessApprovalRequestsSchema.parse(doc)
isPolicyDeleted: Boolean(doc.policyDeletedAt)
}), }),
childrenMapper: [ childrenMapper: [
{ {
@ -752,8 +751,7 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR
(req) => (req) =>
!req.privilegeId && !req.privilegeId &&
!req.reviewers.some((r) => r.status === ApprovalStatus.REJECTED) && !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. // 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.
@ -761,8 +759,7 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR
(req) => (req) =>
req.privilegeId || req.privilegeId ||
req.reviewers.some((r) => r.status === ApprovalStatus.REJECTED) || 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 }; return { pendingCount: pendingApprovals.length, finalizedCount: finalizedApprovals.length };

View File

@ -275,7 +275,7 @@ export const accessApprovalRequestServiceFactory = ({
const listApprovalRequests: TAccessApprovalRequestServiceFactory["listApprovalRequests"] = async ({ const listApprovalRequests: TAccessApprovalRequestServiceFactory["listApprovalRequests"] = async ({
projectSlug, projectSlug,
authorUserId, authorProjectMembershipId,
envSlug, envSlug,
actor, actor,
actorOrgId, actorOrgId,
@ -300,8 +300,8 @@ export const accessApprovalRequestServiceFactory = ({
const policies = await accessApprovalPolicyDAL.find({ projectId: project.id }); const policies = await accessApprovalPolicyDAL.find({ projectId: project.id });
let requests = await accessApprovalRequestDAL.findRequestsWithPrivilegeByPolicyIds(policies.map((p) => p.id)); let requests = await accessApprovalRequestDAL.findRequestsWithPrivilegeByPolicyIds(policies.map((p) => p.id));
if (authorUserId) { if (authorProjectMembershipId) {
requests = requests.filter((request) => request.requestedByUserId === authorUserId); requests = requests.filter((request) => request.requestedByUserId === actorId);
} }
if (envSlug) { if (envSlug) {

View File

@ -31,7 +31,7 @@ export type TCreateAccessApprovalRequestDTO = {
export type TListApprovalRequestsDTO = { export type TListApprovalRequestsDTO = {
projectSlug: string; projectSlug: string;
authorUserId?: string; authorProjectMembershipId?: string;
envSlug?: string; envSlug?: string;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;

View File

@ -24,7 +24,6 @@ type TFindQueryFilter = {
committer?: string; committer?: string;
limit?: number; limit?: number;
offset?: number; offset?: number;
search?: string;
}; };
export const secretApprovalRequestDALFactory = (db: TDbClient) => { export const secretApprovalRequestDALFactory = (db: TDbClient) => {
@ -315,6 +314,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
.where(`${TableName.SecretApprovalPolicyApprover}.approverUserId`, userId) .where(`${TableName.SecretApprovalPolicyApprover}.approverUserId`, userId)
.orWhere(`${TableName.SecretApprovalRequest}.committerUserId`, userId) .orWhere(`${TableName.SecretApprovalRequest}.committerUserId`, userId)
) )
.andWhere((bd) => void bd.where(`${TableName.SecretApprovalPolicy}.deletedAt`, null))
.select("status", `${TableName.SecretApprovalRequest}.id`) .select("status", `${TableName.SecretApprovalRequest}.id`)
.groupBy(`${TableName.SecretApprovalRequest}.id`, "status") .groupBy(`${TableName.SecretApprovalRequest}.id`, "status")
.count("status") .count("status")
@ -340,13 +340,13 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
}; };
const findByProjectId = async ( const findByProjectId = async (
{ status, limit = 20, offset = 0, projectId, committer, environment, userId, search }: TFindQueryFilter, { status, limit = 20, offset = 0, projectId, committer, environment, userId }: TFindQueryFilter,
tx?: Knex tx?: Knex
) => { ) => {
try { try {
// akhilmhdh: If ever u wanted a 1 to so many relationship connected with pagination // akhilmhdh: If ever u wanted a 1 to so many relationship connected with pagination
// this is the place u wanna look at. // this is the place u wanna look at.
const innerQuery = (tx || db.replicaNode())(TableName.SecretApprovalRequest) const query = (tx || db.replicaNode())(TableName.SecretApprovalRequest)
.join(TableName.SecretFolder, `${TableName.SecretApprovalRequest}.folderId`, `${TableName.SecretFolder}.id`) .join(TableName.SecretFolder, `${TableName.SecretApprovalRequest}.folderId`, `${TableName.SecretFolder}.id`)
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`) .join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
.join( .join(
@ -435,30 +435,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
db.ref("firstName").withSchema("committerUser").as("committerUserFirstName"), db.ref("firstName").withSchema("committerUser").as("committerUserFirstName"),
db.ref("lastName").withSchema("committerUser").as("committerUserLastName") db.ref("lastName").withSchema("committerUser").as("committerUserLastName")
) )
.distinctOn(`${TableName.SecretApprovalRequest}.id`) .orderBy("createdAt", "desc");
.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) const docs = await (tx || db)
.with("w", query) .with("w", query)
@ -466,10 +443,6 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
.from<Awaited<typeof query>[number]>("w") .from<Awaited<typeof query>[number]>("w")
.where("w.rank", ">=", offset) .where("w.rank", ">=", offset)
.andWhere("w.rank", "<", offset + limit); .andWhere("w.rank", "<", offset + limit);
// @ts-expect-error knex does not infer
const totalCount = Number(docs[0]?.total_count || 0);
const formattedDoc = sqlNestRelationships({ const formattedDoc = sqlNestRelationships({
data: docs, data: docs,
key: "id", key: "id",
@ -531,26 +504,23 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
} }
] ]
}); });
return { return formattedDoc.map((el) => ({
approvals: formattedDoc.map((el) => ({
...el, ...el,
policy: { ...el.policy, approvers: el.approvers, bypassers: el.bypassers } policy: { ...el.policy, approvers: el.approvers, bypassers: el.bypassers }
})), }));
totalCount
};
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "FindSAR" }); throw new DatabaseError({ error, name: "FindSAR" });
} }
}; };
const findByProjectIdBridgeSecretV2 = async ( const findByProjectIdBridgeSecretV2 = async (
{ status, limit = 20, offset = 0, projectId, committer, environment, userId, search }: TFindQueryFilter, { status, limit = 20, offset = 0, projectId, committer, environment, userId }: TFindQueryFilter,
tx?: Knex tx?: Knex
) => { ) => {
try { try {
// akhilmhdh: If ever u wanted a 1 to so many relationship connected with pagination // akhilmhdh: If ever u wanted a 1 to so many relationship connected with pagination
// this is the place u wanna look at. // this is the place u wanna look at.
const innerQuery = (tx || db.replicaNode())(TableName.SecretApprovalRequest) const query = (tx || db.replicaNode())(TableName.SecretApprovalRequest)
.join(TableName.SecretFolder, `${TableName.SecretApprovalRequest}.folderId`, `${TableName.SecretFolder}.id`) .join(TableName.SecretFolder, `${TableName.SecretApprovalRequest}.folderId`, `${TableName.SecretFolder}.id`)
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`) .join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
.join( .join(
@ -639,42 +609,14 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
db.ref("firstName").withSchema("committerUser").as("committerUserFirstName"), db.ref("firstName").withSchema("committerUser").as("committerUserFirstName"),
db.ref("lastName").withSchema("committerUser").as("committerUserLastName") db.ref("lastName").withSchema("committerUser").as("committerUserLastName")
) )
.distinctOn(`${TableName.SecretApprovalRequest}.id`) .orderBy("createdAt", "desc");
.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) const docs = await (tx || db)
.with("w", query) .with("w", query)
.select("*") .select("*")
.from<Awaited<typeof query>[number]>("w") .from<Awaited<typeof query>[number]>("w")
.where("w.rank", ">=", rankOffset) .where("w.rank", ">=", offset)
.andWhere("w.rank", "<", rankOffset + limit); .andWhere("w.rank", "<", offset + limit);
// @ts-expect-error knex does not infer
const totalCount = Number(docs[0]?.total_count || 0);
const formattedDoc = sqlNestRelationships({ const formattedDoc = sqlNestRelationships({
data: docs, data: docs,
key: "id", key: "id",
@ -740,13 +682,10 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
} }
] ]
}); });
return { return formattedDoc.map((el) => ({
approvals: formattedDoc.map((el) => ({
...el, ...el,
policy: { ...el.policy, approvers: el.approvers, bypassers: el.bypassers } policy: { ...el.policy, approvers: el.approvers, bypassers: el.bypassers }
})), }));
totalCount
};
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "FindSAR" }); throw new DatabaseError({ error, name: "FindSAR" });
} }

View File

@ -194,8 +194,7 @@ export const secretApprovalRequestServiceFactory = ({
environment, environment,
committer, committer,
limit, limit,
offset, offset
search
}: TListApprovalsDTO) => { }: TListApprovalsDTO) => {
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" }); if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
@ -209,7 +208,6 @@ export const secretApprovalRequestServiceFactory = ({
}); });
const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId); const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
if (shouldUseSecretV2Bridge) { if (shouldUseSecretV2Bridge) {
return secretApprovalRequestDAL.findByProjectIdBridgeSecretV2({ return secretApprovalRequestDAL.findByProjectIdBridgeSecretV2({
projectId, projectId,
@ -218,21 +216,19 @@ export const secretApprovalRequestServiceFactory = ({
status, status,
userId: actorId, userId: actorId,
limit, limit,
offset, offset
search
}); });
} }
const approvals = await secretApprovalRequestDAL.findByProjectId({
return secretApprovalRequestDAL.findByProjectId({
projectId, projectId,
committer, committer,
environment, environment,
status, status,
userId: actorId, userId: actorId,
limit, limit,
offset, offset
search
}); });
return approvals;
}; };
const getSecretApprovalDetails = async ({ const getSecretApprovalDetails = async ({

View File

@ -93,7 +93,6 @@ export type TListApprovalsDTO = {
committer?: string; committer?: string;
limit?: number; limit?: number;
offset?: number; offset?: number;
search?: string;
} & TProjectPermission; } & TProjectPermission;
export type TSecretApprovalDetailsDTO = { export type TSecretApprovalDetailsDTO = {

View File

@ -2020,10 +2020,16 @@ export const registerRoutes = async (
if (licenseSyncJob) { if (licenseSyncJob) {
cronJobs.push(licenseSyncJob); cronJobs.push(licenseSyncJob);
} }
const microsoftTeamsSyncJob = await microsoftTeamsService.initializeBackgroundSync(); const microsoftTeamsSyncJob = await microsoftTeamsService.initializeBackgroundSync();
if (microsoftTeamsSyncJob) { if (microsoftTeamsSyncJob) {
cronJobs.push(microsoftTeamsSyncJob); cronJobs.push(microsoftTeamsSyncJob);
} }
const adminIntegrationsSyncJob = await superAdminService.initializeAdminIntegrationConfigSync();
if (adminIntegrationsSyncJob) {
cronJobs.push(adminIntegrationsSyncJob);
}
} }
server.decorate<FastifyZodProvider["store"]>("store", { server.decorate<FastifyZodProvider["store"]>("store", {

View File

@ -37,7 +37,12 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
encryptedSlackClientSecret: true, encryptedSlackClientSecret: true,
encryptedMicrosoftTeamsAppId: true, encryptedMicrosoftTeamsAppId: true,
encryptedMicrosoftTeamsClientSecret: true, encryptedMicrosoftTeamsClientSecret: true,
encryptedMicrosoftTeamsBotId: true encryptedMicrosoftTeamsBotId: true,
encryptedGitHubAppConnectionClientId: true,
encryptedGitHubAppConnectionClientSecret: true,
encryptedGitHubAppConnectionSlug: true,
encryptedGitHubAppConnectionId: true,
encryptedGitHubAppConnectionPrivateKey: true
}).extend({ }).extend({
isMigrationModeOn: z.boolean(), isMigrationModeOn: z.boolean(),
defaultAuthOrgSlug: z.string().nullable(), defaultAuthOrgSlug: z.string().nullable(),
@ -87,6 +92,11 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
microsoftTeamsAppId: z.string().optional(), microsoftTeamsAppId: z.string().optional(),
microsoftTeamsClientSecret: z.string().optional(), microsoftTeamsClientSecret: z.string().optional(),
microsoftTeamsBotId: 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 authConsentContent: z
.string() .string()
.trim() .trim()
@ -348,6 +358,13 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
appId: z.string(), appId: z.string(),
clientSecret: z.string(), clientSecret: z.string(),
botId: z.string() botId: z.string()
}),
gitHubAppConnection: z.object({
clientId: z.string(),
clientSecret: z.string(),
appSlug: z.string(),
appId: z.string(),
privateKey: z.string()
}) })
}) })
} }

View File

@ -7,6 +7,7 @@ import { request } from "@app/lib/config/request";
import { BadRequestError, ForbiddenRequestError, InternalServerError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, InternalServerError } from "@app/lib/errors";
import { getAppConnectionMethodName } from "@app/services/app-connection/app-connection-fns"; import { getAppConnectionMethodName } from "@app/services/app-connection/app-connection-fns";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; 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 { AppConnection } from "../app-connection-enums";
import { GitHubConnectionMethod } from "./github-connection-enums"; import { GitHubConnectionMethod } from "./github-connection-enums";
@ -14,13 +15,14 @@ import { TGitHubConnection, TGitHubConnectionConfig } from "./github-connection-
export const getGitHubConnectionListItem = () => { export const getGitHubConnectionListItem = () => {
const { INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID, INF_APP_CONNECTION_GITHUB_APP_SLUG } = getConfig(); const { INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID, INF_APP_CONNECTION_GITHUB_APP_SLUG } = getConfig();
const { gitHubAppConnection } = getInstanceIntegrationsConfig();
return { return {
name: "GitHub" as const, name: "GitHub" as const,
app: AppConnection.GitHub as const, app: AppConnection.GitHub as const,
methods: Object.values(GitHubConnectionMethod) as [GitHubConnectionMethod.App, GitHubConnectionMethod.OAuth], methods: Object.values(GitHubConnectionMethod) as [GitHubConnectionMethod.App, GitHubConnectionMethod.OAuth],
oauthClientId: INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID, oauthClientId: INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID,
appClientSlug: INF_APP_CONNECTION_GITHUB_APP_SLUG appClientSlug: gitHubAppConnection.appSlug || INF_APP_CONNECTION_GITHUB_APP_SLUG
}; };
}; };
@ -30,23 +32,24 @@ export const getGitHubClient = (appConnection: TGitHubConnection) => {
const { method, credentials } = appConnection; const { method, credentials } = appConnection;
let client: Octokit; 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) { switch (method) {
case GitHubConnectionMethod.App: case GitHubConnectionMethod.App:
if (!appCfg.INF_APP_CONNECTION_GITHUB_APP_ID || !appCfg.INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY) { if (!appId || !appPrivateKey) {
throw new InternalServerError({ throw new InternalServerError({
message: `GitHub ${getAppConnectionMethodName(method).replace( message: `GitHub ${getAppConnectionMethodName(method).replace("GitHub", "")} has not been configured`
"GitHub",
""
)} environment variables have not been configured`
}); });
} }
client = new Octokit({ client = new Octokit({
authStrategy: createAppAuth, authStrategy: createAppAuth,
auth: { auth: {
appId: appCfg.INF_APP_CONNECTION_GITHUB_APP_ID, appId,
privateKey: appCfg.INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY, privateKey: appPrivateKey,
installationId: credentials.installationId installationId: credentials.installationId
} }
}); });
@ -154,6 +157,8 @@ type TokenRespData = {
export const validateGitHubConnectionCredentials = async (config: TGitHubConnectionConfig) => { export const validateGitHubConnectionCredentials = async (config: TGitHubConnectionConfig) => {
const { credentials, method } = config; const { credentials, method } = config;
const { gitHubAppConnection } = getInstanceIntegrationsConfig();
const { const {
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID, INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID,
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_SECRET, INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_SECRET,
@ -165,8 +170,8 @@ export const validateGitHubConnectionCredentials = async (config: TGitHubConnect
const { clientId, clientSecret } = const { clientId, clientSecret } =
method === GitHubConnectionMethod.App method === GitHubConnectionMethod.App
? { ? {
clientId: INF_APP_CONNECTION_GITHUB_APP_CLIENT_ID, clientId: gitHubAppConnection.clientId || INF_APP_CONNECTION_GITHUB_APP_CLIENT_ID,
clientSecret: INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET clientSecret: gitHubAppConnection.clientSecret || INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET
} }
: // oauth : // oauth
{ {

View File

@ -1,4 +1,5 @@
import bcrypt from "bcrypt"; import bcrypt from "bcrypt";
import { CronJob } from "cron";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import { IdentityAuthMethod, OrgMembershipRole, TSuperAdmin, TSuperAdminUpdate } from "@app/db/schemas"; import { IdentityAuthMethod, OrgMembershipRole, TSuperAdmin, TSuperAdminUpdate } from "@app/db/schemas";
@ -8,6 +9,7 @@ import { getConfig } from "@app/lib/config/env";
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { generateUserSrpKeys, getUserPrivateKey } from "@app/lib/crypto/srp"; import { generateUserSrpKeys, getUserPrivateKey } from "@app/lib/crypto/srp";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { TIdentityDALFactory } from "@app/services/identity/identity-dal"; import { TIdentityDALFactory } from "@app/services/identity/identity-dal";
import { TAuthLoginFactory } from "../auth/auth-login-service"; import { TAuthLoginFactory } from "../auth/auth-login-service";
@ -35,6 +37,7 @@ import {
TAdminBootstrapInstanceDTO, TAdminBootstrapInstanceDTO,
TAdminGetIdentitiesDTO, TAdminGetIdentitiesDTO,
TAdminGetUsersDTO, TAdminGetUsersDTO,
TAdminIntegrationConfig,
TAdminSignUpDTO, TAdminSignUpDTO,
TGetOrganizationsDTO TGetOrganizationsDTO
} from "./super-admin-types"; } from "./super-admin-types";
@ -70,6 +73,31 @@ 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 = "infisical-admin-cfg";
const ADMIN_CONFIG_KEY_EXP = 60; // 60s const ADMIN_CONFIG_KEY_EXP = 60; // 60s
export const ADMIN_CONFIG_DB_UUID = "00000000-0000-0000-0000-000000000000"; export const ADMIN_CONFIG_DB_UUID = "00000000-0000-0000-0000-000000000000";
@ -138,6 +166,74 @@ export const superAdminServiceFactory = ({
return serverCfg; 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 ( const updateServerCfg = async (
data: TSuperAdminUpdate & { data: TSuperAdminUpdate & {
slackClientId?: string; slackClientId?: string;
@ -145,6 +241,11 @@ export const superAdminServiceFactory = ({
microsoftTeamsAppId?: string; microsoftTeamsAppId?: string;
microsoftTeamsClientSecret?: string; microsoftTeamsClientSecret?: string;
microsoftTeamsBotId?: string; microsoftTeamsBotId?: string;
gitHubAppConnectionClientId?: string;
gitHubAppConnectionClientSecret?: string;
gitHubAppConnectionSlug?: string;
gitHubAppConnectionId?: string;
gitHubAppConnectionPrivateKey?: string;
}, },
userId: string userId: string
) => { ) => {
@ -236,10 +337,51 @@ export const superAdminServiceFactory = ({
updatedData.microsoftTeamsBotId = undefined; updatedData.microsoftTeamsBotId = undefined;
microsoftTeamsSettingsUpdated = true; 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); const updatedServerCfg = await serverCfgDAL.updateById(ADMIN_CONFIG_DB_UUID, updatedData);
await keyStore.setItemWithExpiry(ADMIN_CONFIG_KEY, ADMIN_CONFIG_KEY_EXP, JSON.stringify(updatedServerCfg)); await keyStore.setItemWithExpiry(ADMIN_CONFIG_KEY, ADMIN_CONFIG_KEY_EXP, JSON.stringify(updatedServerCfg));
if (gitHubAppConnectionSettingsUpdated) {
await $syncAdminIntegrationConfig();
}
if ( if (
updatedServerCfg.encryptedMicrosoftTeamsAppId && updatedServerCfg.encryptedMicrosoftTeamsAppId &&
updatedServerCfg.encryptedMicrosoftTeamsClientSecret && updatedServerCfg.encryptedMicrosoftTeamsClientSecret &&
@ -593,43 +735,6 @@ export const superAdminServiceFactory = ({
await userDAL.updateById(userId, { superAdmin: true }); 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 getConfiguredEncryptionStrategies = async () => {
const appCfg = getConfig(); const appCfg = getConfig();
@ -696,6 +801,19 @@ export const superAdminServiceFactory = ({
return (await keyStore.getItem("invalidating-cache")) !== null; 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 { return {
initServerCfg, initServerCfg,
updateServerCfg, updateServerCfg,
@ -714,6 +832,7 @@ export const superAdminServiceFactory = ({
checkIfInvalidatingCache, checkIfInvalidatingCache,
getOrganizations, getOrganizations,
deleteOrganization, deleteOrganization,
deleteOrganizationMembership deleteOrganizationMembership,
initializeAdminIntegrationConfigSync
}; };
}; };

View File

@ -55,3 +55,22 @@ export enum CacheType {
ALL = "all", ALL = "all",
SECRETS = "secrets" 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;
};
};

View File

@ -11,9 +11,9 @@ Fairly frequently, you might run into situations when you need to spend company
As a perk of working at Infisical, we cover some of your meal expenses. As a perk of working at Infisical, we cover some of your meal expenses.
**HQ team members**: meals and unlimited snacks are provided **on-site** at no cost. HQ team members: meals and unlimited snacks are provided on-site at no cost.
**Remote team members**: a food stipend is allocated based on location. Remote team members: a food stipend is allocated based on location.
# Trivial expenses # Trivial expenses
@ -27,28 +27,21 @@ This means expenses that are:
Please spend money in a way that you think is in the best interest of the company. Please spend money in a way that you think is in the best interest of the company.
</Note> </Note>
## Saving receipts
# Travel Make sure you keep copies for all receipts. If you expense something on a company card and cannot provide a receipt, this may be deducted from your pay.
If you need to travel on Infisicals behalf for in-person onboarding, meeting customers, and offsites, again please spend money in the best interests of the company. You should default to using your company card in all cases - it has no transaction fees. If using your personal card is unavoidable, please reach out to Maidul to get it reimbursed manually.
We do not pre-approve your travel expenses, and trust team members to make the right decisions here. Some guidance: ## Training
- Please find a flight ticket that is reasonably priced. We all travel economy by default we cannot afford for folks to fly premium or business class. Feel free to upgrade using your personal money/airmiles if youd like to.
- Feel free to pay for the Uber/subway/bus to and from the airport with your Brex card.
- For business travel, Infisical will cover reasonable expenses for breakfast, lunch, and dinner.
- When traveling internationally, Infisical does not cover roaming charges for your phone. You can expense a reasonable eSIM, which usually is no more than $20.
<Note>
Note that this only applies to business travel. It is not applicable for personal travel or day-to-day commuting.
</Note>
For engineers, youre welcome to take an approved Udemy course. Please reach out to Maidul. For the GTM team, you may buy a book a month if its relevant to your work.
# Equipment # Equipment
Infisical is a remote first company so we understand the importance of having a comfortable work setup. To support this, we provide allowances for essential office equipment. Infisical is a remote first company so we understand the importance of having a comfortable work setup. To support this, we provide allowances for essential office equipment.
### 1. Desk & Chair ### Desk & Chair
Most people already have a comfortable desk and chair, but if you need an upgrade, we offer the following allowances. Most people already have a comfortable desk and chair, but if you need an upgrade, we offer the following allowances.
While we're not yet able to provide the latest and greatest, we strive to be reasonable given the stage of our company. While we're not yet able to provide the latest and greatest, we strive to be reasonable given the stage of our company.
@ -57,10 +50,10 @@ While we're not yet able to provide the latest and greatest, we strive to be rea
**Chair**: $150 USD **Chair**: $150 USD
### 2. Laptop ### Laptop
Each team member will receive a company-issued Macbook Pro before they start their first day. Each team member will receive a company-issued Macbook Pro before they start their first day.
### 3. Notes ### Notes
1. All equipment purchased using company allowances remains the property of Infisical. 1. All equipment purchased using company allowances remains the property of Infisical.
2. Keep all receipts for equipment purchases and submit them for reimbursement. 2. Keep all receipts for equipment purchases and submit them for reimbursement.
@ -72,28 +65,6 @@ This is because we don't yet have a formal HR department to handle such logistic
For any equipment related questions, please reach out to Maidul. For any equipment related questions, please reach out to Maidul.
# Brex ## Brex
We use Brex as our primary credit card provider. Don't have a company card yet? Reach out to Maidul. We use Brex as our primary credit card provider. Don't have a company card yet? Reach out to Maidul.
### Budgets
You will generally have multiple budgets assigned to you. "General Company Expenses" primarily covers quick SaaS purchases (not food). Remote team members should have a "Lunch Stipend" budget that applies to food.
If your position involves a lot of travel, you may also have a "Travel" budget that applies to expenses related to business travel (e.g., you can not use it for transportation or food during personal travel).
### Saving receipts
Make sure you keep copies for all receipts. If you expense something on a company card and cannot provide a receipt, this may be deducted from your pay.
You should default to using your company card in all cases - it has no transaction fees. If using your personal card is unavoidable, please reach out to Maidul to get it reimbursed manually.
### Need a one-off budget increase?
You can do this directly within Brex - just request the amount and duration for the relevant budget in the app, and your hiring manager will automatically be notified for approval.
# Training
For engineers, youre welcome to take an approved Udemy course. Please reach out to Maidul. For the GTM team, you may buy a book a month if its relevant to your work.

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 610 KiB

View File

@ -53,7 +53,22 @@ 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. Obtain the necessary Github application credentials. This would be the application slug, client ID, app ID, client secret, and private key.
![integrations github app credentials](/images/integrations/github/app/self-hosted-github-app-credentials.png) ![integrations github app credentials](/images/integrations/github/app/self-hosted-github-app-credentials.png)
Back in your Infisical instance, add the five new environment variables for the credentials of your GitHub application: 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:
![integrations github app admin panel](/images/integrations/github/app/self-hosted-github-app-admin-panel.png)
- **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:
- `INF_APP_CONNECTION_GITHUB_APP_CLIENT_ID`: The **Client ID** 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. - `INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET`: The **Client Secret** of your GitHub application.
@ -61,7 +76,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_ID`: The **App ID** of your GitHub application.
- `INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY`: The **Private Key** of your GitHub application. - `INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY`: The **Private Key** of your GitHub application.
Once added, restart your Infisical instance and use the GitHub integration via app authentication. 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.
</Step> </Step>
</Steps> </Steps>
</Accordion> </Accordion>
@ -158,4 +173,5 @@ Infisical supports two methods for connecting to GitHub.
</Step> </Step>
</Steps> </Steps>
</Tab> </Tab>
</Tabs> </Tabs>

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

2215
docs/mint.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,3 @@
* {
border-radius: 0 !important;
}
#navbar .max-w-8xl { #navbar .max-w-8xl {
max-width: 100%; max-width: 100%;
border-bottom: 1px solid #ebebeb; border-bottom: 1px solid #ebebeb;
@ -30,20 +26,24 @@
} }
#sidebar li > div.mt-2 { #sidebar li > div.mt-2 {
border-radius: 0;
padding: 5px; padding: 5px;
} }
#sidebar li > a.text-primary { #sidebar li > a.text-primary {
border-radius: 0;
background-color: #FBFFCC; background-color: #FBFFCC;
border-left: 4px solid #EFFF33; border-left: 4px solid #EFFF33;
padding: 5px; padding: 5px;
} }
#sidebar li > a.mt-2 { #sidebar li > a.mt-2 {
border-radius: 0;
padding: 5px; padding: 5px;
} }
#sidebar li > a.leading-6 { #sidebar li > a.leading-6 {
border-radius: 0;
padding: 0px; padding: 0px;
} }
@ -68,26 +68,65 @@
} }
#content-area .mt-8 .block{ #content-area .mt-8 .block{
border-radius: 0;
border-width: 1px; border-width: 1px;
background-color: #FCFBFA; background-color: #FCFBFA;
border-color: #ebebeb; border-color: #ebebeb;
} }
/* #content-area:hover .mt-8 .block:hover{ /* #content-area:hover .mt-8 .block:hover{
border-radius: 0;
border-width: 1px; border-width: 1px;
background-color: #FDFFE5; background-color: #FDFFE5;
border-color: #EFFF33; 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{ #content-area div.my-4{
border-radius: 0;
border-width: 1px; border-width: 1px;
} }
/* #content-area div.flex-1 { #content-area div.flex-1 {
/* text-transform: uppercase; */
opacity: 0.8; opacity: 0.8;
font-weight: 400; font-weight: 400;
} */ }
#content-area button {
border-radius: 0;
}
#content-area a {
border-radius: 0;
}
#content-area .not-prose {
border-radius: 0;
}
/* .eyebrow { /* .eyebrow {
text-transform: uppercase; text-transform: uppercase;

View File

@ -94,7 +94,7 @@ export const DropdownMenuItem = <T extends ElementType = "button">({
className={twMerge( 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", "block cursor-pointer rounded-sm px-4 py-2 font-inter text-xs text-mineshaft-200 outline-none data-[highlighted]:bg-mineshaft-700",
className, className,
isDisabled ? "pointer-events-none cursor-not-allowed opacity-50" : "" isDisabled ? "pointer-events-none opacity-50" : ""
)} )}
> >
<Item type="button" role="menuitem" className="flex w-full items-center" ref={inputRef}> <Item type="button" role="menuitem" className="flex w-full items-center" ref={inputRef}>

View File

@ -16,6 +16,12 @@ export const ROUTE_PATHS = Object.freeze({
PasswordResetPage: setRoute("/password-reset", "/_restrict-login-signup/password-reset"), PasswordResetPage: setRoute("/password-reset", "/_restrict-login-signup/password-reset"),
PasswordSetupPage: setRoute("/password-setup", "/_authenticate/password-setup") PasswordSetupPage: setRoute("/password-setup", "/_authenticate/password-setup")
}, },
Admin: {
IntegrationsPage: setRoute(
"/admin/integrations",
"/_authenticate/_inject-org-details/admin/_admin-layout/integrations"
)
},
Organization: { Organization: {
Settings: { Settings: {
OauthCallbackPage: setRoute( OauthCallbackPage: setRoute(

View File

@ -1,20 +1,12 @@
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"; import { PolicyType } from "@app/hooks/api/policies/enums";
export const policyDetails: Record< export const policyDetails: Record<PolicyType, { name: string; className: string }> = {
PolicyType,
{ name: string; className: string; icon: IconDefinition }
> = {
[PolicyType.AccessPolicy]: { [PolicyType.AccessPolicy]: {
className: "bg-green/20 text-green", className: "bg-lime-900 text-lime-100",
name: "Access Policy", name: "Access Policy"
icon: faArrowRightToBracket
}, },
[PolicyType.ChangePolicy]: { [PolicyType.ChangePolicy]: {
className: "bg-yellow/20 text-yellow", className: "bg-indigo-900 text-indigo-100",
name: "Change Policy", name: "Change Policy"
icon: faEdit
} }
}; };

View File

@ -65,11 +65,11 @@ const fetchApprovalPolicies = async ({ projectSlug }: TGetAccessApprovalRequests
const fetchApprovalRequests = async ({ const fetchApprovalRequests = async ({
projectSlug, projectSlug,
envSlug, envSlug,
authorUserId authorProjectMembershipId
}: TGetAccessApprovalRequestsDTO) => { }: TGetAccessApprovalRequestsDTO) => {
const { data } = await apiRequest.get<{ requests: TAccessApprovalRequest[] }>( const { data } = await apiRequest.get<{ requests: TAccessApprovalRequest[] }>(
"/api/v1/access-approvals/requests", "/api/v1/access-approvals/requests",
{ params: { projectSlug, envSlug, authorUserId } } { params: { projectSlug, envSlug, authorProjectMembershipId } }
); );
return data.requests.map((request) => ({ return data.requests.map((request) => ({
@ -109,12 +109,12 @@ export const useGetAccessRequestsCount = ({
export const useGetAccessApprovalPolicies = ({ export const useGetAccessApprovalPolicies = ({
projectSlug, projectSlug,
envSlug, envSlug,
authorUserId, authorProjectMembershipId,
options = {} options = {}
}: TGetAccessApprovalRequestsDTO & TReactQueryOptions) => }: TGetAccessApprovalRequestsDTO & TReactQueryOptions) =>
useQuery({ useQuery({
queryKey: accessApprovalKeys.getAccessApprovalPolicies(projectSlug), queryKey: accessApprovalKeys.getAccessApprovalPolicies(projectSlug),
queryFn: () => fetchApprovalPolicies({ projectSlug, envSlug, authorUserId }), queryFn: () => fetchApprovalPolicies({ projectSlug, envSlug, authorProjectMembershipId }),
...options, ...options,
enabled: Boolean(projectSlug) && (options?.enabled ?? true) enabled: Boolean(projectSlug) && (options?.enabled ?? true)
}); });
@ -122,13 +122,16 @@ export const useGetAccessApprovalPolicies = ({
export const useGetAccessApprovalRequests = ({ export const useGetAccessApprovalRequests = ({
projectSlug, projectSlug,
envSlug, envSlug,
authorUserId, authorProjectMembershipId,
options = {} options = {}
}: TGetAccessApprovalRequestsDTO & TReactQueryOptions) => }: TGetAccessApprovalRequestsDTO & TReactQueryOptions) =>
useQuery({ useQuery({
queryKey: accessApprovalKeys.getAccessApprovalRequests(projectSlug, envSlug, authorUserId), queryKey: accessApprovalKeys.getAccessApprovalRequests(
queryFn: () => fetchApprovalRequests({ projectSlug, envSlug, authorUserId }), projectSlug,
envSlug,
authorProjectMembershipId
),
queryFn: () => fetchApprovalRequests({ projectSlug, envSlug, authorProjectMembershipId }),
...options, ...options,
enabled: Boolean(projectSlug) && (options?.enabled ?? true), enabled: Boolean(projectSlug) && (options?.enabled ?? true)
placeholderData: (previousData) => previousData
}); });

View File

@ -148,7 +148,7 @@ export type TCreateAccessRequestDTO = {
export type TGetAccessApprovalRequestsDTO = { export type TGetAccessApprovalRequestsDTO = {
projectSlug: string; projectSlug: string;
envSlug?: string; envSlug?: string;
authorUserId?: string; authorProjectMembershipId?: string;
}; };
export type TGetAccessPolicyApprovalCountDTO = { export type TGetAccessPolicyApprovalCountDTO = {

View File

@ -56,6 +56,11 @@ export type TUpdateServerConfigDTO = {
microsoftTeamsAppId?: string; microsoftTeamsAppId?: string;
microsoftTeamsClientSecret?: string; microsoftTeamsClientSecret?: string;
microsoftTeamsBotId?: string; microsoftTeamsBotId?: string;
gitHubAppConnectionClientId?: string;
gitHubAppConnectionClientSecret?: string;
gitHubAppConnectionSlug?: string;
gitHubAppConnectionId?: string;
gitHubAppConnectionPrivateKey?: string;
} & Partial<TServerConfig>; } & Partial<TServerConfig>;
export type TCreateAdminUserDTO = { export type TCreateAdminUserDTO = {
@ -100,6 +105,13 @@ export type AdminIntegrationsConfig = {
clientSecret: string; clientSecret: string;
botId: string; botId: string;
}; };
gitHubAppConnection: {
clientId: string;
clientSecret: string;
appSlug: string;
appId: string;
privateKey: string;
};
}; };
export type TGetServerRootKmsEncryptionDetails = { export type TGetServerRootKmsEncryptionDetails = {

View File

@ -1,5 +1,5 @@
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
import { useQuery, UseQueryOptions } from "@tanstack/react-query"; import { useInfiniteQuery, useQuery, UseQueryOptions } from "@tanstack/react-query";
import { import {
decryptAssymmetric, decryptAssymmetric,
@ -25,11 +25,10 @@ export const secretApprovalRequestKeys = {
status, status,
committer, committer,
offset, offset,
limit, limit
search
}: TGetSecretApprovalRequestList) => }: TGetSecretApprovalRequestList) =>
[ [
{ workspaceId, environment, status, committer, offset, limit, search }, { workspaceId, environment, status, committer, offset, limit },
"secret-approval-requests" "secret-approval-requests"
] as const, ] as const,
detail: ({ id }: Omit<TGetSecretApprovalRequestDetails, "decryptKey">) => detail: ({ id }: Omit<TGetSecretApprovalRequestDetails, "decryptKey">) =>
@ -119,25 +118,23 @@ const fetchSecretApprovalRequestList = async ({
committer, committer,
status = "open", status = "open",
limit = 20, limit = 20,
offset = 0, offset
search = ""
}: TGetSecretApprovalRequestList) => { }: TGetSecretApprovalRequestList) => {
const { data } = await apiRequest.get<{ const { data } = await apiRequest.get<{ approvals: TSecretApprovalRequest[] }>(
approvals: TSecretApprovalRequest[]; "/api/v1/secret-approval-requests",
totalCount: number; {
}>("/api/v1/secret-approval-requests", {
params: { params: {
workspaceId, workspaceId,
environment, environment,
committer, committer,
status, status,
limit, limit,
offset, offset
search
} }
}); }
);
return data; return data.approvals;
}; };
export const useGetSecretApprovalRequests = ({ export const useGetSecretApprovalRequests = ({
@ -146,32 +143,31 @@ export const useGetSecretApprovalRequests = ({
options = {}, options = {},
status, status,
limit = 20, limit = 20,
offset = 0,
search,
committer committer
}: TGetSecretApprovalRequestList & TReactQueryOptions) => }: TGetSecretApprovalRequestList & TReactQueryOptions) =>
useQuery({ useInfiniteQuery({
initialPageParam: 0,
queryKey: secretApprovalRequestKeys.list({ queryKey: secretApprovalRequestKeys.list({
workspaceId, workspaceId,
environment, environment,
committer, committer,
status, status
limit,
search,
offset
}), }),
queryFn: () => queryFn: ({ pageParam }) =>
fetchSecretApprovalRequestList({ fetchSecretApprovalRequestList({
workspaceId, workspaceId,
environment, environment,
status, status,
committer, committer,
limit, limit,
offset, offset: pageParam
search
}), }),
enabled: Boolean(workspaceId) && (options?.enabled ?? true), enabled: Boolean(workspaceId) && (options?.enabled ?? true),
placeholderData: (previousData) => previousData getNextPageParam: (lastPage, pages) => {
if (lastPage.length && lastPage.length < limit) return undefined;
return lastPage?.length !== 0 ? pages.length * limit : undefined;
}
}); });
const fetchSecretApprovalRequestDetails = async ({ const fetchSecretApprovalRequestDetails = async ({

View File

@ -113,7 +113,6 @@ export type TGetSecretApprovalRequestList = {
committer?: string; committer?: string;
limit?: number; limit?: number;
offset?: number; offset?: number;
search?: string;
}; };
export type TGetSecretApprovalRequestCount = { export type TGetSecretApprovalRequestCount = {

View File

@ -352,9 +352,9 @@ export const ProjectLayout = () => {
secretApprovalReqCount?.open || secretApprovalReqCount?.open ||
accessApprovalRequestCount?.pendingCount accessApprovalRequestCount?.pendingCount
) && ( ) && (
<Badge variant="primary" className="ml-1.5"> <span className="ml-2 rounded border border-primary-400 bg-primary-600 px-1 py-0.5 text-xs font-semibold text-black">
{pendingRequestsCount} {pendingRequestsCount}
</Badge> </span>
)} )}
</MenuItem> </MenuItem>
)} )}

View File

@ -0,0 +1,222 @@
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&apos;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>
);
};

View File

@ -1,24 +1,94 @@
import { useGetAdminIntegrationsConfig } from "@app/hooks/api"; 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 { MicrosoftTeamsIntegrationForm } from "./MicrosoftTeamsIntegrationForm";
import { SlackIntegrationForm } from "./SlackIntegrationForm"; 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 = () => { export const IntegrationsPageForm = () => {
const { data: adminIntegrationsConfig } = useGetAdminIntegrationsConfig(); 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 ( return (
<div className="mb-6 min-h-64 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <div className="mb-6 min-h-64 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4"> <div className="mb-4">
<div className="text-xl font-semibold text-mineshaft-100">Integrations</div> <div className="text-xl font-semibold text-mineshaft-100">Integrations</div>
<div className="text-sm text-mineshaft-300"> <div className="text-sm text-mineshaft-300">
Configure your instance-wide settings to enable integration with Slack and Microsoft Configure your instance-wide settings to enable integration with third-party services.
Teams.
</div> </div>
</div> </div>
<div className="flex flex-col gap-2"> <Tabs value={selectedTab} onValueChange={updateSelectedTab}>
<SlackIntegrationForm adminIntegrationsConfig={adminIntegrationsConfig} /> <TabList>
<MicrosoftTeamsIntegrationForm adminIntegrationsConfig={adminIntegrationsConfig} /> {tabSections.map((section) => (
</div> <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> </div>
); );
}; };

View File

@ -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"> <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"> <div className="text-md group order-1 ml-3 flex items-center gap-2">
<BsMicrosoftTeams className="text-lg group-hover:text-primary-400" /> <BsMicrosoftTeams className="text-lg group-hover:text-primary-400" />
<div className="text-[15px] font-semibold">Microsoft Teams Integration</div> <div className="text-[15px] font-semibold">Microsoft Teams</div>
</div> </div>
</AccordionTrigger> </AccordionTrigger>
<AccordionContent childrenClassName="px-0 py-0"> <AccordionContent childrenClassName="px-0 py-0">

View File

@ -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"> <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"> <div className="text-md group order-1 ml-3 flex items-center gap-2">
<BsSlack className="text-lg group-hover:text-primary-400" /> <BsSlack className="text-lg group-hover:text-primary-400" />
<div className="text-[15px] font-semibold">Slack Integration</div> <div className="text-[15px] font-semibold">Slack</div>
</div> </div>
</AccordionTrigger> </AccordionTrigger>
<AccordionContent childrenClassName="px-0 py-0"> <AccordionContent childrenClassName="px-0 py-0">

View File

@ -111,10 +111,10 @@ export const GitHubConnectionForm = ({ appConnection }: Props) => {
}. This field cannot be changed after creation.`} }. This field cannot be changed after creation.`}
errorText={ errorText={
!isLoading && isMissingConfig !isLoading && isMissingConfig
? `Environment variables have not been configured. ${ ? `Credentials have not been configured. ${
isInfisicalCloud() isInfisicalCloud()
? "Please contact Infisical." ? "Please contact Infisical."
: `See Docs to configure GitHub ${methodDetails.name} Connections.` : `See Docs to configure Github ${methodDetails.name} Connections.`
}` }`
: error?.message : error?.message
} }

View File

@ -1,5 +1,7 @@
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; 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 { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { Badge } from "@app/components/v2/Badge"; import { Badge } from "@app/components/v2/Badge";
@ -43,7 +45,21 @@ export const SecretApprovalsPage = () => {
<PageHeader <PageHeader
title="Approval Workflows" title="Approval Workflows"
description="Create approval policies for any modifications to secrets in sensitive environments and folders." 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}> <Tabs defaultValue={defaultTab}>
<TabList> <TabList>
<Tab value={TabSection.SecretApprovalRequests}> <Tab value={TabSection.SecretApprovalRequests}>

View File

@ -2,25 +2,15 @@
/* eslint-disable react/jsx-no-useless-fragment */ /* eslint-disable react/jsx-no-useless-fragment */
import { useCallback, useMemo, useState } from "react"; import { useCallback, useMemo, useState } from "react";
import { import {
faArrowUpRightFromSquare,
faBan,
faBookOpen,
faCheck, faCheck,
faCheckCircle, faCheckCircle,
faChevronDown, faChevronDown,
faClipboardCheck,
faLock, faLock,
faMagnifyingGlass, faPlus
faPlus,
faSearch,
faStopwatch,
faUser,
IconDefinition
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format, formatDistance } from "date-fns"; import { formatDistance } from "date-fns";
import { AnimatePresence, motion } from "framer-motion"; import { AnimatePresence, motion } from "framer-motion";
import { twMerge } from "tailwind-merge";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { import {
@ -31,8 +21,6 @@ import {
DropdownMenuLabel, DropdownMenuLabel,
DropdownMenuTrigger, DropdownMenuTrigger,
EmptyState, EmptyState,
Input,
Pagination,
Tooltip Tooltip
} from "@app/components/v2"; } from "@app/components/v2";
import { Badge } from "@app/components/v2/Badge"; import { Badge } from "@app/components/v2/Badge";
@ -44,12 +32,7 @@ import {
useUser, useUser,
useWorkspace useWorkspace
} from "@app/context"; } from "@app/context";
import { import { usePopUp } from "@app/hooks";
getUserTablePreference,
PreferenceKey,
setUserTablePreference
} from "@app/helpers/userTablePreferences";
import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
import { useGetWorkspaceUsers } from "@app/hooks/api"; import { useGetWorkspaceUsers } from "@app/hooks/api";
import { import {
accessApprovalKeys, accessApprovalKeys,
@ -65,21 +48,28 @@ import { ApprovalStatus, TWorkspaceUser } from "@app/hooks/api/types";
import { RequestAccessModal } from "./components/RequestAccessModal"; import { RequestAccessModal } from "./components/RequestAccessModal";
import { ReviewAccessRequestModal } from "./components/ReviewAccessModal"; import { ReviewAccessRequestModal } from "./components/ReviewAccessModal";
const generateRequestText = (request: TAccessApprovalRequest) => { const generateRequestText = (request: TAccessApprovalRequest, userId: string) => {
const { isTemporary } = request; const { isTemporary } = request;
return ( return (
<div className="flex items-center justify-between text-sm"> <div className="flex w-full items-center justify-between text-sm">
<div> <div>
Requested {isTemporary ? "temporary" : "permanent"} access to{" "} Requested {isTemporary ? "temporary" : "permanent"} access to{" "}
<code className="mx-1 rounded bg-mineshaft-600 px-1.5 py-0.5 font-mono text-[13px] text-mineshaft-200"> <code className="mx-1 rounded-sm bg-primary-500/20 px-1.5 py-0.5 font-mono text-xs text-primary">
{request.policy.secretPath} {request.policy.secretPath}
</code>{" "} </code>
in{" "} in
<code className="mx-1 rounded bg-mineshaft-600 px-1.5 py-0.5 font-mono text-[13px] text-mineshaft-200"> <code className="mx-1 rounded-sm bg-primary-500/20 px-1.5 py-0.5 font-mono text-xs text-primary">
{request.environmentName} {request.environmentName}
</code> </code>
</div> </div>
<div>
{request.requestedByUserId === userId && (
<span className="text-xs text-gray-500">
<Badge className="ml-1">Requested By You</Badge>
</span>
)}
</div>
</div> </div>
); );
}; };
@ -130,64 +120,30 @@ export const AccessApprovalRequest = ({
projectSlug projectSlug
}); });
const { const { data: requests, refetch: refetchRequests } = useGetAccessApprovalRequests({
data: requests,
refetch: refetchRequests,
isPending: areRequestsPending
} = useGetAccessApprovalRequests({
projectSlug, projectSlug,
authorUserId: requestedByFilter, authorProjectMembershipId: requestedByFilter,
envSlug: envFilter 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(() => { const filteredRequests = useMemo(() => {
let accessRequests: typeof requests;
if (statusFilter === "open") if (statusFilter === "open")
accessRequests = requests?.filter( return requests?.filter(
(request) => (request) =>
!request.policy.deletedAt && !request.policy.deletedAt &&
!request.isApproved && !request.isApproved &&
!request.reviewers.some((reviewer) => reviewer.status === ApprovalStatus.REJECTED) !request.reviewers.some((reviewer) => reviewer.status === ApprovalStatus.REJECTED)
); );
if (statusFilter === "close") if (statusFilter === "close")
accessRequests = requests?.filter( return requests?.filter(
(request) => (request) =>
request.policy.deletedAt || request.policy.deletedAt ||
request.isApproved || request.isApproved ||
request.reviewers.some((reviewer) => reviewer.status === ApprovalStatus.REJECTED) request.reviewers.some((reviewer) => reviewer.status === ApprovalStatus.REJECTED)
); );
return ( return requests;
accessRequests?.filter((request) => { }, [requests, statusFilter, requestedByFilter, envFilter]);
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( const generateRequestDetails = useCallback(
(request: TAccessApprovalRequest) => { (request: TAccessApprovalRequest) => {
@ -206,15 +162,9 @@ export const AccessApprovalRequest = ({
const canBypass = const canBypass =
!request.policy.bypassers.length || request.policy.bypassers.includes(user.id); !request.policy.bypassers.length || request.policy.bypassers.includes(user.id);
let displayData: { let displayData: { label: string; type: "primary" | "danger" | "success" } = {
label: string;
type: "primary" | "danger" | "success";
tooltipContent?: string;
icon: IconDefinition | null;
} = {
label: "", label: "",
type: "primary", type: "primary"
icon: null
}; };
const isExpired = const isExpired =
@ -222,42 +172,20 @@ export const AccessApprovalRequest = ({
request.isApproved && request.isApproved &&
new Date() > new Date(request.privilege.temporaryAccessEndTime || ("" as string)); new Date() > new Date(request.privilege.temporaryAccessEndTime || ("" as string));
if (isExpired) if (isExpired) displayData = { label: "Access Expired", type: "danger" };
displayData = { else if (isAccepted) displayData = { label: "Access Granted", type: "success" };
label: "Access Expired", else if (isRejectedByAnyone) displayData = { label: "Rejected", type: "danger" };
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) { else if (userReviewStatus === ApprovalStatus.APPROVED) {
displayData = { displayData = {
label: `Pending ${request.policy.approvals - request.reviewers.length} review${ label: `Pending ${request.policy.approvals - request.reviewers.length} review${
request.policy.approvals - request.reviewers.length > 1 ? "s" : "" request.policy.approvals - request.reviewers.length > 1 ? "s" : ""
}`, }`,
type: "primary", type: "primary"
icon: faClipboardCheck
}; };
} else if (!isReviewedByUser) } else if (!isReviewedByUser)
displayData = { displayData = {
label: "Review Required", label: "Review Required",
type: "primary", type: "primary"
icon: faClipboardCheck
}; };
return { return {
@ -297,42 +225,16 @@ export const AccessApprovalRequest = ({
[generateRequestDetails, membersGroupById, user, setSelectedRequest, handlePopUpOpen] [generateRequestDetails, membersGroupById, user, setSelectedRequest, handlePopUpOpen]
); );
const isFiltered = Boolean(search || envFilter || requestedByFilter);
return ( 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>
<div className="flex items-start gap-1"> <div className="mb-6 flex items-end justify-between">
<p className="text-xl font-semibold text-mineshaft-100">Access Requests</p> <div className="flex flex-col">
<a <span className="text-xl font-semibold text-mineshaft-100">Access Requests</span>
href="https://infisical.com/docs/documentation/platform/access-controls/access-requests" <div className="mt-2 text-sm text-bunker-300">
target="_blank" Request access to secrets in sensitive environments and folders.
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> </div>
</a>
</div>
<p className="text-sm text-bunker-300">
Request and review access to secrets in sensitive environments and folders
</p>
</div> </div>
<div>
<Tooltip <Tooltip
content="To submit Access Requests, your project needs to create Access Request policies first." content="To submit Access Requests, your project needs to create Access Request policies first."
isDisabled={policiesLoading || !!policies?.length} isDisabled={policiesLoading || !!policies?.length}
@ -345,23 +247,25 @@ export const AccessApprovalRequest = ({
} }
handlePopUpOpen("requestAccess"); handlePopUpOpen("requestAccess");
}} }}
colorSchema="secondary"
leftIcon={<FontAwesomeIcon icon={faPlus} />} leftIcon={<FontAwesomeIcon icon={faPlus} />}
isDisabled={policiesLoading || !policies?.length} isDisabled={policiesLoading || !policies?.length}
> >
Request Access Request access
</Button> </Button>
</Tooltip> </Tooltip>
</div> </div>
<Input </div>
value={search}
onChange={(e) => setSearch(e.target.value)} <AnimatePresence>
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />} <motion.div
placeholder="Search approval requests by requesting user or environment..." key="approval-changes-list"
className="flex-1" transition={{ duration: 0.1 }}
containerClassName="mb-4" initial={{ opacity: 0, translateX: 30 }}
/> animate={{ opacity: 1, translateX: 0 }}
<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"> 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 <div
role="button" role="button"
tabIndex={0} tabIndex={0}
@ -369,19 +273,17 @@ export const AccessApprovalRequest = ({
onKeyDown={(evt) => { onKeyDown={(evt) => {
if (evt.key === "Enter") setStatusFilter("open"); if (evt.key === "Enter") setStatusFilter("open");
}} }}
className={twMerge( className={
"font-medium", statusFilter === "close" ? "text-gray-500 duration-100 hover:text-gray-400" : ""
statusFilter === "close" && "text-gray-500 duration-100 hover:text-gray-400" }
)}
> >
<FontAwesomeIcon icon={faLock} className="mr-2" /> <FontAwesomeIcon icon={faLock} className="mr-2" />
{!!requestCount && requestCount?.pendingCount} Pending {!!requestCount && requestCount?.pendingCount} Pending
</div> </div>
<div <div
className={twMerge( className={
"font-medium", statusFilter === "open" ? "text-gray-500 duration-100 hover:text-gray-400" : ""
statusFilter === "open" && "text-gray-500 duration-100 hover:text-gray-400" }
)}
role="button" role="button"
tabIndex={0} tabIndex={0}
onClick={() => setStatusFilter("close")} onClick={() => setStatusFilter("close")}
@ -390,7 +292,7 @@ export const AccessApprovalRequest = ({
}} }}
> >
<FontAwesomeIcon icon={faCheck} className="mr-2" /> <FontAwesomeIcon icon={faCheck} className="mr-2" />
{!!requestCount && requestCount.finalizedCount} Closed {!!requestCount && requestCount.finalizedCount} Completed
</div> </div>
<div className="flex flex-grow justify-end space-x-8"> <div className="flex flex-grow justify-end space-x-8">
<DropdownMenu> <DropdownMenu>
@ -398,20 +300,14 @@ export const AccessApprovalRequest = ({
<Button <Button
variant="plain" variant="plain"
colorSchema="secondary" colorSchema="secondary"
className={envFilter ? "text-white" : "text-bunker-300"} className="text-bunker-300"
rightIcon={<FontAwesomeIcon icon={faChevronDown} size="sm" className="ml-2" />} rightIcon={<FontAwesomeIcon icon={faChevronDown} size="sm" className="ml-2" />}
> >
Environments Environments
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent <DropdownMenuContent>
align="end" <DropdownMenuLabel>Select an environment</DropdownMenuLabel>
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 }) => ( {currentWorkspace?.environments.map(({ slug, name }) => (
<DropdownMenuItem <DropdownMenuItem
onClick={() => setEnvFilter((state) => (state === slug ? undefined : slug))} onClick={() => setEnvFilter((state) => (state === slug ? undefined : slug))}
@ -441,27 +337,15 @@ export const AccessApprovalRequest = ({
Requested By Requested By
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent <DropdownMenuContent align="end">
align="end" <DropdownMenuLabel>Select an author</DropdownMenuLabel>
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 }) => ( {members?.map(({ user: membershipUser, id }) => (
<DropdownMenuItem <DropdownMenuItem
onClick={() => onClick={() =>
setRequestedByFilter((state) => setRequestedByFilter((state) => (state === id ? undefined : id))
state === membershipUser.id ? undefined : membershipUser.id
)
} }
key={`request-filter-member-${id}`} key={`request-filter-member-${id}`}
icon={ icon={requestedByFilter === id && <FontAwesomeIcon icon={faCheckCircle} />}
requestedByFilter === membershipUser.id && (
<FontAwesomeIcon icon={faCheckCircle} />
)
}
iconPos="right" iconPos="right"
> >
{membershipUser.username} {membershipUser.username}
@ -473,26 +357,19 @@ export const AccessApprovalRequest = ({
</div> </div>
</div> </div>
<div className="flex flex-col rounded-b-md border-x border-b border-t border-mineshaft-600 bg-mineshaft-800"> <div className="flex flex-col rounded-b-md border-x border-b border-t border-mineshaft-600 bg-mineshaft-800">
{filteredRequests?.length === 0 && !isFiltered && ( {filteredRequests?.length === 0 && (
<div className="py-12"> <div className="py-12">
<EmptyState <EmptyState title="No more access requests pending." />
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> </div>
)} )}
{!!filteredRequests?.length && {!!filteredRequests?.length &&
filteredRequests?.slice(offset, perPage * page).map((request) => { filteredRequests?.map((request) => {
const details = generateRequestDetails(request); const details = generateRequestDetails(request);
return ( return (
<div <div
key={request.id} key={request.id}
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" className="flex w-full cursor-pointer px-8 py-4 hover:bg-mineshaft-700 aria-disabled:opacity-80"
role="button" role="button"
tabIndex={0} tabIndex={0}
onClick={() => handleSelectRequest(request)} onClick={() => handleSelectRequest(request)}
@ -502,18 +379,14 @@ export const AccessApprovalRequest = ({
} }
}} }}
> >
<div className="flex w-full items-center justify-between"> <div className="w-full">
<div className="flex w-full flex-col justify-between"> <div className="flex w-full flex-col justify-between">
<div className="mb-1 flex w-full items-center"> <div className="mb-1 flex w-full items-center">
<FontAwesomeIcon <FontAwesomeIcon icon={faLock} className="mr-2" />
icon={faLock} {generateRequestText(request, user.id)}
size="xs"
className="mr-1.5 text-mineshaft-300"
/>
{generateRequestText(request)}
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="text-xs leading-3 text-gray-500"> <div className="text-xs text-gray-500">
{membersGroupById?.[request.requestedByUserId]?.user && ( {membersGroupById?.[request.requestedByUserId]?.user && (
<> <>
Requested {formatDistance(new Date(request.createdAt), new Date())}{" "} Requested {formatDistance(new Date(request.createdAt), new Date())}{" "}
@ -524,45 +397,21 @@ export const AccessApprovalRequest = ({
</> </>
)} )}
</div> </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> <div>
<Badge <Badge variant={details.displayData.type}>
className="flex items-center gap-1.5 whitespace-nowrap" {details.displayData.label}
variant={details.displayData.type}
>
{details.displayData.icon && (
<FontAwesomeIcon icon={details.displayData.icon} />
)}
<span>{details.displayData.label}</span>
</Badge> </Badge>
</div> </div>
</Tooltip> </div>
</div> </div>
</div> </div>
</div> </div>
); );
})} })}
{Boolean(filteredRequests.length) && (
<Pagination
className="border-none"
count={filteredRequests.length}
page={page}
perPage={perPage}
onChangePage={setPage}
onChangePerPage={handlePerPageChange}
/>
)}
</div>
</div> </div>
</motion.div>
</AnimatePresence>
{!!policies && ( {!!policies && (
<RequestAccessModal <RequestAccessModal
policies={policies} policies={policies}
@ -603,7 +452,6 @@ export const AccessApprovalRequest = ({
isOpen={popUp.upgradePlan.isOpen} isOpen={popUp.upgradePlan.isOpen}
onOpenChange={() => handlePopUpClose("upgradePlan")} onOpenChange={() => handlePopUpClose("upgradePlan")}
/> />
</motion.div> </div>
</AnimatePresence>
); );
}; };

View File

@ -1,19 +1,11 @@
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { import {
faArrowDown,
faArrowUp,
faArrowUpRightFromSquare,
faBookOpen,
faCheckCircle, faCheckCircle,
faChevronDown,
faFileShield, faFileShield,
faFilter, faPlus
faMagnifyingGlass,
faPlus,
faSearch
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 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 { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
@ -27,9 +19,8 @@ import {
DropdownMenuLabel, DropdownMenuLabel,
DropdownMenuTrigger, DropdownMenuTrigger,
EmptyState, EmptyState,
IconButton, Modal,
Input, ModalContent,
Pagination,
Table, Table,
TableContainer, TableContainer,
TableSkeleton, TableSkeleton,
@ -47,12 +38,7 @@ import {
useWorkspace useWorkspace
} from "@app/context"; } from "@app/context";
import { ProjectPermissionActions } from "@app/context/ProjectPermissionContext/types"; import { ProjectPermissionActions } from "@app/context/ProjectPermissionContext/types";
import { import { usePopUp } from "@app/hooks";
getUserTablePreference,
PreferenceKey,
setUserTablePreference
} from "@app/helpers/userTablePreferences";
import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
import { import {
useDeleteAccessApprovalPolicy, useDeleteAccessApprovalPolicy,
useDeleteSecretApprovalPolicy, useDeleteSecretApprovalPolicy,
@ -61,7 +47,6 @@ import {
useListWorkspaceGroups useListWorkspaceGroups
} from "@app/hooks/api"; } from "@app/hooks/api";
import { useGetAccessApprovalPolicies } from "@app/hooks/api/accessApproval/queries"; 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 { PolicyType } from "@app/hooks/api/policies/enums";
import { TAccessApprovalPolicy, Workspace } from "@app/hooks/api/types"; import { TAccessApprovalPolicy, Workspace } from "@app/hooks/api/types";
@ -72,18 +57,6 @@ interface IProps {
workspaceId: string; 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 useApprovalPolicies = (permission: TProjectPermission, currentWorkspace?: Workspace) => {
const { data: accessPolicies, isPending: isAccessPoliciesLoading } = useGetAccessApprovalPolicies( const { data: accessPolicies, isPending: isAccessPoliciesLoading } = useGetAccessApprovalPolicies(
{ {
@ -139,79 +112,11 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
currentWorkspace currentWorkspace
); );
const [filters, setFilters] = useState<PolicyFilters>({ const [filterType, setFilterType] = useState<string | null>(null);
type: null,
environmentIds: []
});
const { const filteredPolicies = useMemo(() => {
search, return filterType ? policies.filter((policy) => policy.policyType === filterType) : policies;
setSearch, }, [policies, filterType]);
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: deleteSecretApprovalPolicy } = useDeleteSecretApprovalPolicy();
const { mutateAsync: deleteAccessApprovalPolicy } = useDeleteAccessApprovalPolicy(); const { mutateAsync: deleteAccessApprovalPolicy } = useDeleteAccessApprovalPolicy();
@ -246,57 +151,16 @@ 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 ( 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>
<div className="flex items-start gap-1"> <div className="mb-6 flex items-end justify-between">
<p className="text-xl font-semibold text-mineshaft-100">Policies</p> <div className="flex flex-col">
<a <span className="text-xl font-semibold text-mineshaft-100">Policies</span>
href="https://infisical.com/docs/documentation/platform/pr-workflows" <div className="mt-2 text-sm text-bunker-300">
target="_blank" Implement granular policies for access requests and secrets management.
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> </div>
</a>
</div>
<p className="text-sm text-bunker-300">
Implement granular policies for access requests and secrets management
</p>
</div> </div>
<div>
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Create} I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.SecretApproval} a={ProjectPermissionSub.SecretApproval}
@ -310,7 +174,6 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
} }
handlePopUpOpen("policyForm"); handlePopUpOpen("policyForm");
}} }}
colorSchema="secondary"
leftIcon={<FontAwesomeIcon icon={faPlus} />} leftIcon={<FontAwesomeIcon icon={faPlus} />}
isDisabled={!isAllowed} isDisabled={!isAllowed}
> >
@ -319,54 +182,41 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
)} )}
</ProjectPermissionCan> </ProjectPermissionCan>
</div> </div>
<div className="mb-4 flex items-center gap-2"> </div>
<Input <TableContainer>
value={search} <Table>
onChange={(e) => setSearch(e.target.value)} <THead>
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />} <Tr>
placeholder="Search policies by name, type, environment or secret path..." <Th>Name</Th>
className="flex-1" <Th>Environment</Th>
/> <Th>Secret Path</Th>
<Th>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger>
<IconButton <Button
ariaLabel="Filter findings"
variant="plain" variant="plain"
size="sm" colorSchema="secondary"
className={twMerge( className="text-xs font-semibold uppercase text-bunker-300"
"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", rightIcon={
isTableFiltered && "border-primary/50 text-primary" <FontAwesomeIcon icon={faChevronDown} size="sm" className="ml-2" />
)}
>
<FontAwesomeIcon icon={faFilter} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className="thin-scrollbar max-h-[70vh] overflow-y-auto"
align="end"
>
<DropdownMenuLabel>Policy Type</DropdownMenuLabel>
<DropdownMenuItem
onClick={() =>
setFilters((prev) => ({
...prev,
type: null
}))
} }
icon={!filters && <FontAwesomeIcon icon={faCheckCircle} />} >
Type
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLabel>Select a type</DropdownMenuLabel>
<DropdownMenuItem
onClick={() => setFilterType(null)}
icon={!filterType && <FontAwesomeIcon icon={faCheckCircle} />}
iconPos="right" iconPos="right"
> >
All All
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={() => onClick={() => setFilterType(PolicyType.AccessPolicy)}
setFilters((prev) => ({
...prev,
type: PolicyType.AccessPolicy
}))
}
icon={ icon={
filters.type === PolicyType.AccessPolicy && ( filterType === PolicyType.AccessPolicy && (
<FontAwesomeIcon icon={faCheckCircle} /> <FontAwesomeIcon icon={faCheckCircle} />
) )
} }
@ -375,14 +225,9 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
Access Policy Access Policy
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={() => onClick={() => setFilterType(PolicyType.ChangePolicy)}
setFilters((prev) => ({
...prev,
type: PolicyType.ChangePolicy
}))
}
icon={ icon={
filters.type === PolicyType.ChangePolicy && ( filterType === PolicyType.ChangePolicy && (
<FontAwesomeIcon icon={faCheckCircle} /> <FontAwesomeIcon icon={faCheckCircle} />
) )
} }
@ -390,110 +235,25 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
> >
Change Policy Change Policy
</DropdownMenuItem> </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> </DropdownMenuContent>
</DropdownMenu> </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> <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> </Tr>
</THead> </THead>
<TBody> <TBody>
{isPoliciesLoading && ( {isPoliciesLoading && (
<TableSkeleton <TableSkeleton columns={6} innerKey="secret-policies" className="bg-mineshaft-700" />
columns={5}
innerKey="secret-policies"
className="bg-mineshaft-700"
/>
)} )}
{!isPoliciesLoading && !policies?.length && ( {!isPoliciesLoading && !filteredPolicies?.length && (
<Tr> <Tr>
<Td colSpan={5}> <Td colSpan={6}>
<EmptyState title="No Policies Found" icon={faFileShield} /> <EmptyState title="No policies found" icon={faFileShield} />
</Td> </Td>
</Tr> </Tr>
)} )}
{!!currentWorkspace && {!!currentWorkspace &&
filteredPolicies filteredPolicies?.map((policy) => (
?.slice(offset, perPage * page)
.map((policy) => (
<ApprovalPolicyRow <ApprovalPolicyRow
policy={policy} policy={policy}
key={policy.id} key={policy.id}
@ -505,21 +265,20 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
))} ))}
</TBody> </TBody>
</Table> </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> </TableContainer>
</div> <Modal
</motion.div> 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"
>
<AccessPolicyForm <AccessPolicyForm
projectId={currentWorkspace.id} projectId={currentWorkspace.id}
projectSlug={currentWorkspace.slug} projectSlug={currentWorkspace.slug}
@ -528,6 +287,8 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
members={members} members={members}
editValues={popUp.policyForm.data as TAccessApprovalPolicy} editValues={popUp.policyForm.data as TAccessApprovalPolicy}
/> />
</ModalContent>
</Modal>
<DeleteActionModal <DeleteActionModal
isOpen={popUp.deletePolicy.isOpen} isOpen={popUp.deletePolicy.isOpen}
deleteKey="remove" deleteKey="remove"
@ -540,6 +301,6 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)} onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can add secret approval policy if you switch to Infisical's Enterprise plan." text="You can add secret approval policy if you switch to Infisical's Enterprise plan."
/> />
</AnimatePresence> </div>
); );
}; };

View File

@ -1,4 +1,4 @@
import { RefObject, useMemo, useRef, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { Controller, useFieldArray, useForm } from "react-hook-form"; import { Controller, useFieldArray, useForm } from "react-hook-form";
import { faGripVertical, faTrash } from "@fortawesome/free-solid-svg-icons"; import { faGripVertical, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@ -13,8 +13,6 @@ import {
FormControl, FormControl,
IconButton, IconButton,
Input, Input,
Modal,
ModalContent,
Select, Select,
SelectItem, SelectItem,
Switch, Switch,
@ -112,20 +110,20 @@ const formSchema = z
type TFormSchema = z.infer<typeof formSchema>; type TFormSchema = z.infer<typeof formSchema>;
const Form = ({ export const AccessPolicyForm = ({
isOpen,
onToggle, onToggle,
members = [], members = [],
projectId, projectId,
projectSlug, projectSlug,
editValues, editValues
modalContainer, }: Props) => {
isEditMode
}: Props & { modalContainer: RefObject<HTMLDivElement>; isEditMode: boolean }) => {
const [draggedItem, setDraggedItem] = useState<number | null>(null); const [draggedItem, setDraggedItem] = useState<number | null>(null);
const [dragOverItem, setDragOverItem] = useState<number | null>(null); const [dragOverItem, setDragOverItem] = useState<number | null>(null);
const { const {
control, control,
handleSubmit, handleSubmit,
reset,
watch, watch,
formState: { isSubmitting } formState: { isSubmitting }
} = useForm<TFormSchema>({ } = useForm<TFormSchema>({
@ -190,8 +188,13 @@ const Form = ({
const { data: groups } = useListWorkspaceGroups(projectId); const { data: groups } = useListWorkspaceGroups(projectId);
const environments = currentWorkspace?.environments || []; const environments = currentWorkspace?.environments || [];
const isEditMode = Boolean(editValues);
const isAccessPolicyType = watch("policyType") === PolicyType.AccessPolicy; const isAccessPolicyType = watch("policyType") === PolicyType.AccessPolicy;
useEffect(() => {
if (!isOpen || !isEditMode) reset({});
}, [isOpen, isEditMode]);
const { mutateAsync: createAccessApprovalPolicy } = useCreateAccessApprovalPolicy(); const { mutateAsync: createAccessApprovalPolicy } = useCreateAccessApprovalPolicy();
const { mutateAsync: updateAccessApprovalPolicy } = useUpdateAccessApprovalPolicy(); const { mutateAsync: updateAccessApprovalPolicy } = useUpdateAccessApprovalPolicy();
@ -384,7 +387,6 @@ const Form = ({
setDraggedItem(null); setDraggedItem(null);
setDragOverItem(null); setDragOverItem(null);
}; };
return ( return (
<div className="flex flex-col space-y-3"> <div className="flex flex-col space-y-3">
<form onSubmit={handleSubmit(handleFormSubmit)}> <form onSubmit={handleSubmit(handleFormSubmit)}>
@ -570,7 +572,7 @@ const Form = ({
className="flex-grow" className="flex-grow"
> >
<FilterableSelect <FilterableSelect
menuPortalTarget={modalContainer.current} menuPortalTarget={document.getElementById("policy-form")}
menuPlacement="top" menuPlacement="top"
isMulti isMulti
placeholder="Select members..." placeholder="Select members..."
@ -600,7 +602,7 @@ const Form = ({
className="flex-grow" className="flex-grow"
> >
<FilterableSelect <FilterableSelect
menuPortalTarget={modalContainer.current} menuPortalTarget={document.getElementById("policy-form")}
menuPlacement="top" menuPlacement="top"
isMulti isMulti
placeholder="Select groups..." placeholder="Select groups..."
@ -811,27 +813,3 @@ const Form = ({
</div> </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>
);
};

View File

@ -1,5 +1,5 @@
import { useMemo } from "react"; import { useMemo } from "react";
import { faEdit, faEllipsisV, faTrash } from "@fortawesome/free-solid-svg-icons"; import { faEllipsis } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
@ -9,8 +9,6 @@ import {
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
GenericFieldLabel,
IconButton,
Td, Td,
Tr Tr
} from "@app/components/v2"; } from "@app/components/v2";
@ -82,11 +80,11 @@ export const ApprovalPolicyRow = ({
userLabels: members userLabels: members
?.filter((member) => el.user.find((i) => i.id === member.user.id)) ?.filter((member) => el.user.find((i) => i.id === member.user.id))
.map((member) => getMemberLabel(member)) .map((member) => getMemberLabel(member))
.join(", "), .join(","),
groupLabels: groups groupLabels: groups
?.filter(({ group }) => el.group.find((i) => i.id === group.id)) ?.filter(({ group }) => el.group.find((i) => i.id === group.id))
.map(({ group }) => group.name) .map(({ group }) => group.name)
.join(", "), .join(","),
approvals: el.approvals approvals: el.approvals
}; };
}); });
@ -104,47 +102,36 @@ export const ApprovalPolicyRow = ({
}} }}
onClick={() => setIsExpanded.toggle()} onClick={() => setIsExpanded.toggle()}
> >
<Td>{policy.name || <span className="text-mineshaft-400">Unnamed Policy</span>}</Td> <Td>{policy.name}</Td>
<Td>{policy.environment.name}</Td> <Td>{policy.environment.slug}</Td>
<Td>{policy.secretPath || "*"}</Td> <Td>{policy.secretPath || "*"}</Td>
<Td> <Td>
<Badge <Badge className={policyDetails[policy.policyType].className}>
className={twMerge( {policyDetails[policy.policyType].name}
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> </Badge>
</Td> </Td>
<Td> <Td>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild className="cursor-pointer rounded-lg"> <DropdownMenuTrigger asChild className="cursor-pointer rounded-lg">
<DropdownMenuTrigger asChild> <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">
<IconButton <FontAwesomeIcon size="sm" icon={faEllipsis} />
ariaLabel="Options" </div>
colorSchema="secondary"
className="w-6"
variant="plain"
>
<FontAwesomeIcon icon={faEllipsisV} />
</IconButton>
</DropdownMenuTrigger> </DropdownMenuTrigger>
</DropdownMenuTrigger> <DropdownMenuContent align="center" className="min-w-[100%] p-1">
<DropdownMenuContent sideOffset={2} align="end" className="min-w-[12rem] p-1">
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Edit} I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.SecretApproval} a={ProjectPermissionSub.SecretApproval}
> >
{(isAllowed) => ( {(isAllowed) => (
<DropdownMenuItem <DropdownMenuItem
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
onEdit(); onEdit();
}} }}
isDisabled={!isAllowed} disabled={!isAllowed}
icon={<FontAwesomeIcon icon={faEdit} />}
> >
Edit Policy Edit Policy
</DropdownMenuItem> </DropdownMenuItem>
@ -156,12 +143,16 @@ export const ApprovalPolicyRow = ({
> >
{(isAllowed) => ( {(isAllowed) => (
<DropdownMenuItem <DropdownMenuItem
className={twMerge(
isAllowed
? "hover:!bg-red-500 hover:!text-white"
: "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
onDelete(); onDelete();
}} }}
isDisabled={!isAllowed} disabled={!isAllowed}
icon={<FontAwesomeIcon icon={faTrash} />}
> >
Delete Policy Delete Policy
</DropdownMenuItem> </DropdownMenuItem>
@ -171,41 +162,45 @@ export const ApprovalPolicyRow = ({
</DropdownMenu> </DropdownMenu>
</Td> </Td>
</Tr> </Tr>
{isExpanded && (
<Tr> <Tr>
<Td colSpan={6} className="!border-none p-0"> <Td colSpan={5} className="rounded bg-mineshaft-900">
<div <div className="mb-4 border-b-2 border-mineshaft-500 py-2 text-lg">Approvers</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) => ( {labels?.map((el, index) => (
<div <div
key={`approval-list-${index + 1}`} key={`approval-list-${index + 1}`}
className="relative mb-2 flex rounded border border-mineshaft-500 bg-mineshaft-800 p-4" className="relative mb-2 flex rounded border border-mineshaft-500 bg-mineshaft-700 p-4"
> >
<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>
<div>{index + 1}</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> </div>
{index !== labels.length - 1 && ( {index !== labels.length - 1 && (
<div className="absolute bottom-0 left-8 h-[1.25rem] border-r border-mineshaft-400" /> <div className="absolute bottom-0 left-8 h-6 border-r border-gray-400" />
)} )}
{index !== 0 && ( {index !== 0 && (
<div className="absolute left-8 top-0 h-[1.25rem] border-r border-mineshaft-400" /> <div className="absolute left-8 top-0 h-4 border-r border-gray-400" />
)} )}
</div>
<div className="grid flex-grow grid-cols-3"> <div className="grid flex-grow grid-cols-3">
<GenericFieldLabel label="Users">{el.userLabels}</GenericFieldLabel> <div>
<GenericFieldLabel label="Groups">{el.groupLabels}</GenericFieldLabel> <div className="mb-1 text-xs font-semibold uppercase">Users</div>
<GenericFieldLabel label="Approvals Required">{el.approvals}</GenericFieldLabel> <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>
</div> </div>
</div> </div>
))} ))}
</div>
</div>
</Td> </Td>
</Tr> </Tr>
)}
</> </>
); );
}; };

View File

@ -1,19 +1,14 @@
import { useEffect, useState } from "react"; import { Fragment, useEffect, useState } from "react";
import { import {
faArrowUpRightFromSquare,
faBookOpen,
faCheck, faCheck,
faCheckCircle, faCheckCircle,
faChevronDown, faChevronDown,
faCodeBranch, faCodeBranch
faMagnifyingGlass,
faSearch
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useSearch } from "@tanstack/react-router"; import { useSearch } from "@tanstack/react-router";
import { formatDistance } from "date-fns"; import { formatDistance } from "date-fns";
import { AnimatePresence, motion } from "framer-motion"; import { AnimatePresence, motion } from "framer-motion";
import { twMerge } from "tailwind-merge";
import { import {
Button, Button,
@ -23,8 +18,6 @@ import {
DropdownMenuLabel, DropdownMenuLabel,
DropdownMenuTrigger, DropdownMenuTrigger,
EmptyState, EmptyState,
Input,
Pagination,
Skeleton Skeleton
} from "@app/components/v2"; } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes"; import { ROUTE_PATHS } from "@app/const/routes";
@ -35,12 +28,6 @@ import {
useUser, useUser,
useWorkspace useWorkspace
} from "@app/context"; } from "@app/context";
import {
getUserTablePreference,
PreferenceKey,
setUserTablePreference
} from "@app/helpers/userTablePreferences";
import { usePagination } from "@app/hooks";
import { import {
useGetSecretApprovalRequestCount, useGetSecretApprovalRequestCount,
useGetSecretApprovalRequests, useGetSecretApprovalRequests,
@ -65,41 +52,18 @@ export const SecretApprovalRequest = () => {
const [usingUrlRequestId, setUsingUrlRequestId] = useState(false); const [usingUrlRequestId, setUsingUrlRequestId] = useState(false);
const { const {
debouncedSearch: debouncedSearchFilter, data: secretApprovalRequests,
search: searchFilter, isFetchingNextPage: isFetchingNextApprovalRequest,
setSearch: setSearchFilter, fetchNextPage: fetchNextApprovalRequest,
setPage, hasNextPage: hasNextApprovalPage,
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, isPending: isApprovalRequestLoading,
refetch refetch
} = useGetSecretApprovalRequests({ } = useGetSecretApprovalRequests({
workspaceId, workspaceId,
status: statusFilter, status: statusFilter,
environment: envFilter, environment: envFilter,
committer: committerFilter, committer: committerFilter
search: debouncedSearchFilter,
limit,
offset
}); });
const totalApprovalCount = data?.totalCount ?? 0;
const secretApprovalRequests = data?.approvals ?? [];
const { data: secretApprovalRequestCount, isSuccess: isSecretApprovalReqCountSuccess } = const { data: secretApprovalRequestCount, isSuccess: isSecretApprovalReqCountSuccess } =
useGetSecretApprovalRequestCount({ workspaceId }); useGetSecretApprovalRequestCount({ workspaceId });
const { user: userSession } = useUser(); const { user: userSession } = useUser();
@ -124,9 +88,8 @@ export const SecretApprovalRequest = () => {
refetch(); refetch();
}; };
const isRequestListEmpty = !isApprovalRequestLoading && secretApprovalRequests?.length === 0; const isRequestListEmpty =
!isApprovalRequestLoading && secretApprovalRequests?.pages[0]?.length === 0;
const isFiltered = Boolean(searchFilter || envFilter || committerFilter);
return ( return (
<AnimatePresence mode="wait"> <AnimatePresence mode="wait">
@ -153,38 +116,7 @@ export const SecretApprovalRequest = () => {
exit={{ opacity: 0, translateX: 30 }} exit={{ opacity: 0, translateX: 30 }}
className="rounded-md text-gray-300" className="rounded-md text-gray-300"
> >
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <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="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 <div
role="button" role="button"
tabIndex={0} tabIndex={0}
@ -192,19 +124,17 @@ export const SecretApprovalRequest = () => {
onKeyDown={(evt) => { onKeyDown={(evt) => {
if (evt.key === "Enter") setStatusFilter("open"); if (evt.key === "Enter") setStatusFilter("open");
}} }}
className={twMerge( className={
"font-medium", statusFilter === "close" ? "text-gray-500 duration-100 hover:text-gray-400" : ""
statusFilter === "close" && "text-gray-500 duration-100 hover:text-gray-400" }
)}
> >
<FontAwesomeIcon icon={faCodeBranch} className="mr-2" /> <FontAwesomeIcon icon={faCodeBranch} className="mr-2" />
{isSecretApprovalReqCountSuccess && secretApprovalRequestCount?.open} Open {isSecretApprovalReqCountSuccess && secretApprovalRequestCount?.open} Open
</div> </div>
<div <div
className={twMerge( className={
"font-medium", statusFilter === "open" ? "text-gray-500 duration-100 hover:text-gray-400" : ""
statusFilter === "open" && "text-gray-500 duration-100 hover:text-gray-400" }
)}
role="button" role="button"
tabIndex={0} tabIndex={0}
onClick={() => setStatusFilter("close")} onClick={() => setStatusFilter("close")}
@ -222,21 +152,13 @@ export const SecretApprovalRequest = () => {
variant="plain" variant="plain"
colorSchema="secondary" colorSchema="secondary"
className={envFilter ? "text-white" : "text-bunker-300"} className={envFilter ? "text-white" : "text-bunker-300"}
rightIcon={ rightIcon={<FontAwesomeIcon icon={faChevronDown} size="sm" className="ml-2" />}
<FontAwesomeIcon icon={faChevronDown} size="sm" className="ml-2" />
}
> >
Environments Environments
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent <DropdownMenuContent>
align="end" <DropdownMenuLabel>Select an environment</DropdownMenuLabel>
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 }) => ( {currentWorkspace?.environments.map(({ slug, name }) => (
<DropdownMenuItem <DropdownMenuItem
onClick={() => setEnvFilter((state) => (state === slug ? undefined : slug))} onClick={() => setEnvFilter((state) => (state === slug ? undefined : slug))}
@ -266,14 +188,8 @@ export const SecretApprovalRequest = () => {
Author Author
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent <DropdownMenuContent align="end">
align="end" <DropdownMenuLabel>Select an author</DropdownMenuLabel>
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 }) => ( {members?.map(({ user, id }) => (
<DropdownMenuItem <DropdownMenuItem
onClick={() => onClick={() =>
@ -294,14 +210,14 @@ export const SecretApprovalRequest = () => {
</div> </div>
</div> </div>
<div className="flex flex-col rounded-b-md border-x border-b border-t border-mineshaft-600 bg-mineshaft-800"> <div className="flex flex-col rounded-b-md border-x border-b border-t border-mineshaft-600 bg-mineshaft-800">
{isRequestListEmpty && !isFiltered && ( {isRequestListEmpty && (
<div className="py-12"> <div className="py-12">
<EmptyState <EmptyState title="No more requests pending." />
title={`No ${statusFilter === "open" ? "Open" : "Closed"} Change Requests`}
/>
</div> </div>
)} )}
{secretApprovalRequests.map((secretApproval) => { {secretApprovalRequests?.pages?.map((group, i) => (
<Fragment key={`secret-approval-request-${i + 1}`}>
{group?.map((secretApproval) => {
const { const {
id: reqId, id: reqId,
commits, commits,
@ -317,7 +233,7 @@ export const SecretApprovalRequest = () => {
return ( return (
<div <div
key={reqId} key={reqId}
className="flex flex-col border-b border-mineshaft-600 px-8 py-3 last:border-b-0 hover:bg-mineshaft-700" className="flex flex-col px-8 py-4 hover:bg-mineshaft-700"
role="button" role="button"
tabIndex={0} tabIndex={0}
onClick={() => setSelectedApprovalId(secretApproval.id)} onClick={() => setSelectedApprovalId(secretApproval.id)}
@ -325,18 +241,14 @@ export const SecretApprovalRequest = () => {
if (evt.key === "Enter") setSelectedApprovalId(secretApproval.id); if (evt.key === "Enter") setSelectedApprovalId(secretApproval.id);
}} }}
> >
<div className="mb-1 text-sm"> <div className="mb-1">
<FontAwesomeIcon <FontAwesomeIcon icon={faCodeBranch} className="mr-2" />
icon={faCodeBranch}
size="sm"
className="mr-1.5 text-mineshaft-300"
/>
{secretApproval.isReplicated {secretApproval.isReplicated
? `${commits.length} secret pending import` ? `${commits.length} secret pending import`
: generateCommitText(commits)} : generateCommitText(commits)}
<span className="text-xs text-bunker-300"> #{secretApproval.slug}</span> <span className="text-xs text-bunker-300"> #{secretApproval.slug}</span>
</div> </div>
<span className="text-xs leading-3 text-gray-500"> <span className="text-xs text-gray-500">
Opened {formatDistance(new Date(createdAt), new Date())} ago by{" "} Opened {formatDistance(new Date(createdAt), new Date())} ago by{" "}
{committerUser?.firstName || ""} {committerUser?.lastName || ""} ( {committerUser?.firstName || ""} {committerUser?.lastName || ""} (
{committerUser?.email}) {committerUser?.email})
@ -345,24 +257,9 @@ export const SecretApprovalRequest = () => {
</div> </div>
); );
})} })}
{Boolean( </Fragment>
!secretApprovalRequests.length && isFiltered && !isApprovalRequestLoading ))}
) && ( {(isFetchingNextApprovalRequest || 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> <div>
{Array.apply(0, Array(3)).map((_x, index) => ( {Array.apply(0, Array(3)).map((_x, index) => (
<div <div
@ -379,7 +276,18 @@ export const SecretApprovalRequest = () => {
</div> </div>
)} )}
</div> </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>
)}
</motion.div> </motion.div>
)} )}
</AnimatePresence> </AnimatePresence>

View File

@ -56,24 +56,27 @@ export const generateCommitText = (commits: { op: CommitType }[] = [], isReplica
if (score[CommitType.CREATE]) if (score[CommitType.CREATE])
text.push( text.push(
<span key="created-commit"> <span key="created-commit">
{score[CommitType.CREATE]} Secret{score[CommitType.CREATE] !== 1 && "s"} {score[CommitType.CREATE]} secret{score[CommitType.CREATE] !== 1 && "s"}
<span className="text-green-600"> Created</span> <span style={{ color: "#60DD00" }}> created</span>
</span> </span>
); );
if (score[CommitType.UPDATE]) if (score[CommitType.UPDATE])
text.push( text.push(
<span key="updated-commit"> <span key="updated-commit">
{Boolean(text.length) && ", "} {Boolean(text.length) && ","}
{score[CommitType.UPDATE]} Secret{score[CommitType.UPDATE] !== 1 && "s"} {score[CommitType.UPDATE]} secret{score[CommitType.UPDATE] !== 1 && "s"}
<span className="text-yellow-600"> Updated</span> <span style={{ color: "#F8EB30" }} className="text-orange-600">
{" "}
updated
</span>
</span> </span>
); );
if (score[CommitType.DELETE]) if (score[CommitType.DELETE])
text.push( text.push(
<span className="deleted-commit"> <span className="deleted-commit">
{Boolean(text.length) && "and"} {Boolean(text.length) && "and"}
{score[CommitType.DELETE]} Secret{score[CommitType.DELETE] !== 1 && "s"} {score[CommitType.DELETE]} secret{score[CommitType.UPDATE] !== 1 && "s"}
<span className="text-red-600"> Deleted</span> <span style={{ color: "#F83030" }}> deleted</span>
</span> </span>
); );
return text; return text;

View File

@ -37,10 +37,7 @@ const formSchema = z.object({
policyArns: z.string().trim().optional(), policyArns: z.string().trim().optional(),
tags: z tags: z
.array( .array(
z.object({ z.object({ key: z.string().trim().min(1).max(128), value: z.string().trim().min(1).max(256) })
key: z.string().trim().min(1).max(128),
value: z.string().trim().min(1).max(256)
})
) )
.optional() .optional()
}), }),
@ -55,10 +52,7 @@ const formSchema = z.object({
policyArns: z.string().trim().optional(), policyArns: z.string().trim().optional(),
tags: z tags: z
.array( .array(
z.object({ z.object({ key: z.string().trim().min(1).max(128), value: z.string().trim().min(1).max(256) })
key: z.string().trim().min(1).max(128),
value: z.string().trim().min(1).max(256)
})
) )
.optional() .optional()
}) })

View File

@ -26,7 +26,7 @@ const formSchema = z.object({
policyArns: z.string().trim().optional(), policyArns: z.string().trim().optional(),
tags: z tags: z
.array(z.object({ key: z.string().trim().min(1), value: z.string().trim().min(1) })) .array(z.object({ key: z.string().trim().min(1), value: z.string().trim().min(1) }))
.optional() .optional(),
}), }),
z.object({ z.object({
method: z.literal(DynamicSecretAwsIamAuth.AssumeRole), method: z.literal(DynamicSecretAwsIamAuth.AssumeRole),
@ -97,7 +97,7 @@ export const EditDynamicSecretAwsIamForm = ({
usernameTemplate: dynamicSecret?.usernameTemplate || "{{randomUsername}}", usernameTemplate: dynamicSecret?.usernameTemplate || "{{randomUsername}}",
inputs: { inputs: {
...(dynamicSecret.inputs as TForm["inputs"]) ...(dynamicSecret.inputs as TForm["inputs"])
} },
} }
}); });
const isAccessKeyMethod = watch("inputs.method") === DynamicSecretAwsIamAuth.AccessKey; const isAccessKeyMethod = watch("inputs.method") === DynamicSecretAwsIamAuth.AccessKey;
@ -125,7 +125,8 @@ export const EditDynamicSecretAwsIamForm = ({
defaultTTL, defaultTTL,
inputs, inputs,
newName: newName === dynamicSecret.name ? undefined : newName, newName: newName === dynamicSecret.name ? undefined : newName,
usernameTemplate: !usernameTemplate || isDefaultUsernameTemplate ? null : usernameTemplate usernameTemplate:
!usernameTemplate || isDefaultUsernameTemplate ? null : usernameTemplate
} }
}); });
onClose(); onClose();