mirror of
https://github.com/Infisical/infisical.git
synced 2025-07-25 14:07:47 +00:00
Compare commits
46 Commits
feat/add-c
...
daniel/fix
Author | SHA1 | Date | |
---|---|---|---|
|
79e62eec25 | ||
|
aac63d3097 | ||
|
3d2465ae41 | ||
|
f4f34802bc | ||
|
59cc857aef | ||
|
a6713b2f76 | ||
|
3c9a7c77ff | ||
|
f1bfea61d0 | ||
|
42aaddccd5 | ||
|
39abeaaab5 | ||
|
b336c0c3d6 | ||
|
305f2d79de | ||
|
d4a6faa92c | ||
|
4800e9c36e | ||
|
842a2e9a06 | ||
|
de81d2d380 | ||
|
f5d769fa05 | ||
|
b3ace353ce | ||
|
48353ab201 | ||
|
2137d13157 | ||
|
647e13d654 | ||
|
bb2a933a39 | ||
|
6f75debb9c | ||
|
90588bc3c9 | ||
|
4a09fc5e63 | ||
|
f0ec8c883f | ||
|
f5238598aa | ||
|
982aa80092 | ||
|
b30706607f | ||
|
2a3d19dcb2 | ||
|
b4ff620b44 | ||
|
23f1888123 | ||
|
7764f63299 | ||
|
cb3365afd4 | ||
|
58705ffc3f | ||
|
67e57d8993 | ||
|
90ff13a6b5 | ||
|
f85efdc6f8 | ||
|
8680c52412 | ||
|
0ad3c67f82 | ||
|
f75fff0565 | ||
|
1fa1d0a15a | ||
|
e5a967b918 | ||
|
3cfe2223b6 | ||
|
a8eb72a8c5 | ||
|
f76d3e2a14 |
@@ -26,6 +26,7 @@ export const mockQueue = (): TQueueServiceFactory => {
|
||||
getRepeatableJobs: async () => [],
|
||||
clearQueue: async () => {},
|
||||
stopJobById: async () => {},
|
||||
stopJobByIdPg: async () => {},
|
||||
stopRepeatableJobByJobId: async () => true,
|
||||
stopRepeatableJobByKey: async () => true
|
||||
};
|
||||
|
4
backend/src/@types/fastify.d.ts
vendored
4
backend/src/@types/fastify.d.ts
vendored
@@ -10,8 +10,8 @@ import { TAuditLogServiceFactory, TCreateAuditLogDTO } from "@app/ee/services/au
|
||||
import { TAuditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-types";
|
||||
import { TCertificateAuthorityCrlServiceFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-types";
|
||||
import { TCertificateEstServiceFactory } from "@app/ee/services/certificate-est/certificate-est-service";
|
||||
import { TDynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-service";
|
||||
import { TDynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service";
|
||||
import { TDynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-types";
|
||||
import { TDynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-types";
|
||||
import { TExternalKmsServiceFactory } from "@app/ee/services/external-kms/external-kms-service";
|
||||
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
|
||||
import { TGithubOrgSyncServiceFactory } from "@app/ee/services/github-org-sync/github-org-sync-service";
|
||||
|
@@ -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");
|
||||
}
|
||||
});
|
||||
}
|
@@ -29,7 +29,12 @@ export const SuperAdminSchema = z.object({
|
||||
adminIdentityIds: z.string().array().nullable().optional(),
|
||||
encryptedMicrosoftTeamsAppId: 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>;
|
||||
|
@@ -3,9 +3,43 @@ import { Knex } from "knex";
|
||||
import { TDbClient } from "@app/db";
|
||||
import { DynamicSecretLeasesSchema, TableName } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
import { ormify, selectAllTableCols, TOrmify } from "@app/lib/knex";
|
||||
|
||||
export type TDynamicSecretLeaseDALFactory = ReturnType<typeof dynamicSecretLeaseDALFactory>;
|
||||
export interface TDynamicSecretLeaseDALFactory extends Omit<TOrmify<TableName.DynamicSecretLease>, "findById"> {
|
||||
countLeasesForDynamicSecret: (dynamicSecretId: string, tx?: Knex) => Promise<number>;
|
||||
findById: (
|
||||
id: string,
|
||||
tx?: Knex
|
||||
) => Promise<
|
||||
| {
|
||||
dynamicSecret: {
|
||||
id: string;
|
||||
name: string;
|
||||
version: number;
|
||||
type: string;
|
||||
defaultTTL: string;
|
||||
maxTTL: string | null | undefined;
|
||||
encryptedInput: Buffer;
|
||||
folderId: string;
|
||||
status: string | null | undefined;
|
||||
statusDetails: string | null | undefined;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
};
|
||||
version: number;
|
||||
id: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
externalEntityId: string;
|
||||
expireAt: Date;
|
||||
dynamicSecretId: string;
|
||||
status?: string | null | undefined;
|
||||
config?: unknown;
|
||||
statusDetails?: string | null | undefined;
|
||||
}
|
||||
| undefined
|
||||
>;
|
||||
}
|
||||
|
||||
export const dynamicSecretLeaseDALFactory = (db: TDbClient) => {
|
||||
const orm = ormify(db, TableName.DynamicSecretLease);
|
||||
|
@@ -21,7 +21,12 @@ type TDynamicSecretLeaseQueueServiceFactoryDep = {
|
||||
folderDAL: Pick<TSecretFolderDALFactory, "findById">;
|
||||
};
|
||||
|
||||
export type TDynamicSecretLeaseQueueServiceFactory = ReturnType<typeof dynamicSecretLeaseQueueServiceFactory>;
|
||||
export type TDynamicSecretLeaseQueueServiceFactory = {
|
||||
pruneDynamicSecret: (dynamicSecretCfgId: string) => Promise<void>;
|
||||
setLeaseRevocation: (leaseId: string, expiryAt: Date) => Promise<void>;
|
||||
unsetLeaseRevocation: (leaseId: string) => Promise<void>;
|
||||
init: () => Promise<void>;
|
||||
};
|
||||
|
||||
export const dynamicSecretLeaseQueueServiceFactory = ({
|
||||
queueService,
|
||||
@@ -30,55 +35,48 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
|
||||
dynamicSecretLeaseDAL,
|
||||
kmsService,
|
||||
folderDAL
|
||||
}: TDynamicSecretLeaseQueueServiceFactoryDep) => {
|
||||
}: TDynamicSecretLeaseQueueServiceFactoryDep): TDynamicSecretLeaseQueueServiceFactory => {
|
||||
const pruneDynamicSecret = async (dynamicSecretCfgId: string) => {
|
||||
await queueService.queue(
|
||||
QueueName.DynamicSecretRevocation,
|
||||
await queueService.queuePg<QueueName.DynamicSecretRevocation>(
|
||||
QueueJobs.DynamicSecretPruning,
|
||||
{ dynamicSecretCfgId },
|
||||
{
|
||||
jobId: dynamicSecretCfgId,
|
||||
backoff: {
|
||||
type: "exponential",
|
||||
delay: 3000
|
||||
},
|
||||
removeOnFail: {
|
||||
count: 3
|
||||
},
|
||||
removeOnComplete: true
|
||||
singletonKey: dynamicSecretCfgId,
|
||||
retryLimit: 3,
|
||||
retryBackoff: true
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const setLeaseRevocation = async (leaseId: string, expiry: number) => {
|
||||
await queueService.queue(
|
||||
QueueName.DynamicSecretRevocation,
|
||||
const setLeaseRevocation = async (leaseId: string, expiryAt: Date) => {
|
||||
await queueService.queuePg<QueueName.DynamicSecretRevocation>(
|
||||
QueueJobs.DynamicSecretRevocation,
|
||||
{ leaseId },
|
||||
{
|
||||
jobId: leaseId,
|
||||
backoff: {
|
||||
type: "exponential",
|
||||
delay: 3000
|
||||
},
|
||||
delay: expiry,
|
||||
removeOnFail: {
|
||||
count: 3
|
||||
},
|
||||
removeOnComplete: true
|
||||
id: leaseId,
|
||||
singletonKey: leaseId,
|
||||
startAfter: expiryAt,
|
||||
retryLimit: 3,
|
||||
retryBackoff: true,
|
||||
retentionDays: 2
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const unsetLeaseRevocation = async (leaseId: string) => {
|
||||
await queueService.stopJobById(QueueName.DynamicSecretRevocation, leaseId);
|
||||
await queueService.stopJobByIdPg(QueueName.DynamicSecretRevocation, leaseId);
|
||||
};
|
||||
|
||||
queueService.start(QueueName.DynamicSecretRevocation, async (job) => {
|
||||
const $dynamicSecretQueueJob = async (
|
||||
jobName: string,
|
||||
jobId: string,
|
||||
data: { leaseId: string } | { dynamicSecretCfgId: string }
|
||||
): Promise<void> => {
|
||||
try {
|
||||
if (job.name === QueueJobs.DynamicSecretRevocation) {
|
||||
const { leaseId } = job.data as { leaseId: string };
|
||||
logger.info("Dynamic secret lease revocation started: ", leaseId, job.id);
|
||||
if (jobName === QueueJobs.DynamicSecretRevocation) {
|
||||
const { leaseId } = data as { leaseId: string };
|
||||
logger.info("Dynamic secret lease revocation started: ", leaseId, jobId);
|
||||
|
||||
const dynamicSecretLease = await dynamicSecretLeaseDAL.findById(leaseId);
|
||||
if (!dynamicSecretLease) throw new DisableRotationErrors({ message: "Dynamic secret lease not found" });
|
||||
@@ -107,9 +105,9 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
|
||||
return;
|
||||
}
|
||||
|
||||
if (job.name === QueueJobs.DynamicSecretPruning) {
|
||||
const { dynamicSecretCfgId } = job.data as { dynamicSecretCfgId: string };
|
||||
logger.info("Dynamic secret pruning started: ", dynamicSecretCfgId, job.id);
|
||||
if (jobName === QueueJobs.DynamicSecretPruning) {
|
||||
const { dynamicSecretCfgId } = data as { dynamicSecretCfgId: string };
|
||||
logger.info("Dynamic secret pruning started: ", dynamicSecretCfgId, jobId);
|
||||
const dynamicSecretCfg = await dynamicSecretDAL.findById(dynamicSecretCfgId);
|
||||
if (!dynamicSecretCfg) throw new DisableRotationErrors({ message: "Dynamic secret not found" });
|
||||
if ((dynamicSecretCfg.status as DynamicSecretStatus) !== DynamicSecretStatus.Deleting)
|
||||
@@ -150,38 +148,68 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
|
||||
|
||||
await dynamicSecretDAL.deleteById(dynamicSecretCfgId);
|
||||
}
|
||||
logger.info("Finished dynamic secret job", job.id);
|
||||
logger.info("Finished dynamic secret job", jobId);
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
|
||||
if (job?.name === QueueJobs.DynamicSecretPruning) {
|
||||
const { dynamicSecretCfgId } = job.data as { dynamicSecretCfgId: string };
|
||||
if (jobName === QueueJobs.DynamicSecretPruning) {
|
||||
const { dynamicSecretCfgId } = data as { dynamicSecretCfgId: string };
|
||||
await dynamicSecretDAL.updateById(dynamicSecretCfgId, {
|
||||
status: DynamicSecretStatus.FailedDeletion,
|
||||
statusDetails: (error as Error)?.message?.slice(0, 255)
|
||||
});
|
||||
}
|
||||
|
||||
if (job?.name === QueueJobs.DynamicSecretRevocation) {
|
||||
const { leaseId } = job.data as { leaseId: string };
|
||||
if (jobName === QueueJobs.DynamicSecretRevocation) {
|
||||
const { leaseId } = data as { leaseId: string };
|
||||
await dynamicSecretLeaseDAL.updateById(leaseId, {
|
||||
status: DynamicSecretStatus.FailedDeletion,
|
||||
statusDetails: (error as Error)?.message?.slice(0, 255)
|
||||
});
|
||||
}
|
||||
if (error instanceof DisableRotationErrors) {
|
||||
if (job.id) {
|
||||
await queueService.stopRepeatableJobByJobId(QueueName.DynamicSecretRevocation, job.id);
|
||||
if (jobId) {
|
||||
await queueService.stopRepeatableJobByJobId(QueueName.DynamicSecretRevocation, jobId);
|
||||
await queueService.stopJobByIdPg(QueueName.DynamicSecretRevocation, jobId);
|
||||
}
|
||||
}
|
||||
// propogate to next part
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
queueService.start(QueueName.DynamicSecretRevocation, async (job) => {
|
||||
await $dynamicSecretQueueJob(job.name, job.id as string, job.data);
|
||||
});
|
||||
|
||||
const init = async () => {
|
||||
await queueService.startPg<QueueName.DynamicSecretRevocation>(
|
||||
QueueJobs.DynamicSecretRevocation,
|
||||
async ([job]) => {
|
||||
await $dynamicSecretQueueJob(job.name, job.id, job.data);
|
||||
},
|
||||
{
|
||||
workerCount: 5,
|
||||
pollingIntervalSeconds: 1
|
||||
}
|
||||
);
|
||||
|
||||
await queueService.startPg<QueueName.DynamicSecretRevocation>(
|
||||
QueueJobs.DynamicSecretPruning,
|
||||
async ([job]) => {
|
||||
await $dynamicSecretQueueJob(job.name, job.id, job.data);
|
||||
},
|
||||
{
|
||||
workerCount: 1,
|
||||
pollingIntervalSeconds: 1
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
pruneDynamicSecret,
|
||||
setLeaseRevocation,
|
||||
unsetLeaseRevocation
|
||||
unsetLeaseRevocation,
|
||||
init
|
||||
};
|
||||
};
|
||||
|
@@ -26,12 +26,8 @@ import { TDynamicSecretLeaseDALFactory } from "./dynamic-secret-lease-dal";
|
||||
import { TDynamicSecretLeaseQueueServiceFactory } from "./dynamic-secret-lease-queue";
|
||||
import {
|
||||
DynamicSecretLeaseStatus,
|
||||
TCreateDynamicSecretLeaseDTO,
|
||||
TDeleteDynamicSecretLeaseDTO,
|
||||
TDetailsDynamicSecretLeaseDTO,
|
||||
TDynamicSecretLeaseConfig,
|
||||
TListDynamicSecretLeasesDTO,
|
||||
TRenewDynamicSecretLeaseDTO
|
||||
TDynamicSecretLeaseServiceFactory
|
||||
} from "./dynamic-secret-lease-types";
|
||||
|
||||
type TDynamicSecretLeaseServiceFactoryDep = {
|
||||
@@ -48,8 +44,6 @@ type TDynamicSecretLeaseServiceFactoryDep = {
|
||||
identityDAL: TIdentityDALFactory;
|
||||
};
|
||||
|
||||
export type TDynamicSecretLeaseServiceFactory = ReturnType<typeof dynamicSecretLeaseServiceFactory>;
|
||||
|
||||
export const dynamicSecretLeaseServiceFactory = ({
|
||||
dynamicSecretLeaseDAL,
|
||||
dynamicSecretProviders,
|
||||
@@ -62,14 +56,14 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
kmsService,
|
||||
userDAL,
|
||||
identityDAL
|
||||
}: TDynamicSecretLeaseServiceFactoryDep) => {
|
||||
}: TDynamicSecretLeaseServiceFactoryDep): TDynamicSecretLeaseServiceFactory => {
|
||||
const extractEmailUsername = (email: string) => {
|
||||
const regex = new RE2(/^([^@]+)/);
|
||||
const match = email.match(regex);
|
||||
return match ? match[1] : email;
|
||||
};
|
||||
|
||||
const create = async ({
|
||||
const create: TDynamicSecretLeaseServiceFactory["create"] = async ({
|
||||
environmentSlug,
|
||||
path,
|
||||
name,
|
||||
@@ -80,7 +74,7 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
ttl,
|
||||
config
|
||||
}: TCreateDynamicSecretLeaseDTO) => {
|
||||
}) => {
|
||||
const appCfg = getConfig();
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
||||
@@ -184,11 +178,11 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
config
|
||||
});
|
||||
|
||||
await dynamicSecretQueueService.setLeaseRevocation(dynamicSecretLease.id, Number(expireAt) - Number(new Date()));
|
||||
await dynamicSecretQueueService.setLeaseRevocation(dynamicSecretLease.id, expireAt);
|
||||
return { lease: dynamicSecretLease, dynamicSecret: dynamicSecretCfg, data };
|
||||
};
|
||||
|
||||
const renewLease = async ({
|
||||
const renewLease: TDynamicSecretLeaseServiceFactory["renewLease"] = async ({
|
||||
ttl,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
@@ -198,7 +192,7 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
path,
|
||||
environmentSlug,
|
||||
leaseId
|
||||
}: TRenewDynamicSecretLeaseDTO) => {
|
||||
}) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
||||
|
||||
@@ -278,7 +272,7 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
);
|
||||
|
||||
await dynamicSecretQueueService.unsetLeaseRevocation(dynamicSecretLease.id);
|
||||
await dynamicSecretQueueService.setLeaseRevocation(dynamicSecretLease.id, Number(expireAt) - Number(new Date()));
|
||||
await dynamicSecretQueueService.setLeaseRevocation(dynamicSecretLease.id, expireAt);
|
||||
const updatedDynamicSecretLease = await dynamicSecretLeaseDAL.updateById(dynamicSecretLease.id, {
|
||||
expireAt,
|
||||
externalEntityId: entityId
|
||||
@@ -286,7 +280,7 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
return updatedDynamicSecretLease;
|
||||
};
|
||||
|
||||
const revokeLease = async ({
|
||||
const revokeLease: TDynamicSecretLeaseServiceFactory["revokeLease"] = async ({
|
||||
leaseId,
|
||||
environmentSlug,
|
||||
path,
|
||||
@@ -296,7 +290,7 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
isForced
|
||||
}: TDeleteDynamicSecretLeaseDTO) => {
|
||||
}) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
||||
|
||||
@@ -376,7 +370,7 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
return deletedDynamicSecretLease;
|
||||
};
|
||||
|
||||
const listLeases = async ({
|
||||
const listLeases: TDynamicSecretLeaseServiceFactory["listLeases"] = async ({
|
||||
path,
|
||||
name,
|
||||
actor,
|
||||
@@ -385,7 +379,7 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
actorOrgId,
|
||||
environmentSlug,
|
||||
actorAuthMethod
|
||||
}: TListDynamicSecretLeasesDTO) => {
|
||||
}) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
||||
|
||||
@@ -424,7 +418,7 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
return dynamicSecretLeases;
|
||||
};
|
||||
|
||||
const getLeaseDetails = async ({
|
||||
const getLeaseDetails: TDynamicSecretLeaseServiceFactory["getLeaseDetails"] = async ({
|
||||
projectSlug,
|
||||
actorOrgId,
|
||||
path,
|
||||
@@ -433,7 +427,7 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
actorId,
|
||||
leaseId,
|
||||
actorAuthMethod
|
||||
}: TDetailsDynamicSecretLeaseDTO) => {
|
||||
}) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
||||
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
import { TDynamicSecretLeases } from "@app/db/schemas";
|
||||
import { TDynamicSecretWithMetadata, TProjectPermission } from "@app/lib/types";
|
||||
|
||||
export enum DynamicSecretLeaseStatus {
|
||||
FailedDeletion = "Failed to delete"
|
||||
@@ -48,3 +49,40 @@ export type TDynamicSecretKubernetesLeaseConfig = {
|
||||
};
|
||||
|
||||
export type TDynamicSecretLeaseConfig = TDynamicSecretKubernetesLeaseConfig;
|
||||
|
||||
export type TDynamicSecretLeaseServiceFactory = {
|
||||
create: (arg: TCreateDynamicSecretLeaseDTO) => Promise<{
|
||||
lease: TDynamicSecretLeases;
|
||||
dynamicSecret: TDynamicSecretWithMetadata;
|
||||
data: unknown;
|
||||
}>;
|
||||
listLeases: (arg: TListDynamicSecretLeasesDTO) => Promise<TDynamicSecretLeases[]>;
|
||||
revokeLease: (arg: TDeleteDynamicSecretLeaseDTO) => Promise<TDynamicSecretLeases>;
|
||||
renewLease: (arg: TRenewDynamicSecretLeaseDTO) => Promise<TDynamicSecretLeases>;
|
||||
getLeaseDetails: (arg: TDetailsDynamicSecretLeaseDTO) => Promise<{
|
||||
dynamicSecret: {
|
||||
id: string;
|
||||
name: string;
|
||||
version: number;
|
||||
type: string;
|
||||
defaultTTL: string;
|
||||
maxTTL: string | null | undefined;
|
||||
encryptedInput: Buffer;
|
||||
folderId: string;
|
||||
status: string | null | undefined;
|
||||
statusDetails: string | null | undefined;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
};
|
||||
version: number;
|
||||
id: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
externalEntityId: string;
|
||||
expireAt: Date;
|
||||
dynamicSecretId: string;
|
||||
status?: string | null | undefined;
|
||||
config?: unknown;
|
||||
statusDetails?: string | null | undefined;
|
||||
}>;
|
||||
};
|
||||
|
@@ -10,17 +10,35 @@ import {
|
||||
selectAllTableCols,
|
||||
sqlNestRelationships,
|
||||
TFindFilter,
|
||||
TFindOpt
|
||||
TFindOpt,
|
||||
TOrmify
|
||||
} from "@app/lib/knex";
|
||||
import { OrderByDirection } from "@app/lib/types";
|
||||
import { OrderByDirection, TDynamicSecretWithMetadata } from "@app/lib/types";
|
||||
import { SecretsOrderBy } from "@app/services/secret/secret-types";
|
||||
|
||||
export type TDynamicSecretDALFactory = ReturnType<typeof dynamicSecretDALFactory>;
|
||||
export interface TDynamicSecretDALFactory extends Omit<TOrmify<TableName.DynamicSecret>, "findOne"> {
|
||||
findOne: (filter: TFindFilter<TDynamicSecrets>, tx?: Knex) => Promise<TDynamicSecretWithMetadata>;
|
||||
listDynamicSecretsByFolderIds: (
|
||||
arg: {
|
||||
folderIds: string[];
|
||||
search?: string | undefined;
|
||||
limit?: number | undefined;
|
||||
offset?: number | undefined;
|
||||
orderBy?: SecretsOrderBy | undefined;
|
||||
orderDirection?: OrderByDirection | undefined;
|
||||
},
|
||||
tx?: Knex
|
||||
) => Promise<Array<TDynamicSecretWithMetadata & { environment: string }>>;
|
||||
findWithMetadata: (
|
||||
filter: TFindFilter<TDynamicSecrets>,
|
||||
arg?: TFindOpt<TDynamicSecrets>
|
||||
) => Promise<TDynamicSecretWithMetadata[]>;
|
||||
}
|
||||
|
||||
export const dynamicSecretDALFactory = (db: TDbClient) => {
|
||||
export const dynamicSecretDALFactory = (db: TDbClient): TDynamicSecretDALFactory => {
|
||||
const orm = ormify(db, TableName.DynamicSecret);
|
||||
|
||||
const findOne = async (filter: TFindFilter<TDynamicSecrets>, tx?: Knex) => {
|
||||
const findOne: TDynamicSecretDALFactory["findOne"] = async (filter, tx) => {
|
||||
const query = (tx || db.replicaNode())(TableName.DynamicSecret)
|
||||
.leftJoin(
|
||||
TableName.ResourceMetadata,
|
||||
@@ -55,9 +73,9 @@ export const dynamicSecretDALFactory = (db: TDbClient) => {
|
||||
return docs[0];
|
||||
};
|
||||
|
||||
const findWithMetadata = async (
|
||||
filter: TFindFilter<TDynamicSecrets>,
|
||||
{ offset, limit, sort, tx }: TFindOpt<TDynamicSecrets> = {}
|
||||
const findWithMetadata: TDynamicSecretDALFactory["findWithMetadata"] = async (
|
||||
filter,
|
||||
{ offset, limit, sort, tx } = {}
|
||||
) => {
|
||||
const query = (tx || db.replicaNode())(TableName.DynamicSecret)
|
||||
.leftJoin(
|
||||
@@ -101,23 +119,9 @@ export const dynamicSecretDALFactory = (db: TDbClient) => {
|
||||
};
|
||||
|
||||
// find dynamic secrets for multiple environments (folder IDs are cross env, thus need to rank for pagination)
|
||||
const listDynamicSecretsByFolderIds = async (
|
||||
{
|
||||
folderIds,
|
||||
search,
|
||||
limit,
|
||||
offset = 0,
|
||||
orderBy = SecretsOrderBy.Name,
|
||||
orderDirection = OrderByDirection.ASC
|
||||
}: {
|
||||
folderIds: string[];
|
||||
search?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
orderBy?: SecretsOrderBy;
|
||||
orderDirection?: OrderByDirection;
|
||||
},
|
||||
tx?: Knex
|
||||
const listDynamicSecretsByFolderIds: TDynamicSecretDALFactory["listDynamicSecretsByFolderIds"] = async (
|
||||
{ folderIds, search, limit, offset = 0, orderBy = SecretsOrderBy.Name, orderDirection = OrderByDirection.ASC },
|
||||
tx
|
||||
) => {
|
||||
try {
|
||||
const query = (tx || db.replicaNode())(TableName.DynamicSecret)
|
||||
|
@@ -8,7 +8,7 @@ import {
|
||||
ProjectPermissionSub
|
||||
} from "@app/ee/services/permission/project-permission";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { OrderByDirection, OrgServiceActor } from "@app/lib/types";
|
||||
import { OrderByDirection } from "@app/lib/types";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
@@ -20,17 +20,7 @@ import { TDynamicSecretLeaseQueueServiceFactory } from "../dynamic-secret-lease/
|
||||
import { TGatewayDALFactory } from "../gateway/gateway-dal";
|
||||
import { OrgPermissionGatewayActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||
import { TDynamicSecretDALFactory } from "./dynamic-secret-dal";
|
||||
import {
|
||||
DynamicSecretStatus,
|
||||
TCreateDynamicSecretDTO,
|
||||
TDeleteDynamicSecretDTO,
|
||||
TDetailsDynamicSecretDTO,
|
||||
TGetDynamicSecretsCountDTO,
|
||||
TListDynamicSecretsByFolderMappingsDTO,
|
||||
TListDynamicSecretsDTO,
|
||||
TListDynamicSecretsMultiEnvDTO,
|
||||
TUpdateDynamicSecretDTO
|
||||
} from "./dynamic-secret-types";
|
||||
import { DynamicSecretStatus, TDynamicSecretServiceFactory } from "./dynamic-secret-types";
|
||||
import { AzureEntraIDProvider } from "./providers/azure-entra-id";
|
||||
import { DynamicSecretProviders, TDynamicProviderFns } from "./providers/models";
|
||||
|
||||
@@ -51,8 +41,6 @@ type TDynamicSecretServiceFactoryDep = {
|
||||
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
|
||||
};
|
||||
|
||||
export type TDynamicSecretServiceFactory = ReturnType<typeof dynamicSecretServiceFactory>;
|
||||
|
||||
export const dynamicSecretServiceFactory = ({
|
||||
dynamicSecretDAL,
|
||||
dynamicSecretLeaseDAL,
|
||||
@@ -65,8 +53,8 @@ export const dynamicSecretServiceFactory = ({
|
||||
kmsService,
|
||||
gatewayDAL,
|
||||
resourceMetadataDAL
|
||||
}: TDynamicSecretServiceFactoryDep) => {
|
||||
const create = async ({
|
||||
}: TDynamicSecretServiceFactoryDep): TDynamicSecretServiceFactory => {
|
||||
const create: TDynamicSecretServiceFactory["create"] = async ({
|
||||
path,
|
||||
actor,
|
||||
name,
|
||||
@@ -80,7 +68,7 @@ export const dynamicSecretServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
metadata,
|
||||
usernameTemplate
|
||||
}: TCreateDynamicSecretDTO) => {
|
||||
}) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
||||
|
||||
@@ -188,7 +176,7 @@ export const dynamicSecretServiceFactory = ({
|
||||
return dynamicSecretCfg;
|
||||
};
|
||||
|
||||
const updateByName = async ({
|
||||
const updateByName: TDynamicSecretServiceFactory["updateByName"] = async ({
|
||||
name,
|
||||
maxTTL,
|
||||
defaultTTL,
|
||||
@@ -203,7 +191,7 @@ export const dynamicSecretServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
metadata,
|
||||
usernameTemplate
|
||||
}: TUpdateDynamicSecretDTO) => {
|
||||
}) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
||||
|
||||
@@ -345,7 +333,7 @@ export const dynamicSecretServiceFactory = ({
|
||||
return updatedDynamicCfg;
|
||||
};
|
||||
|
||||
const deleteByName = async ({
|
||||
const deleteByName: TDynamicSecretServiceFactory["deleteByName"] = async ({
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actorId,
|
||||
@@ -355,7 +343,7 @@ export const dynamicSecretServiceFactory = ({
|
||||
path,
|
||||
environmentSlug,
|
||||
isForced
|
||||
}: TDeleteDynamicSecretDTO) => {
|
||||
}) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
||||
|
||||
@@ -413,7 +401,7 @@ export const dynamicSecretServiceFactory = ({
|
||||
return deletedDynamicSecretCfg;
|
||||
};
|
||||
|
||||
const getDetails = async ({
|
||||
const getDetails: TDynamicSecretServiceFactory["getDetails"] = async ({
|
||||
name,
|
||||
projectSlug,
|
||||
path,
|
||||
@@ -422,7 +410,7 @@ export const dynamicSecretServiceFactory = ({
|
||||
actorOrgId,
|
||||
actorId,
|
||||
actor
|
||||
}: TDetailsDynamicSecretDTO) => {
|
||||
}) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
||||
|
||||
@@ -480,7 +468,7 @@ export const dynamicSecretServiceFactory = ({
|
||||
};
|
||||
|
||||
// get unique dynamic secret count across multiple envs
|
||||
const getCountMultiEnv = async ({
|
||||
const getCountMultiEnv: TDynamicSecretServiceFactory["getCountMultiEnv"] = async ({
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actorId,
|
||||
@@ -490,7 +478,7 @@ export const dynamicSecretServiceFactory = ({
|
||||
environmentSlugs,
|
||||
search,
|
||||
isInternal
|
||||
}: TListDynamicSecretsMultiEnvDTO) => {
|
||||
}) => {
|
||||
if (!isInternal) {
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
@@ -526,7 +514,7 @@ export const dynamicSecretServiceFactory = ({
|
||||
};
|
||||
|
||||
// get dynamic secret count for a single env
|
||||
const getDynamicSecretCount = async ({
|
||||
const getDynamicSecretCount: TDynamicSecretServiceFactory["getDynamicSecretCount"] = async ({
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actorId,
|
||||
@@ -535,7 +523,7 @@ export const dynamicSecretServiceFactory = ({
|
||||
environmentSlug,
|
||||
search,
|
||||
projectId
|
||||
}: TGetDynamicSecretsCountDTO) => {
|
||||
}) => {
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
@@ -561,7 +549,7 @@ export const dynamicSecretServiceFactory = ({
|
||||
return Number(dynamicSecretCfg[0]?.count ?? 0);
|
||||
};
|
||||
|
||||
const listDynamicSecretsByEnv = async ({
|
||||
const listDynamicSecretsByEnv: TDynamicSecretServiceFactory["listDynamicSecretsByEnv"] = async ({
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actorId,
|
||||
@@ -575,7 +563,7 @@ export const dynamicSecretServiceFactory = ({
|
||||
orderDirection = OrderByDirection.ASC,
|
||||
search,
|
||||
...params
|
||||
}: TListDynamicSecretsDTO) => {
|
||||
}) => {
|
||||
let { projectId } = params;
|
||||
|
||||
if (!projectId) {
|
||||
@@ -619,9 +607,9 @@ export const dynamicSecretServiceFactory = ({
|
||||
});
|
||||
};
|
||||
|
||||
const listDynamicSecretsByFolderIds = async (
|
||||
{ folderMappings, filters, projectId }: TListDynamicSecretsByFolderMappingsDTO,
|
||||
actor: OrgServiceActor
|
||||
const listDynamicSecretsByFolderIds: TDynamicSecretServiceFactory["listDynamicSecretsByFolderIds"] = async (
|
||||
{ folderMappings, filters, projectId },
|
||||
actor
|
||||
) => {
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
@@ -657,7 +645,7 @@ export const dynamicSecretServiceFactory = ({
|
||||
};
|
||||
|
||||
// get dynamic secrets for multiple envs
|
||||
const listDynamicSecretsByEnvs = async ({
|
||||
const listDynamicSecretsByEnvs: TDynamicSecretServiceFactory["listDynamicSecretsByEnvs"] = async ({
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actorId,
|
||||
@@ -667,7 +655,7 @@ export const dynamicSecretServiceFactory = ({
|
||||
projectId,
|
||||
isInternal,
|
||||
...params
|
||||
}: TListDynamicSecretsMultiEnvDTO) => {
|
||||
}) => {
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
@@ -700,14 +688,10 @@ export const dynamicSecretServiceFactory = ({
|
||||
});
|
||||
};
|
||||
|
||||
const fetchAzureEntraIdUsers = async ({
|
||||
const fetchAzureEntraIdUsers: TDynamicSecretServiceFactory["fetchAzureEntraIdUsers"] = async ({
|
||||
tenantId,
|
||||
applicationId,
|
||||
clientSecret
|
||||
}: {
|
||||
tenantId: string;
|
||||
applicationId: string;
|
||||
clientSecret: string;
|
||||
}) => {
|
||||
const azureEntraIdUsers = await AzureEntraIDProvider().fetchAzureEntraIdUsers(
|
||||
tenantId,
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { OrderByDirection, TProjectPermission } from "@app/lib/types";
|
||||
import { TDynamicSecrets } from "@app/db/schemas";
|
||||
import { OrderByDirection, OrgServiceActor, TDynamicSecretWithMetadata, TProjectPermission } from "@app/lib/types";
|
||||
import { ResourceMetadataDTO } from "@app/services/resource-metadata/resource-metadata-schema";
|
||||
import { SecretsOrderBy } from "@app/services/secret/secret-types";
|
||||
|
||||
@@ -83,3 +84,27 @@ export type TListDynamicSecretsMultiEnvDTO = Omit<
|
||||
export type TGetDynamicSecretsCountDTO = Omit<TListDynamicSecretsDTO, "projectSlug" | "projectId"> & {
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export type TDynamicSecretServiceFactory = {
|
||||
create: (arg: TCreateDynamicSecretDTO) => Promise<TDynamicSecrets>;
|
||||
updateByName: (arg: TUpdateDynamicSecretDTO) => Promise<TDynamicSecrets>;
|
||||
deleteByName: (arg: TDeleteDynamicSecretDTO) => Promise<TDynamicSecrets>;
|
||||
getDetails: (arg: TDetailsDynamicSecretDTO) => Promise<TDynamicSecretWithMetadata>;
|
||||
listDynamicSecretsByEnv: (arg: TListDynamicSecretsDTO) => Promise<TDynamicSecretWithMetadata[]>;
|
||||
listDynamicSecretsByEnvs: (
|
||||
arg: TListDynamicSecretsMultiEnvDTO
|
||||
) => Promise<Array<TDynamicSecretWithMetadata & { environment: string }>>;
|
||||
getDynamicSecretCount: (arg: TGetDynamicSecretsCountDTO) => Promise<number>;
|
||||
getCountMultiEnv: (arg: TListDynamicSecretsMultiEnvDTO) => Promise<number>;
|
||||
fetchAzureEntraIdUsers: (arg: { tenantId: string; applicationId: string; clientSecret: string }) => Promise<
|
||||
{
|
||||
name: string;
|
||||
id: string;
|
||||
email: string;
|
||||
}[]
|
||||
>;
|
||||
listDynamicSecretsByFolderIds: (
|
||||
arg: TListDynamicSecretsByFolderMappingsDTO,
|
||||
actor: OrgServiceActor
|
||||
) => Promise<Array<TDynamicSecretWithMetadata & { environment: string; path: string }>>;
|
||||
};
|
||||
|
@@ -52,9 +52,8 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
||||
gatewayId: string;
|
||||
targetHost: string;
|
||||
targetPort: number;
|
||||
caCert?: string;
|
||||
httpsAgent?: https.Agent;
|
||||
reviewTokenThroughGateway: boolean;
|
||||
enableSsl: boolean;
|
||||
},
|
||||
gatewayCallback: (host: string, port: number, httpsAgent?: https.Agent) => Promise<T>
|
||||
): Promise<T> => {
|
||||
@@ -85,10 +84,7 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
||||
key: relayDetails.privateKey.toString()
|
||||
},
|
||||
// we always pass this, because its needed for both tcp and http protocol
|
||||
httpsAgent: new https.Agent({
|
||||
ca: inputs.caCert,
|
||||
rejectUnauthorized: inputs.enableSsl
|
||||
})
|
||||
httpsAgent: inputs.httpsAgent
|
||||
}
|
||||
);
|
||||
|
||||
@@ -311,6 +307,14 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
||||
const k8sHost = `${url.protocol}//${url.hostname}`;
|
||||
|
||||
try {
|
||||
const httpsAgent =
|
||||
providerInputs.ca && providerInputs.sslEnabled
|
||||
? new https.Agent({
|
||||
ca: providerInputs.ca,
|
||||
rejectUnauthorized: true
|
||||
})
|
||||
: undefined;
|
||||
|
||||
if (providerInputs.gatewayId) {
|
||||
if (providerInputs.authMethod === KubernetesAuthMethod.Gateway) {
|
||||
await $gatewayProxyWrapper(
|
||||
@@ -318,8 +322,7 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
||||
gatewayId: providerInputs.gatewayId,
|
||||
targetHost: k8sHost,
|
||||
targetPort: k8sPort,
|
||||
enableSsl: providerInputs.sslEnabled,
|
||||
caCert: providerInputs.ca,
|
||||
httpsAgent,
|
||||
reviewTokenThroughGateway: true
|
||||
},
|
||||
providerInputs.credentialType === KubernetesCredentialType.Static
|
||||
@@ -332,8 +335,7 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
||||
gatewayId: providerInputs.gatewayId,
|
||||
targetHost: k8sGatewayHost,
|
||||
targetPort: k8sPort,
|
||||
enableSsl: providerInputs.sslEnabled,
|
||||
caCert: providerInputs.ca,
|
||||
httpsAgent,
|
||||
reviewTokenThroughGateway: false
|
||||
},
|
||||
providerInputs.credentialType === KubernetesCredentialType.Static
|
||||
@@ -342,9 +344,9 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
||||
);
|
||||
}
|
||||
} else if (providerInputs.credentialType === KubernetesCredentialType.Static) {
|
||||
await serviceAccountStaticCallback(k8sHost, k8sPort);
|
||||
await serviceAccountStaticCallback(k8sHost, k8sPort, httpsAgent);
|
||||
} else {
|
||||
await serviceAccountDynamicCallback(k8sHost, k8sPort);
|
||||
await serviceAccountDynamicCallback(k8sHost, k8sPort, httpsAgent);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -546,6 +548,15 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
||||
|
||||
try {
|
||||
let tokenData;
|
||||
|
||||
const httpsAgent =
|
||||
providerInputs.ca && providerInputs.sslEnabled
|
||||
? new https.Agent({
|
||||
ca: providerInputs.ca,
|
||||
rejectUnauthorized: true
|
||||
})
|
||||
: undefined;
|
||||
|
||||
if (providerInputs.gatewayId) {
|
||||
if (providerInputs.authMethod === KubernetesAuthMethod.Gateway) {
|
||||
tokenData = await $gatewayProxyWrapper(
|
||||
@@ -553,8 +564,7 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
||||
gatewayId: providerInputs.gatewayId,
|
||||
targetHost: k8sHost,
|
||||
targetPort: k8sPort,
|
||||
enableSsl: providerInputs.sslEnabled,
|
||||
caCert: providerInputs.ca,
|
||||
httpsAgent,
|
||||
reviewTokenThroughGateway: true
|
||||
},
|
||||
providerInputs.credentialType === KubernetesCredentialType.Static
|
||||
@@ -567,8 +577,7 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
||||
gatewayId: providerInputs.gatewayId,
|
||||
targetHost: k8sGatewayHost,
|
||||
targetPort: k8sPort,
|
||||
enableSsl: providerInputs.sslEnabled,
|
||||
caCert: providerInputs.ca,
|
||||
httpsAgent,
|
||||
reviewTokenThroughGateway: false
|
||||
},
|
||||
providerInputs.credentialType === KubernetesCredentialType.Static
|
||||
@@ -579,8 +588,8 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
||||
} else {
|
||||
tokenData =
|
||||
providerInputs.credentialType === KubernetesCredentialType.Static
|
||||
? await tokenRequestStaticCallback(k8sHost, k8sPort)
|
||||
: await serviceAccountDynamicCallback(k8sHost, k8sPort);
|
||||
? await tokenRequestStaticCallback(k8sHost, k8sPort, httpsAgent)
|
||||
: await serviceAccountDynamicCallback(k8sHost, k8sPort, httpsAgent);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -684,6 +693,14 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
||||
const k8sPort = url.port ? Number(url.port) : 443;
|
||||
const k8sHost = `${url.protocol}//${url.hostname}`;
|
||||
|
||||
const httpsAgent =
|
||||
providerInputs.ca && providerInputs.sslEnabled
|
||||
? new https.Agent({
|
||||
ca: providerInputs.ca,
|
||||
rejectUnauthorized: true
|
||||
})
|
||||
: undefined;
|
||||
|
||||
if (providerInputs.gatewayId) {
|
||||
if (providerInputs.authMethod === KubernetesAuthMethod.Gateway) {
|
||||
await $gatewayProxyWrapper(
|
||||
@@ -691,8 +708,7 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
||||
gatewayId: providerInputs.gatewayId,
|
||||
targetHost: k8sHost,
|
||||
targetPort: k8sPort,
|
||||
enableSsl: providerInputs.sslEnabled,
|
||||
caCert: providerInputs.ca,
|
||||
httpsAgent,
|
||||
reviewTokenThroughGateway: true
|
||||
},
|
||||
serviceAccountDynamicCallback
|
||||
@@ -703,15 +719,14 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
||||
gatewayId: providerInputs.gatewayId,
|
||||
targetHost: k8sGatewayHost,
|
||||
targetPort: k8sPort,
|
||||
enableSsl: providerInputs.sslEnabled,
|
||||
caCert: providerInputs.ca,
|
||||
httpsAgent,
|
||||
reviewTokenThroughGateway: false
|
||||
},
|
||||
serviceAccountDynamicCallback
|
||||
);
|
||||
}
|
||||
} else {
|
||||
await serviceAccountDynamicCallback(k8sHost, k8sPort);
|
||||
await serviceAccountDynamicCallback(k8sHost, k8sPort, httpsAgent);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -11,7 +11,8 @@ export const PgSqlLock = {
|
||||
OrgGatewayRootCaInit: (orgId: string) => pgAdvisoryLockHashText(`org-gateway-root-ca:${orgId}`),
|
||||
OrgGatewayCertExchange: (orgId: string) => pgAdvisoryLockHashText(`org-gateway-cert-exchange:${orgId}`),
|
||||
SecretRotationV2Creation: (folderId: string) => pgAdvisoryLockHashText(`secret-rotation-v2-creation:${folderId}`),
|
||||
CreateProject: (orgId: string) => pgAdvisoryLockHashText(`create-project:${orgId}`)
|
||||
CreateProject: (orgId: string) => pgAdvisoryLockHashText(`create-project:${orgId}`),
|
||||
CreateFolder: (envId: string, projectId: string) => pgAdvisoryLockHashText(`create-folder:${envId}-${projectId}`)
|
||||
} as const;
|
||||
|
||||
// all the key prefixes used must be set here to avoid conflict
|
||||
|
@@ -19,3 +19,5 @@ export const getMinExpiresIn = (exp1: string | number, exp2: string | number): s
|
||||
|
||||
return ms1 <= ms2 ? exp1 : exp2;
|
||||
};
|
||||
|
||||
export const convertMsToSecond = (time: number) => time / 1000;
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import { TDynamicSecrets } from "@app/db/schemas";
|
||||
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
|
||||
|
||||
export type TGenericPermission = {
|
||||
@@ -84,3 +85,7 @@ export enum QueueWorkerProfile {
|
||||
Standard = "standard",
|
||||
SecretScanning = "secret-scanning"
|
||||
}
|
||||
|
||||
export interface TDynamicSecretWithMetadata extends TDynamicSecrets {
|
||||
metadata: { id: string; key: string; value: string }[];
|
||||
}
|
||||
|
@@ -377,6 +377,7 @@ export type TQueueServiceFactory = {
|
||||
stopRepeatableJobByKey: <T extends QueueName>(name: T, repeatJobKey: string) => Promise<boolean>;
|
||||
clearQueue: (name: QueueName) => Promise<void>;
|
||||
stopJobById: <T extends QueueName>(name: T, jobId: string) => Promise<void | undefined>;
|
||||
stopJobByIdPg: <T extends QueueName>(name: T, jobId: string) => Promise<void | undefined>;
|
||||
getRepeatableJobs: (
|
||||
name: QueueName,
|
||||
startOffset?: number,
|
||||
@@ -542,6 +543,10 @@ export const queueServiceFactory = (
|
||||
return q.removeRepeatableByKey(repeatJobKey);
|
||||
};
|
||||
|
||||
const stopJobByIdPg: TQueueServiceFactory["stopJobByIdPg"] = async (name, jobId) => {
|
||||
await pgBoss.deleteJob(name, jobId);
|
||||
};
|
||||
|
||||
const stopJobById: TQueueServiceFactory["stopJobById"] = async (name, jobId) => {
|
||||
const q = queueContainer[name];
|
||||
const job = await q.getJob(jobId);
|
||||
@@ -568,6 +573,7 @@ export const queueServiceFactory = (
|
||||
stopRepeatableJobByKey,
|
||||
clearQueue,
|
||||
stopJobById,
|
||||
stopJobByIdPg,
|
||||
getRepeatableJobs,
|
||||
startPg,
|
||||
queuePg,
|
||||
|
@@ -1903,6 +1903,7 @@ export const registerRoutes = async (
|
||||
await pkiSubscriberQueue.startDailyAutoRenewalJob();
|
||||
await kmsService.startService();
|
||||
await microsoftTeamsService.start();
|
||||
await dynamicSecretQueueService.init();
|
||||
|
||||
// inject all services
|
||||
server.decorate<FastifyZodProvider["services"]>("services", {
|
||||
@@ -2020,10 +2021,16 @@ export const registerRoutes = async (
|
||||
if (licenseSyncJob) {
|
||||
cronJobs.push(licenseSyncJob);
|
||||
}
|
||||
|
||||
const microsoftTeamsSyncJob = await microsoftTeamsService.initializeBackgroundSync();
|
||||
if (microsoftTeamsSyncJob) {
|
||||
cronJobs.push(microsoftTeamsSyncJob);
|
||||
}
|
||||
|
||||
const adminIntegrationsSyncJob = await superAdminService.initializeAdminIntegrationConfigSync();
|
||||
if (adminIntegrationsSyncJob) {
|
||||
cronJobs.push(adminIntegrationsSyncJob);
|
||||
}
|
||||
}
|
||||
|
||||
server.decorate<FastifyZodProvider["store"]>("store", {
|
||||
|
@@ -37,7 +37,12 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
||||
encryptedSlackClientSecret: true,
|
||||
encryptedMicrosoftTeamsAppId: true,
|
||||
encryptedMicrosoftTeamsClientSecret: true,
|
||||
encryptedMicrosoftTeamsBotId: true
|
||||
encryptedMicrosoftTeamsBotId: true,
|
||||
encryptedGitHubAppConnectionClientId: true,
|
||||
encryptedGitHubAppConnectionClientSecret: true,
|
||||
encryptedGitHubAppConnectionSlug: true,
|
||||
encryptedGitHubAppConnectionId: true,
|
||||
encryptedGitHubAppConnectionPrivateKey: true
|
||||
}).extend({
|
||||
isMigrationModeOn: z.boolean(),
|
||||
defaultAuthOrgSlug: z.string().nullable(),
|
||||
@@ -87,6 +92,11 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
||||
microsoftTeamsAppId: z.string().optional(),
|
||||
microsoftTeamsClientSecret: z.string().optional(),
|
||||
microsoftTeamsBotId: z.string().optional(),
|
||||
gitHubAppConnectionClientId: z.string().optional(),
|
||||
gitHubAppConnectionClientSecret: z.string().optional(),
|
||||
gitHubAppConnectionSlug: z.string().optional(),
|
||||
gitHubAppConnectionId: z.string().optional(),
|
||||
gitHubAppConnectionPrivateKey: z.string().optional(),
|
||||
authConsentContent: z
|
||||
.string()
|
||||
.trim()
|
||||
@@ -348,6 +358,13 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
||||
appId: z.string(),
|
||||
clientSecret: z.string(),
|
||||
botId: z.string()
|
||||
}),
|
||||
gitHubAppConnection: z.object({
|
||||
clientId: z.string(),
|
||||
clientSecret: z.string(),
|
||||
appSlug: z.string(),
|
||||
appId: z.string(),
|
||||
privateKey: z.string()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@@ -83,7 +83,7 @@ export const registerInviteOrgRouter = async (server: FastifyZodProvider) => {
|
||||
config: {
|
||||
rateLimit: smtpRateLimit({
|
||||
keyGenerator: (req) =>
|
||||
(req.body as { membershipId?: string })?.membershipId?.trim().substring(0, 100) ?? req.realIp
|
||||
(req.body as { membershipId?: string })?.membershipId?.trim().substring(0, 100) || req.realIp
|
||||
})
|
||||
},
|
||||
method: "POST",
|
||||
|
@@ -81,7 +81,7 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
|
||||
url: "/email/password-reset",
|
||||
config: {
|
||||
rateLimit: smtpRateLimit({
|
||||
keyGenerator: (req) => (req.body as { email?: string })?.email?.trim().substring(0, 100) ?? req.realIp
|
||||
keyGenerator: (req) => (req.body as { email?: string })?.email?.trim().substring(0, 100) || req.realIp
|
||||
})
|
||||
},
|
||||
schema: {
|
||||
@@ -107,7 +107,9 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
|
||||
method: "POST",
|
||||
url: "/email/password-reset-verify",
|
||||
config: {
|
||||
rateLimit: authRateLimit
|
||||
rateLimit: smtpRateLimit({
|
||||
keyGenerator: (req) => (req.body as { email?: string })?.email?.trim().substring(0, 100) || req.realIp
|
||||
})
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
|
@@ -2,7 +2,7 @@ import { z } from "zod";
|
||||
|
||||
import { AuthTokenSessionsSchema, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas";
|
||||
import { ApiKeysSchema } from "@app/db/schemas/api-keys";
|
||||
import { authRateLimit, readLimit, smtpRateLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { readLimit, smtpRateLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMethod, AuthMode, MfaMethod } from "@app/services/auth/auth-type";
|
||||
import { sanitizedOrganizationSchema } from "@app/services/org/org-schema";
|
||||
@@ -13,7 +13,7 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
|
||||
url: "/me/emails/code",
|
||||
config: {
|
||||
rateLimit: smtpRateLimit({
|
||||
keyGenerator: (req) => (req.body as { username?: string })?.username?.trim().substring(0, 100) ?? req.realIp
|
||||
keyGenerator: (req) => (req.body as { username?: string })?.username?.trim().substring(0, 100) || req.realIp
|
||||
})
|
||||
},
|
||||
schema: {
|
||||
@@ -34,7 +34,9 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
|
||||
method: "POST",
|
||||
url: "/me/emails/verify",
|
||||
config: {
|
||||
rateLimit: authRateLimit
|
||||
rateLimit: smtpRateLimit({
|
||||
keyGenerator: (req) => (req.body as { username?: string })?.username?.trim().substring(0, 100) || req.realIp
|
||||
})
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
|
@@ -4,7 +4,7 @@ import { z } from "zod";
|
||||
import { SecretApprovalRequestsSchema, SecretsSchema, SecretType, ServiceTokenScopes } from "@app/db/schemas";
|
||||
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { ApiDocsTags, RAW_SECRETS, SECRETS } from "@app/lib/api-docs";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { secretsLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { BaseSecretNameSchema, SecretNameSchema } from "@app/server/lib/schemas";
|
||||
@@ -12,7 +12,6 @@ import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||
import { getUserAgentType } from "@app/server/plugins/audit-log";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
|
||||
import { ProjectFilterType } from "@app/services/project/project-types";
|
||||
import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema";
|
||||
import { SecretOperations, SecretProtectionType } from "@app/services/secret/secret-types";
|
||||
import { SecretUpdateMode } from "@app/services/secret-v2-bridge/secret-v2-bridge-types";
|
||||
@@ -286,22 +285,17 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
environment = scope[0].environment;
|
||||
workspaceId = req.auth.serviceToken.projectId;
|
||||
}
|
||||
} else if (req.permission.type === ActorType.IDENTITY && req.query.workspaceSlug && !workspaceId) {
|
||||
const workspace = await server.services.project.getAProject({
|
||||
filter: {
|
||||
type: ProjectFilterType.SLUG,
|
||||
orgId: req.permission.orgId,
|
||||
slug: req.query.workspaceSlug
|
||||
},
|
||||
} else {
|
||||
const projectId = await server.services.project.extractProjectIdFromSlug({
|
||||
projectSlug: req.query.workspaceSlug,
|
||||
projectId: workspaceId,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
if (!workspace) throw new NotFoundError({ message: `No project found with slug ${req.query.workspaceSlug}` });
|
||||
|
||||
workspaceId = workspace.id;
|
||||
workspaceId = projectId;
|
||||
}
|
||||
|
||||
if (!workspaceId || !environment) throw new BadRequestError({ message: "Missing workspace id or environment" });
|
||||
@@ -442,11 +436,23 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
environment = scope[0].environment;
|
||||
workspaceId = req.auth.serviceToken.projectId;
|
||||
}
|
||||
} else {
|
||||
const projectId = await server.services.project.extractProjectIdFromSlug({
|
||||
projectSlug: workspaceSlug,
|
||||
projectId: workspaceId,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
workspaceId = projectId;
|
||||
}
|
||||
|
||||
if (!environment) throw new BadRequestError({ message: "Missing environment" });
|
||||
if (!workspaceId && !workspaceSlug)
|
||||
if (!workspaceId) {
|
||||
throw new BadRequestError({ message: "You must provide workspaceSlug or workspaceId" });
|
||||
}
|
||||
|
||||
const secret = await server.services.secret.getSecretByNameRaw({
|
||||
actorId: req.permission.id,
|
||||
@@ -457,7 +463,6 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
environment,
|
||||
projectId: workspaceId,
|
||||
viewSecretValue: req.query.viewSecretValue,
|
||||
projectSlug: workspaceSlug,
|
||||
path: secretPath,
|
||||
secretName: req.params.secretName,
|
||||
type: req.query.type,
|
||||
@@ -518,7 +523,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
secretName: SecretNameSchema.describe(RAW_SECRETS.CREATE.secretName)
|
||||
}),
|
||||
body: z.object({
|
||||
workspaceId: z.string().trim().describe(RAW_SECRETS.CREATE.workspaceId),
|
||||
workspaceId: z.string().trim().optional().describe(RAW_SECRETS.CREATE.workspaceId),
|
||||
projectSlug: z.string().trim().optional().describe(RAW_SECRETS.CREATE.projectSlug),
|
||||
environment: z.string().trim().describe(RAW_SECRETS.CREATE.environment),
|
||||
secretPath: z
|
||||
.string()
|
||||
@@ -558,13 +564,22 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const projectId = await server.services.project.extractProjectIdFromSlug({
|
||||
projectSlug: req.body.projectSlug,
|
||||
projectId: req.body.workspaceId,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
const secretOperation = await server.services.secret.createSecretRaw({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
environment: req.body.environment,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
projectId: req.body.workspaceId,
|
||||
projectId,
|
||||
secretPath: req.body.secretPath,
|
||||
secretName: req.params.secretName,
|
||||
type: req.body.type,
|
||||
@@ -582,7 +597,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
const { secret } = secretOperation;
|
||||
await server.services.auditLog.createAuditLog({
|
||||
projectId: req.body.workspaceId,
|
||||
projectId,
|
||||
...req.auditLogInfo,
|
||||
event: {
|
||||
type: EventType.CREATE_SECRET,
|
||||
@@ -602,7 +617,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
properties: {
|
||||
numberOfSecrets: 1,
|
||||
workspaceId: req.body.workspaceId,
|
||||
workspaceId: projectId,
|
||||
environment: req.body.environment,
|
||||
secretPath: req.body.secretPath,
|
||||
channel: getUserAgentType(req.headers["user-agent"]),
|
||||
@@ -633,7 +648,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
secretName: BaseSecretNameSchema.describe(RAW_SECRETS.UPDATE.secretName)
|
||||
}),
|
||||
body: z.object({
|
||||
workspaceId: z.string().trim().describe(RAW_SECRETS.UPDATE.workspaceId),
|
||||
workspaceId: z.string().trim().optional().describe(RAW_SECRETS.UPDATE.workspaceId),
|
||||
projectSlug: z.string().trim().optional().describe(RAW_SECRETS.UPDATE.projectSlug),
|
||||
environment: z.string().trim().describe(RAW_SECRETS.UPDATE.environment),
|
||||
secretValue: z
|
||||
.string()
|
||||
@@ -679,13 +695,22 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const projectId = await server.services.project.extractProjectIdFromSlug({
|
||||
projectSlug: req.body.projectSlug,
|
||||
projectId: req.body.workspaceId,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
const secretOperation = await server.services.secret.updateSecretRaw({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
environment: req.body.environment,
|
||||
projectId: req.body.workspaceId,
|
||||
projectId,
|
||||
secretPath: req.body.secretPath,
|
||||
secretName: req.params.secretName,
|
||||
type: req.body.type,
|
||||
@@ -707,7 +732,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
const { secret } = secretOperation;
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
projectId: req.body.workspaceId,
|
||||
projectId,
|
||||
...req.auditLogInfo,
|
||||
event: {
|
||||
type: EventType.UPDATE_SECRET,
|
||||
@@ -727,7 +752,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
properties: {
|
||||
numberOfSecrets: 1,
|
||||
workspaceId: req.body.workspaceId,
|
||||
workspaceId: projectId,
|
||||
environment: req.body.environment,
|
||||
secretPath: req.body.secretPath,
|
||||
channel: getUserAgentType(req.headers["user-agent"]),
|
||||
@@ -757,7 +782,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
secretName: z.string().min(1).describe(RAW_SECRETS.DELETE.secretName)
|
||||
}),
|
||||
body: z.object({
|
||||
workspaceId: z.string().trim().describe(RAW_SECRETS.DELETE.workspaceId),
|
||||
workspaceId: z.string().trim().optional().describe(RAW_SECRETS.DELETE.workspaceId),
|
||||
projectSlug: z.string().trim().optional().describe(RAW_SECRETS.DELETE.projectSlug),
|
||||
environment: z.string().trim().describe(RAW_SECRETS.DELETE.environment),
|
||||
secretPath: z
|
||||
.string()
|
||||
@@ -780,13 +806,22 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const projectId = await server.services.project.extractProjectIdFromSlug({
|
||||
projectSlug: req.body.projectSlug,
|
||||
projectId: req.body.workspaceId,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
const secretOperation = await server.services.secret.deleteSecretRaw({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
environment: req.body.environment,
|
||||
projectId: req.body.workspaceId,
|
||||
projectId,
|
||||
secretPath: req.body.secretPath,
|
||||
secretName: req.params.secretName,
|
||||
type: req.body.type
|
||||
@@ -798,7 +833,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
const { secret } = secretOperation;
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
projectId: req.body.workspaceId,
|
||||
projectId,
|
||||
...req.auditLogInfo,
|
||||
event: {
|
||||
type: EventType.DELETE_SECRET,
|
||||
@@ -817,7 +852,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
properties: {
|
||||
numberOfSecrets: 1,
|
||||
workspaceId: req.body.workspaceId,
|
||||
workspaceId: projectId,
|
||||
environment: req.body.environment,
|
||||
secretPath: req.body.secretPath,
|
||||
channel: getUserAgentType(req.headers["user-agent"]),
|
||||
|
@@ -14,7 +14,7 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
|
||||
method: "POST",
|
||||
config: {
|
||||
rateLimit: smtpRateLimit({
|
||||
keyGenerator: (req) => (req.body as { email?: string })?.email?.trim().substring(0, 100) ?? req.realIp
|
||||
keyGenerator: (req) => (req.body as { email?: string })?.email?.trim().substring(0, 100) || req.realIp
|
||||
})
|
||||
},
|
||||
schema: {
|
||||
@@ -55,7 +55,9 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
|
||||
url: "/email/verify",
|
||||
method: "POST",
|
||||
config: {
|
||||
rateLimit: authRateLimit
|
||||
rateLimit: smtpRateLimit({
|
||||
keyGenerator: (req) => (req.body as { email?: string })?.email?.trim().substring(0, 100) || req.realIp
|
||||
})
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
|
@@ -7,6 +7,7 @@ import { request } from "@app/lib/config/request";
|
||||
import { BadRequestError, ForbiddenRequestError, InternalServerError } from "@app/lib/errors";
|
||||
import { getAppConnectionMethodName } from "@app/services/app-connection/app-connection-fns";
|
||||
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
||||
import { getInstanceIntegrationsConfig } from "@app/services/super-admin/super-admin-service";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import { GitHubConnectionMethod } from "./github-connection-enums";
|
||||
@@ -14,13 +15,14 @@ import { TGitHubConnection, TGitHubConnectionConfig } from "./github-connection-
|
||||
|
||||
export const getGitHubConnectionListItem = () => {
|
||||
const { INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID, INF_APP_CONNECTION_GITHUB_APP_SLUG } = getConfig();
|
||||
const { gitHubAppConnection } = getInstanceIntegrationsConfig();
|
||||
|
||||
return {
|
||||
name: "GitHub" as const,
|
||||
app: AppConnection.GitHub as const,
|
||||
methods: Object.values(GitHubConnectionMethod) as [GitHubConnectionMethod.App, GitHubConnectionMethod.OAuth],
|
||||
oauthClientId: INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID,
|
||||
appClientSlug: 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;
|
||||
|
||||
let client: Octokit;
|
||||
const { gitHubAppConnection } = getInstanceIntegrationsConfig();
|
||||
|
||||
const appId = gitHubAppConnection.appId || appCfg.INF_APP_CONNECTION_GITHUB_APP_ID;
|
||||
const appPrivateKey = gitHubAppConnection.privateKey || appCfg.INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY;
|
||||
|
||||
switch (method) {
|
||||
case GitHubConnectionMethod.App:
|
||||
if (!appCfg.INF_APP_CONNECTION_GITHUB_APP_ID || !appCfg.INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY) {
|
||||
if (!appId || !appPrivateKey) {
|
||||
throw new InternalServerError({
|
||||
message: `GitHub ${getAppConnectionMethodName(method).replace(
|
||||
"GitHub",
|
||||
""
|
||||
)} environment variables have not been configured`
|
||||
message: `GitHub ${getAppConnectionMethodName(method).replace("GitHub", "")} has not been configured`
|
||||
});
|
||||
}
|
||||
|
||||
client = new Octokit({
|
||||
authStrategy: createAppAuth,
|
||||
auth: {
|
||||
appId: appCfg.INF_APP_CONNECTION_GITHUB_APP_ID,
|
||||
privateKey: appCfg.INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY,
|
||||
appId,
|
||||
privateKey: appPrivateKey,
|
||||
installationId: credentials.installationId
|
||||
}
|
||||
});
|
||||
@@ -154,6 +157,8 @@ type TokenRespData = {
|
||||
export const validateGitHubConnectionCredentials = async (config: TGitHubConnectionConfig) => {
|
||||
const { credentials, method } = config;
|
||||
|
||||
const { gitHubAppConnection } = getInstanceIntegrationsConfig();
|
||||
|
||||
const {
|
||||
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID,
|
||||
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_SECRET,
|
||||
@@ -165,8 +170,8 @@ export const validateGitHubConnectionCredentials = async (config: TGitHubConnect
|
||||
const { clientId, clientSecret } =
|
||||
method === GitHubConnectionMethod.App
|
||||
? {
|
||||
clientId: INF_APP_CONNECTION_GITHUB_APP_CLIENT_ID,
|
||||
clientSecret: INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET
|
||||
clientId: gitHubAppConnection.clientId || INF_APP_CONNECTION_GITHUB_APP_CLIENT_ID,
|
||||
clientSecret: gitHubAppConnection.clientSecret || INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET
|
||||
}
|
||||
: // oauth
|
||||
{
|
||||
|
@@ -42,7 +42,7 @@ import { TProjectPermission } from "@app/lib/types";
|
||||
import { TQueueServiceFactory } from "@app/queue";
|
||||
import { TPkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal";
|
||||
|
||||
import { ActorType } from "../auth/auth-type";
|
||||
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
||||
import { TCertificateDALFactory } from "../certificate/certificate-dal";
|
||||
import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal";
|
||||
import { expandInternalCa } from "../certificate-authority/certificate-authority-fns";
|
||||
@@ -82,6 +82,7 @@ import { assignWorkspaceKeysToMembers, bootstrapSshProject, createProjectKey } f
|
||||
import { TProjectQueueFactory } from "./project-queue";
|
||||
import { TProjectSshConfigDALFactory } from "./project-ssh-config-dal";
|
||||
import {
|
||||
ProjectFilterType,
|
||||
TCreateProjectDTO,
|
||||
TDeleteProjectDTO,
|
||||
TDeleteProjectWorkflowIntegration,
|
||||
@@ -866,6 +867,39 @@ export const projectServiceFactory = ({
|
||||
});
|
||||
};
|
||||
|
||||
const extractProjectIdFromSlug = async ({
|
||||
projectSlug,
|
||||
projectId,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: {
|
||||
projectSlug?: string;
|
||||
projectId?: string;
|
||||
actorId: string;
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
actor: ActorType;
|
||||
actorOrgId: string;
|
||||
}) => {
|
||||
if (projectId) return projectId;
|
||||
if (!projectSlug) throw new BadRequestError({ message: "You must provide projectSlug or workspaceId" });
|
||||
const project = await getAProject({
|
||||
filter: {
|
||||
type: ProjectFilterType.SLUG,
|
||||
orgId: actorOrgId,
|
||||
slug: projectSlug
|
||||
},
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
});
|
||||
|
||||
if (!project) throw new NotFoundError({ message: `No project found with slug ${projectSlug}` });
|
||||
return project.id;
|
||||
};
|
||||
|
||||
const getProjectUpgradeStatus = async ({
|
||||
projectId,
|
||||
actor,
|
||||
@@ -2006,6 +2040,7 @@ export const projectServiceFactory = ({
|
||||
getProjectSshConfig,
|
||||
updateProjectSshConfig,
|
||||
requestProjectAccess,
|
||||
searchProjects
|
||||
searchProjects,
|
||||
extractProjectIdFromSlug
|
||||
};
|
||||
};
|
||||
|
@@ -6,6 +6,7 @@ import { ActionProjectType, TSecretFoldersInsert } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service";
|
||||
import { PgSqlLock } from "@app/keystore/keystore";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { OrderByDirection, OrgServiceActor } from "@app/lib/types";
|
||||
import { buildFolderPath } from "@app/services/secret-folder/secret-folder-fns";
|
||||
@@ -83,36 +84,75 @@ export const secretFolderServiceFactory = ({
|
||||
// that is this request must be idempotent
|
||||
// so we do a tricky move. we try to find the to be created folder path if that is exactly match return that
|
||||
// else we get some path before that then we will start creating remaining folder
|
||||
await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.CreateFolder(env.id, env.projectId)]);
|
||||
|
||||
const pathWithFolder = path.join(secretPath, name);
|
||||
const parentFolder = await folderDAL.findClosestFolder(projectId, environment, pathWithFolder, tx);
|
||||
// no folder found is not possible root should be their
|
||||
|
||||
if (!parentFolder) {
|
||||
throw new NotFoundError({
|
||||
message: `Folder with path '${pathWithFolder}' in environment with slug '${environment}' not found`
|
||||
message: `Parent folder for path '${pathWithFolder}' not found`
|
||||
});
|
||||
}
|
||||
// exact folder
|
||||
if (parentFolder.path === pathWithFolder) return parentFolder;
|
||||
|
||||
let parentFolderId = parentFolder.id;
|
||||
// check if the exact folder already exists
|
||||
const existingFolder = await folderDAL.findOne(
|
||||
{
|
||||
envId: env.id,
|
||||
parentId: parentFolder.id,
|
||||
name,
|
||||
isReserved: false
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
if (existingFolder) {
|
||||
return existingFolder;
|
||||
}
|
||||
|
||||
// exact folder case
|
||||
if (parentFolder.path === pathWithFolder) {
|
||||
return parentFolder;
|
||||
}
|
||||
|
||||
let currentParentId = parentFolder.id;
|
||||
|
||||
// build the full path we need by processing each segment
|
||||
if (parentFolder.path !== secretPath) {
|
||||
// this is upsert folder in a path
|
||||
// we are not taking snapshots of this because
|
||||
// snapshot will be removed from automatic for all commits to user click or cron based
|
||||
const missingSegment = secretPath.substring(parentFolder.path.length).split("/").filter(Boolean);
|
||||
if (missingSegment.length) {
|
||||
const newFolders: Array<TSecretFoldersInsert & { id: string }> = missingSegment.map((segment) => {
|
||||
const missingSegments = secretPath.substring(parentFolder.path.length).split("/").filter(Boolean);
|
||||
|
||||
const newFolders: TSecretFoldersInsert[] = [];
|
||||
|
||||
// process each segment sequentially
|
||||
for await (const segment of missingSegments) {
|
||||
const existingSegment = await folderDAL.findOne(
|
||||
{
|
||||
name: segment,
|
||||
parentId: currentParentId,
|
||||
envId: env.id,
|
||||
isReserved: false
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
if (existingSegment) {
|
||||
// use existing folder and update the path / parent
|
||||
currentParentId = existingSegment.id;
|
||||
} else {
|
||||
const newFolder = {
|
||||
name: segment,
|
||||
parentId: parentFolderId,
|
||||
parentId: currentParentId,
|
||||
id: uuidv4(),
|
||||
envId: env.id,
|
||||
version: 1
|
||||
};
|
||||
parentFolderId = newFolder.id;
|
||||
return newFolder;
|
||||
});
|
||||
parentFolderId = newFolders.at(-1)?.id as string;
|
||||
|
||||
currentParentId = newFolder.id;
|
||||
newFolders.push(newFolder);
|
||||
}
|
||||
}
|
||||
|
||||
if (newFolders.length) {
|
||||
const docs = await folderDAL.insertMany(newFolders, tx);
|
||||
const folderVersions = await folderVersionDAL.insertMany(
|
||||
docs.map((doc) => ({
|
||||
@@ -133,7 +173,7 @@ export const secretFolderServiceFactory = ({
|
||||
}
|
||||
},
|
||||
message: "Folder created",
|
||||
folderId: parentFolderId,
|
||||
folderId: currentParentId,
|
||||
changes: folderVersions.map((fv) => ({
|
||||
type: CommitType.ADD,
|
||||
folderVersionId: fv.id
|
||||
@@ -145,9 +185,10 @@ export const secretFolderServiceFactory = ({
|
||||
}
|
||||
|
||||
const doc = await folderDAL.create(
|
||||
{ name, envId: env.id, version: 1, parentId: parentFolderId, description },
|
||||
{ name, envId: env.id, version: 1, parentId: currentParentId, description },
|
||||
tx
|
||||
);
|
||||
|
||||
const folderVersion = await folderVersionDAL.create(
|
||||
{
|
||||
name: doc.name,
|
||||
@@ -158,6 +199,7 @@ export const secretFolderServiceFactory = ({
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
await folderCommitService.createCommit(
|
||||
{
|
||||
actor: {
|
||||
@@ -167,7 +209,7 @@ export const secretFolderServiceFactory = ({
|
||||
}
|
||||
},
|
||||
message: "Folder created",
|
||||
folderId: parentFolderId,
|
||||
folderId: doc.id,
|
||||
changes: [
|
||||
{
|
||||
type: CommitType.ADD,
|
||||
@@ -177,6 +219,7 @@ export const secretFolderServiceFactory = ({
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
return doc;
|
||||
});
|
||||
|
||||
|
@@ -1543,9 +1543,8 @@ export const secretServiceFactory = ({
|
||||
actor,
|
||||
environment,
|
||||
viewSecretValue,
|
||||
projectId: workspaceId,
|
||||
projectId,
|
||||
expandSecretReferences,
|
||||
projectSlug,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
@@ -1553,7 +1552,6 @@ export const secretServiceFactory = ({
|
||||
includeImports,
|
||||
version
|
||||
}: TGetASecretRawDTO) => {
|
||||
const projectId = workspaceId || (await projectDAL.findProjectBySlug(projectSlug as string, actorOrgId)).id;
|
||||
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
|
||||
if (shouldUseSecretV2Bridge) {
|
||||
const secret = await secretV2BridgeService.getSecretByName({
|
||||
|
@@ -229,8 +229,7 @@ export type TGetASecretRawDTO = {
|
||||
type: "shared" | "personal";
|
||||
includeImports?: boolean;
|
||||
version?: number;
|
||||
projectSlug?: string;
|
||||
projectId?: string;
|
||||
projectId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetASecretByIdRawDTO = {
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import bcrypt from "bcrypt";
|
||||
import { CronJob } from "cron";
|
||||
import jwt from "jsonwebtoken";
|
||||
|
||||
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 { generateUserSrpKeys, getUserPrivateKey } from "@app/lib/crypto/srp";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { TIdentityDALFactory } from "@app/services/identity/identity-dal";
|
||||
|
||||
import { TAuthLoginFactory } from "../auth/auth-login-service";
|
||||
@@ -35,6 +37,7 @@ import {
|
||||
TAdminBootstrapInstanceDTO,
|
||||
TAdminGetIdentitiesDTO,
|
||||
TAdminGetUsersDTO,
|
||||
TAdminIntegrationConfig,
|
||||
TAdminSignUpDTO,
|
||||
TGetOrganizationsDTO
|
||||
} 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_EXP = 60; // 60s
|
||||
export const ADMIN_CONFIG_DB_UUID = "00000000-0000-0000-0000-000000000000";
|
||||
@@ -138,6 +166,74 @@ export const superAdminServiceFactory = ({
|
||||
return serverCfg;
|
||||
};
|
||||
|
||||
const getAdminIntegrationsConfig = async () => {
|
||||
const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
|
||||
|
||||
if (!serverCfg) {
|
||||
throw new NotFoundError({ name: "AdminConfig", message: "Admin config not found" });
|
||||
}
|
||||
|
||||
const decrypt = kmsService.decryptWithRootKey();
|
||||
|
||||
const slackClientId = serverCfg.encryptedSlackClientId ? decrypt(serverCfg.encryptedSlackClientId).toString() : "";
|
||||
const slackClientSecret = serverCfg.encryptedSlackClientSecret
|
||||
? decrypt(serverCfg.encryptedSlackClientSecret).toString()
|
||||
: "";
|
||||
|
||||
const microsoftAppId = serverCfg.encryptedMicrosoftTeamsAppId
|
||||
? decrypt(serverCfg.encryptedMicrosoftTeamsAppId).toString()
|
||||
: "";
|
||||
const microsoftClientSecret = serverCfg.encryptedMicrosoftTeamsClientSecret
|
||||
? decrypt(serverCfg.encryptedMicrosoftTeamsClientSecret).toString()
|
||||
: "";
|
||||
const microsoftBotId = serverCfg.encryptedMicrosoftTeamsBotId
|
||||
? decrypt(serverCfg.encryptedMicrosoftTeamsBotId).toString()
|
||||
: "";
|
||||
|
||||
const gitHubAppConnectionClientId = serverCfg.encryptedGitHubAppConnectionClientId
|
||||
? decrypt(serverCfg.encryptedGitHubAppConnectionClientId).toString()
|
||||
: "";
|
||||
const gitHubAppConnectionClientSecret = serverCfg.encryptedGitHubAppConnectionClientSecret
|
||||
? decrypt(serverCfg.encryptedGitHubAppConnectionClientSecret).toString()
|
||||
: "";
|
||||
|
||||
const gitHubAppConnectionAppSlug = serverCfg.encryptedGitHubAppConnectionSlug
|
||||
? decrypt(serverCfg.encryptedGitHubAppConnectionSlug).toString()
|
||||
: "";
|
||||
|
||||
const gitHubAppConnectionAppId = serverCfg.encryptedGitHubAppConnectionId
|
||||
? decrypt(serverCfg.encryptedGitHubAppConnectionId).toString()
|
||||
: "";
|
||||
const gitHubAppConnectionAppPrivateKey = serverCfg.encryptedGitHubAppConnectionPrivateKey
|
||||
? decrypt(serverCfg.encryptedGitHubAppConnectionPrivateKey).toString()
|
||||
: "";
|
||||
|
||||
return {
|
||||
slack: {
|
||||
clientSecret: slackClientSecret,
|
||||
clientId: slackClientId
|
||||
},
|
||||
microsoftTeams: {
|
||||
appId: microsoftAppId,
|
||||
clientSecret: microsoftClientSecret,
|
||||
botId: microsoftBotId
|
||||
},
|
||||
gitHubAppConnection: {
|
||||
clientId: gitHubAppConnectionClientId,
|
||||
clientSecret: gitHubAppConnectionClientSecret,
|
||||
appSlug: gitHubAppConnectionAppSlug,
|
||||
appId: gitHubAppConnectionAppId,
|
||||
privateKey: gitHubAppConnectionAppPrivateKey
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const $syncAdminIntegrationConfig = async () => {
|
||||
const config = await getAdminIntegrationsConfig();
|
||||
Object.freeze(config);
|
||||
adminIntegrationsConfig = config;
|
||||
};
|
||||
|
||||
const updateServerCfg = async (
|
||||
data: TSuperAdminUpdate & {
|
||||
slackClientId?: string;
|
||||
@@ -145,6 +241,11 @@ export const superAdminServiceFactory = ({
|
||||
microsoftTeamsAppId?: string;
|
||||
microsoftTeamsClientSecret?: string;
|
||||
microsoftTeamsBotId?: string;
|
||||
gitHubAppConnectionClientId?: string;
|
||||
gitHubAppConnectionClientSecret?: string;
|
||||
gitHubAppConnectionSlug?: string;
|
||||
gitHubAppConnectionId?: string;
|
||||
gitHubAppConnectionPrivateKey?: string;
|
||||
},
|
||||
userId: string
|
||||
) => {
|
||||
@@ -236,10 +337,51 @@ export const superAdminServiceFactory = ({
|
||||
updatedData.microsoftTeamsBotId = undefined;
|
||||
microsoftTeamsSettingsUpdated = true;
|
||||
}
|
||||
|
||||
let gitHubAppConnectionSettingsUpdated = false;
|
||||
if (data.gitHubAppConnectionClientId !== undefined) {
|
||||
const encryptedClientId = encryptWithRoot(Buffer.from(data.gitHubAppConnectionClientId));
|
||||
updatedData.encryptedGitHubAppConnectionClientId = encryptedClientId;
|
||||
updatedData.gitHubAppConnectionClientId = undefined;
|
||||
gitHubAppConnectionSettingsUpdated = true;
|
||||
}
|
||||
|
||||
if (data.gitHubAppConnectionClientSecret !== undefined) {
|
||||
const encryptedClientSecret = encryptWithRoot(Buffer.from(data.gitHubAppConnectionClientSecret));
|
||||
updatedData.encryptedGitHubAppConnectionClientSecret = encryptedClientSecret;
|
||||
updatedData.gitHubAppConnectionClientSecret = undefined;
|
||||
gitHubAppConnectionSettingsUpdated = true;
|
||||
}
|
||||
|
||||
if (data.gitHubAppConnectionSlug !== undefined) {
|
||||
const encryptedAppSlug = encryptWithRoot(Buffer.from(data.gitHubAppConnectionSlug));
|
||||
updatedData.encryptedGitHubAppConnectionSlug = encryptedAppSlug;
|
||||
updatedData.gitHubAppConnectionSlug = undefined;
|
||||
gitHubAppConnectionSettingsUpdated = true;
|
||||
}
|
||||
|
||||
if (data.gitHubAppConnectionId !== undefined) {
|
||||
const encryptedAppId = encryptWithRoot(Buffer.from(data.gitHubAppConnectionId));
|
||||
updatedData.encryptedGitHubAppConnectionId = encryptedAppId;
|
||||
updatedData.gitHubAppConnectionId = undefined;
|
||||
gitHubAppConnectionSettingsUpdated = true;
|
||||
}
|
||||
|
||||
if (data.gitHubAppConnectionPrivateKey !== undefined) {
|
||||
const encryptedAppPrivateKey = encryptWithRoot(Buffer.from(data.gitHubAppConnectionPrivateKey));
|
||||
updatedData.encryptedGitHubAppConnectionPrivateKey = encryptedAppPrivateKey;
|
||||
updatedData.gitHubAppConnectionPrivateKey = undefined;
|
||||
gitHubAppConnectionSettingsUpdated = true;
|
||||
}
|
||||
|
||||
const updatedServerCfg = await serverCfgDAL.updateById(ADMIN_CONFIG_DB_UUID, updatedData);
|
||||
|
||||
await keyStore.setItemWithExpiry(ADMIN_CONFIG_KEY, ADMIN_CONFIG_KEY_EXP, JSON.stringify(updatedServerCfg));
|
||||
|
||||
if (gitHubAppConnectionSettingsUpdated) {
|
||||
await $syncAdminIntegrationConfig();
|
||||
}
|
||||
|
||||
if (
|
||||
updatedServerCfg.encryptedMicrosoftTeamsAppId &&
|
||||
updatedServerCfg.encryptedMicrosoftTeamsClientSecret &&
|
||||
@@ -593,43 +735,6 @@ export const superAdminServiceFactory = ({
|
||||
await userDAL.updateById(userId, { superAdmin: true });
|
||||
};
|
||||
|
||||
const getAdminIntegrationsConfig = async () => {
|
||||
const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
|
||||
|
||||
if (!serverCfg) {
|
||||
throw new NotFoundError({ name: "AdminConfig", message: "Admin config not found" });
|
||||
}
|
||||
|
||||
const decrypt = kmsService.decryptWithRootKey();
|
||||
|
||||
const slackClientId = serverCfg.encryptedSlackClientId ? decrypt(serverCfg.encryptedSlackClientId).toString() : "";
|
||||
const slackClientSecret = serverCfg.encryptedSlackClientSecret
|
||||
? decrypt(serverCfg.encryptedSlackClientSecret).toString()
|
||||
: "";
|
||||
|
||||
const microsoftAppId = serverCfg.encryptedMicrosoftTeamsAppId
|
||||
? decrypt(serverCfg.encryptedMicrosoftTeamsAppId).toString()
|
||||
: "";
|
||||
const microsoftClientSecret = serverCfg.encryptedMicrosoftTeamsClientSecret
|
||||
? decrypt(serverCfg.encryptedMicrosoftTeamsClientSecret).toString()
|
||||
: "";
|
||||
const microsoftBotId = serverCfg.encryptedMicrosoftTeamsBotId
|
||||
? decrypt(serverCfg.encryptedMicrosoftTeamsBotId).toString()
|
||||
: "";
|
||||
|
||||
return {
|
||||
slack: {
|
||||
clientSecret: slackClientSecret,
|
||||
clientId: slackClientId
|
||||
},
|
||||
microsoftTeams: {
|
||||
appId: microsoftAppId,
|
||||
clientSecret: microsoftClientSecret,
|
||||
botId: microsoftBotId
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const getConfiguredEncryptionStrategies = async () => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
@@ -696,6 +801,19 @@ export const superAdminServiceFactory = ({
|
||||
return (await keyStore.getItem("invalidating-cache")) !== null;
|
||||
};
|
||||
|
||||
const initializeAdminIntegrationConfigSync = async () => {
|
||||
logger.info("Setting up background sync process for admin integrations config");
|
||||
|
||||
// initial sync upon startup
|
||||
await $syncAdminIntegrationConfig();
|
||||
|
||||
// sync admin integrations config every 5 minutes
|
||||
const job = new CronJob("*/5 * * * *", $syncAdminIntegrationConfig);
|
||||
job.start();
|
||||
|
||||
return job;
|
||||
};
|
||||
|
||||
return {
|
||||
initServerCfg,
|
||||
updateServerCfg,
|
||||
@@ -714,6 +832,7 @@ export const superAdminServiceFactory = ({
|
||||
checkIfInvalidatingCache,
|
||||
getOrganizations,
|
||||
deleteOrganization,
|
||||
deleteOrganizationMembership
|
||||
deleteOrganizationMembership,
|
||||
initializeAdminIntegrationConfigSync
|
||||
};
|
||||
};
|
||||
|
@@ -55,3 +55,22 @@ export enum CacheType {
|
||||
ALL = "all",
|
||||
SECRETS = "secrets"
|
||||
}
|
||||
|
||||
export type TAdminIntegrationConfig = {
|
||||
slack: {
|
||||
clientSecret: string;
|
||||
clientId: string;
|
||||
};
|
||||
microsoftTeams: {
|
||||
appId: string;
|
||||
clientSecret: string;
|
||||
botId: string;
|
||||
};
|
||||
gitHubAppConnection: {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
appSlug: string;
|
||||
appId: string;
|
||||
privateKey: string;
|
||||
};
|
||||
};
|
||||
|
@@ -374,7 +374,13 @@
|
||||
"internals/permissions/migration"
|
||||
]
|
||||
},
|
||||
"internals/components",
|
||||
{
|
||||
"group": "Architecture",
|
||||
"pages": [
|
||||
"internals/architecture/components",
|
||||
"internals/architecture/cloud"
|
||||
]
|
||||
},
|
||||
"internals/security",
|
||||
"internals/service-tokens"
|
||||
]
|
||||
@@ -2012,7 +2018,7 @@
|
||||
"tab": "SDKs",
|
||||
"groups": [
|
||||
{
|
||||
"group": "",
|
||||
"group": "Overview",
|
||||
"pages": ["sdks/overview"]
|
||||
},
|
||||
{
|
||||
@@ -2032,7 +2038,7 @@
|
||||
"tab": "Changelog",
|
||||
"groups": [
|
||||
{
|
||||
"group": "",
|
||||
"group": "Overview",
|
||||
"pages": ["changelog/overview"]
|
||||
}
|
||||
]
|
||||
|
Binary file not shown.
After Width: | Height: | Size: 610 KiB |
@@ -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.
|
||||

|
||||
|
||||
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:
|
||||

|
||||
|
||||
- **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_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_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>
|
||||
</Steps>
|
||||
</Accordion>
|
||||
@@ -158,4 +173,5 @@ Infisical supports two methods for connecting to GitHub.
|
||||
</Step>
|
||||
</Steps>
|
||||
</Tab>
|
||||
|
||||
</Tabs>
|
||||
|
128
docs/internals/architecture/cloud.mdx
Normal file
128
docs/internals/architecture/cloud.mdx
Normal file
@@ -0,0 +1,128 @@
|
||||
---
|
||||
title: "Infisical Cloud"
|
||||
description: "Architecture overview of Infisical's US and EU cloud deployments"
|
||||
---
|
||||
|
||||
This document provides an overview of Infisical's cloud architecture for our US and EU deployments, detailing the core components and how they interact to provide security and infrastructure services.
|
||||
|
||||
## Overview
|
||||
|
||||
Infisical Cloud operates on AWS infrastructure using containerized services deployed via Amazon ECS (Elastic Container Service). Our US and EU deployments use identical architectural patterns to ensure consistency and reliability across regions.
|
||||
|
||||

|
||||
|
||||
## Components
|
||||
|
||||
A typical Infisical Cloud deployment consists of the following components:
|
||||
|
||||
### Application Services
|
||||
|
||||
- **Infisical Core**: Main application server running the Infisical backend API
|
||||
- **License API**: Dedicated API service for license management with separate RDS instance (shared between US/EU)
|
||||
- **Application Load Balancer**: Routes incoming traffic to application containers with SSL termination and host-based routing
|
||||
|
||||
### Data Layer
|
||||
|
||||
- **Amazon RDS (PostgreSQL)**:
|
||||
- **Main RDS Instance**: Primary database for secrets, users, and metadata (Multi-AZ, encryption enabled)
|
||||
- **License API RDS Instance**: Dedicated database for license management services
|
||||
- **Amazon ElastiCache (Redis)**:
|
||||
- **Main Redis Cluster**: Multi-AZ replication group for core application caching and queuing
|
||||
- **License API Redis**: Dedicated cache for license services
|
||||
- Redis 7 engine with CloudWatch logging and snapshot backups
|
||||
|
||||
### Infrastructure
|
||||
|
||||
- **ECS Fargate**: Serverless container platform running application services
|
||||
- **AWS Global Accelerator**: Global traffic routing and performance optimization
|
||||
- **Cloudflare**: DNS management and routing
|
||||
- **AWS SSM Parameter Store**: Stores application configuration and secrets
|
||||
- **CloudWatch**: Centralized logging and monitoring
|
||||
|
||||
## System Layout
|
||||
|
||||
### Service Architecture
|
||||
|
||||
The Infisical application runs as multiple containerized services on ECS:
|
||||
|
||||
- **Main Server**: Auto-scaling containerized application services
|
||||
- **License API**: Dedicated service with separate infrastructure (shared globally)
|
||||
- **Monitoring**: AWS OTel Collector and Datadog Agent sidecars
|
||||
|
||||
Container images are pulled from Docker Hub and managed via GitHub Actions for deployments.
|
||||
|
||||
### Network Configuration
|
||||
|
||||
Services are deployed in private subnets with the following connectivity:
|
||||
|
||||
- External traffic → Application Load Balancer → ECS Services
|
||||
- Main server exposes port 8080
|
||||
- License API exposes port 4000 (portal.infisical.com, license.infisical.com)
|
||||
- Service-to-service communication via AWS Service Connect
|
||||
|
||||
### Data Flow
|
||||
|
||||
1. **DNS resolution** via Cloudflare routes traffic to AWS Global Accelerator
|
||||
2. **Global Accelerator** optimizes routing to the nearest AWS region
|
||||
3. **Client requests** are routed through the Application Load Balancer to ECS containers
|
||||
4. **Application logic** processes requests in the Infisical Core service
|
||||
5. **Data persistence** occurs via encrypted connections to RDS
|
||||
6. **Caching** utilizes ElastiCache for performance optimization
|
||||
7. **Configuration** is retrieved from AWS SSM Parameter Store
|
||||
|
||||
## Regional Deployments
|
||||
|
||||
Each region operates in a separate AWS account, providing strong isolation boundaries for security, compliance, and operational independence.
|
||||
|
||||
### US Cloud (us.infisical.com or app.infisical.com)
|
||||
|
||||
- **AWS Account**: Dedicated US AWS account
|
||||
- **Infrastructure**: ECS-based containerized deployment
|
||||
- **Monitoring**: Integrated with Datadog for observability and security monitoring
|
||||
|
||||
### EU Cloud (eu.infisical.com)
|
||||
|
||||
- **AWS Account**: Dedicated EU AWS account
|
||||
- **Infrastructure**: ECS-based containerized deployment
|
||||
- **Monitoring**: Integrated with Datadog for observability and security monitoring
|
||||
|
||||
## Configuration Management
|
||||
|
||||
Application configuration and secrets are managed through AWS SSM Parameter Store, with deployment automation handled via GitHub Actions.
|
||||
|
||||
## Monitoring and Observability
|
||||
|
||||
### Logging
|
||||
|
||||
- **CloudWatch**: 365-day retention for application logs
|
||||
- **Health Checks**: HTTP endpoint monitoring for service health
|
||||
|
||||
### Metrics
|
||||
|
||||
- **AWS OTel Collector**: Prometheus metrics collection
|
||||
- **Datadog Agent**: Application performance monitoring and infrastructure metrics
|
||||
|
||||
## Container Management
|
||||
|
||||
- **Images**: `infisical/staging_infisical` and `infisical/license-api` from Docker Hub
|
||||
- **Deployment**: Automated via GitHub Actions updating SSM parameter for image tags
|
||||
- **Registry Access**: Docker Hub credentials stored in AWS Secrets Manager
|
||||
- **Platform**: ECS Fargate serverless container platform
|
||||
|
||||
## Security Overview
|
||||
|
||||
### Data Protection
|
||||
|
||||
- **Encryption**: All secrets encrypted at rest and in transit
|
||||
- **Network Isolation**: Services deployed in private subnets with controlled access
|
||||
- **Authentication**: API tokens and service accounts for secure access
|
||||
- **Audit Logging**: Comprehensive audit trails for all secret operations
|
||||
|
||||
### Network Architecture
|
||||
|
||||
- **VPC Design**: Dedicated VPC with public and private subnets across multiple Availability Zones
|
||||
- **NAT Gateway**: Controlled outbound connectivity from private subnets
|
||||
- **Load Balancing**: Application Load Balancer with SSL termination and health checks
|
||||
- **Security Groups**: Restrictive firewall rules and controlled network access
|
||||
- **High Availability**: Multi-AZ deployment with automatic failover
|
||||
- **Network Monitoring**: VPC Flow Logs with 365-day retention for traffic analysis
|
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: "Infisical Java SDK"
|
||||
sidebarTitle: "Java"
|
||||
url: "https://github.com/Infisical/java-sdk?tab=readme-ov-file#infisical-nodejs-sdk"
|
||||
url: "https://github.com/Infisical/java-sdk?tab=readme-ov-file#infisical-java-sdk"
|
||||
icon: "java"
|
||||
---
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: "Infisical Node.js SDK"
|
||||
sidebarTitle: "Node.js"
|
||||
url: "https://github.com/Infisical/node-sdk-v2"
|
||||
url: "https://github.com/Infisical/node-sdk-v2?tab=readme-ov-file#infisical-nodejs-sdk"
|
||||
icon: "node"
|
||||
---
|
||||
|
||||
|
@@ -43,7 +43,7 @@ def hello_world():
|
||||
This example demonstrates how to use the Infisical Python SDK with a Flask application. The application retrieves a secret named "NAME" and responds to requests with a greeting that includes the secret value.
|
||||
|
||||
<Warning>
|
||||
We do not recommend hardcoding your [Machine Identity Tokens](/platform/identities/overview). Setting it as an environment variable would be best.
|
||||
We do not recommend hardcoding your [Machine Identity Tokens](/platform/identities/overview). Setting it as an environment variable would be best.
|
||||
</Warning>
|
||||
|
||||
## Installation
|
||||
@@ -314,32 +314,32 @@ By default, `getSecret()` fetches and returns a shared secret. If not found, it
|
||||
#### Parameters
|
||||
|
||||
<ParamField query="Parameters" type="object" optional>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="secret_name" type="string" required>
|
||||
The key of the secret to retrieve
|
||||
</ParamField>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="secret_name" type="string" required>
|
||||
The key of the secret to retrieve
|
||||
</ParamField>
|
||||
<ParamField query="include_imports" type="boolean">
|
||||
Whether or not to include imported secrets from the current path. Read about [secret import](/documentation/platform/secret-reference)
|
||||
</ParamField>
|
||||
<ParamField query="environment" type="string" required>
|
||||
The slug name (dev, prod, etc) of the environment from where secrets should be fetched from.
|
||||
</ParamField>
|
||||
<ParamField query="project_id" type="string" required>
|
||||
The project ID where the secret lives in.
|
||||
</ParamField>
|
||||
<ParamField query="path" type="string" optional>
|
||||
The path from where secret should be fetched from.
|
||||
</ParamField>
|
||||
<ParamField query="type" type="string" optional>
|
||||
The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "personal".
|
||||
</ParamField>
|
||||
<ParamField query="include_imports" type="boolean" default="false" optional>
|
||||
Whether or not to include imported secrets from the current path. Read about [secret import](/documentation/platform/secret-reference)
|
||||
</ParamField>
|
||||
<ParamField query="environment" type="string" required>
|
||||
The slug name (dev, prod, etc) of the environment from where secrets should be fetched from.
|
||||
</ParamField>
|
||||
<ParamField query="project_id" type="string" required>
|
||||
The project ID where the secret lives in.
|
||||
</ParamField>
|
||||
<ParamField query="path" type="string" optional>
|
||||
The path from where secret should be fetched from.
|
||||
</ParamField>
|
||||
<ParamField query="type" type="string" optional>
|
||||
The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "personal".
|
||||
</ParamField>
|
||||
<ParamField query="include_imports" type="boolean" default="false" optional>
|
||||
Whether or not to include imported secrets from the current path. Read about [secret import](/documentation/platform/secret-reference)
|
||||
</ParamField>
|
||||
<ParamField query="expand_secret_references" type="boolean" default="true" optional>
|
||||
Whether or not to expand secret references in the fetched secrets. Read about [secret reference](/documentation/platform/secret-reference)
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
</Expandable>
|
||||
</ParamField>
|
||||
|
||||
### client.createSecret(options)
|
||||
@@ -358,26 +358,26 @@ Create a new secret in Infisical.
|
||||
#### Parameters
|
||||
|
||||
<ParamField query="Parameters" type="object" optional>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="secret_name" type="string" required>
|
||||
The key of the secret to create.
|
||||
</ParamField>
|
||||
<ParamField query="secret_value" type="string" required>
|
||||
The value of the secret.
|
||||
</ParamField>
|
||||
<ParamField query="project_id" type="string" required>
|
||||
The project ID where the secret lives in.
|
||||
</ParamField>
|
||||
<ParamField query="environment" type="string" required>
|
||||
The slug name (dev, prod, etc) of the environment from where secrets should be fetched from.
|
||||
</ParamField>
|
||||
<ParamField query="path" type="string" optional>
|
||||
The path from where secret should be created.
|
||||
</ParamField>
|
||||
<ParamField query="type" type="string" optional>
|
||||
The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared".
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="secret_name" type="string" required>
|
||||
The key of the secret to create.
|
||||
</ParamField>
|
||||
<ParamField query="secret_value" type="string" required>
|
||||
The value of the secret.
|
||||
</ParamField>
|
||||
<ParamField query="project_id" type="string" required>
|
||||
The project ID where the secret lives in.
|
||||
</ParamField>
|
||||
<ParamField query="environment" type="string" required>
|
||||
The slug name (dev, prod, etc) of the environment from where secrets should be fetched from.
|
||||
</ParamField>
|
||||
<ParamField query="path" type="string" optional>
|
||||
The path from where secret should be created.
|
||||
</ParamField>
|
||||
<ParamField query="type" type="string" optional>
|
||||
The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared".
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
</ParamField>
|
||||
|
||||
### client.updateSecret(options)
|
||||
@@ -396,26 +396,26 @@ Update an existing secret in Infisical.
|
||||
#### Parameters
|
||||
|
||||
<ParamField query="Parameters" type="object" optional>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="secret_name" type="string" required>
|
||||
The key of the secret to update.
|
||||
</ParamField>
|
||||
<ParamField query="secret_value" type="string" required>
|
||||
The new value of the secret.
|
||||
</ParamField>
|
||||
<ParamField query="project_id" type="string" required>
|
||||
The project ID where the secret lives in.
|
||||
</ParamField>
|
||||
<ParamField query="environment" type="string" required>
|
||||
The slug name (dev, prod, etc) of the environment from where secrets should be fetched from.
|
||||
</ParamField>
|
||||
<ParamField query="path" type="string" optional>
|
||||
The path from where secret should be updated.
|
||||
</ParamField>
|
||||
<ParamField query="type" type="string" optional>
|
||||
The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared".
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="secret_name" type="string" required>
|
||||
The key of the secret to update.
|
||||
</ParamField>
|
||||
<ParamField query="secret_value" type="string" required>
|
||||
The new value of the secret.
|
||||
</ParamField>
|
||||
<ParamField query="project_id" type="string" required>
|
||||
The project ID where the secret lives in.
|
||||
</ParamField>
|
||||
<ParamField query="environment" type="string" required>
|
||||
The slug name (dev, prod, etc) of the environment from where secrets should be fetched from.
|
||||
</ParamField>
|
||||
<ParamField query="path" type="string" optional>
|
||||
The path from where secret should be updated.
|
||||
</ParamField>
|
||||
<ParamField query="type" type="string" optional>
|
||||
The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared".
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
</ParamField>
|
||||
|
||||
### client.deleteSecret(options)
|
||||
@@ -433,23 +433,23 @@ Delete a secret in Infisical.
|
||||
#### Parameters
|
||||
|
||||
<ParamField query="Parameters" type="object" optional>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="secret_name" type="string">
|
||||
The key of the secret to update.
|
||||
</ParamField>
|
||||
<ParamField query="project_id" type="string" required>
|
||||
The project ID where the secret lives in.
|
||||
</ParamField>
|
||||
<ParamField query="environment" type="string" required>
|
||||
The slug name (dev, prod, etc) of the environment from where secrets should be fetched from.
|
||||
</ParamField>
|
||||
<ParamField query="path" type="string" optional>
|
||||
The path from where secret should be deleted.
|
||||
</ParamField>
|
||||
<ParamField query="type" type="string" optional>
|
||||
The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared".
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="secret_name" type="string">
|
||||
The key of the secret to update.
|
||||
</ParamField>
|
||||
<ParamField query="project_id" type="string" required>
|
||||
The project ID where the secret lives in.
|
||||
</ParamField>
|
||||
<ParamField query="environment" type="string" required>
|
||||
The slug name (dev, prod, etc) of the environment from where secrets should be fetched from.
|
||||
</ParamField>
|
||||
<ParamField query="path" type="string" optional>
|
||||
The path from where secret should be deleted.
|
||||
</ParamField>
|
||||
<ParamField query="type" type="string" optional>
|
||||
The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared".
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
</ParamField>
|
||||
|
||||
## Cryptography
|
||||
@@ -480,14 +480,14 @@ encryptedData = client.encryptSymmetric(encryptOptions)
|
||||
#### Parameters
|
||||
|
||||
<ParamField query="Parameters" type="object" required>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="plaintext" type="string">
|
||||
The plaintext you want to encrypt.
|
||||
</ParamField>
|
||||
<ParamField query="key" type="string" required>
|
||||
The symmetric key to use for encryption.
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="plaintext" type="string">
|
||||
The plaintext you want to encrypt.
|
||||
</ParamField>
|
||||
<ParamField query="key" type="string" required>
|
||||
The symmetric key to use for encryption.
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
</ParamField>
|
||||
|
||||
#### Returns (object)
|
||||
@@ -512,20 +512,20 @@ decryptedString = client.decryptSymmetric(decryptOptions)
|
||||
#### Parameters
|
||||
|
||||
<ParamField query="Parameters" type="object" required>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="ciphertext" type="string">
|
||||
The ciphertext you want to decrypt.
|
||||
</ParamField>
|
||||
<ParamField query="key" type="string" required>
|
||||
The symmetric key to use for encryption.
|
||||
</ParamField>
|
||||
<ParamField query="iv" type="string" required>
|
||||
The initialization vector to use for decryption.
|
||||
</ParamField>
|
||||
<ParamField query="tag" type="string" required>
|
||||
The authentication tag to use for decryption.
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="ciphertext" type="string">
|
||||
The ciphertext you want to decrypt.
|
||||
</ParamField>
|
||||
<ParamField query="key" type="string" required>
|
||||
The symmetric key to use for encryption.
|
||||
</ParamField>
|
||||
<ParamField query="iv" type="string" required>
|
||||
The initialization vector to use for decryption.
|
||||
</ParamField>
|
||||
<ParamField query="tag" type="string" required>
|
||||
The authentication tag to use for decryption.
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
</ParamField>
|
||||
|
||||
#### Returns (string)
|
||||
|
@@ -10,24 +10,23 @@ From local development to production, Infisical SDKs provide the easiest way for
|
||||
- Fetch secrets on demand
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Node" href="https://github.com/Infisical/node-sdk-v2" icon="node" color="#68a063">
|
||||
Manage secrets for your Node application on demand
|
||||
<Card title="Node.js" href="https://github.com/Infisical/node-sdk-v2?tab=readme-ov-file#infisical-nodejs-sdk" icon="node" color="#68a063">
|
||||
Manage secrets for your Node application on demand
|
||||
</Card>
|
||||
<Card href="https://github.com/Infisical/python-sdk-official" title="Python" icon="python" color="#4c8abe">
|
||||
Manage secrets for your Python application on demand
|
||||
<Card href="https://github.com/Infisical/python-sdk-official?tab=readme-ov-file#infisical-python-sdk" title="Python" icon="python" color="#4c8abe">
|
||||
Manage secrets for your Python application on demand
|
||||
</Card>
|
||||
<Card href="https://github.com/Infisical/java-sdk?tab=readme-ov-file#infisical-nodejs-sdk" title="Java" icon="java" color="#e41f23">
|
||||
Manage secrets for your Java application on demand
|
||||
<Card href="https://github.com/Infisical/java-sdk?tab=readme-ov-file#infisical-java-sdk" title="Java" icon="java" color="#e41f23">
|
||||
Manage secrets for your Java application on demand
|
||||
</Card>
|
||||
<Card href="/sdks/languages/go" title="Go" icon="golang" color="#367B99">
|
||||
Manage secrets for your Go application on demand
|
||||
Manage secrets for your Go application on demand
|
||||
</Card>
|
||||
<Card href="/sdks/languages/csharp" title="C#" icon="bars" color="#368833">
|
||||
Manage secrets for your C#/.NET application on demand
|
||||
<Card href="https://github.com/Infisical/infisical-dotnet-sdk?tab=readme-ov-file#infisical-net-sdk" title=".NET" icon="bars" color="#368833">
|
||||
Manage secrets for your .NET application on demand
|
||||
</Card>
|
||||
|
||||
<Card href="/sdks/languages/ruby" title="Ruby" icon="diamond" color="#367B99">
|
||||
Manage secrets for your Ruby application on demand
|
||||
<Card href="/sdks/languages/ruby" title="Ruby" icon="diamond" color="#367B99">
|
||||
Manage secrets for your Ruby application on demand
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
|
@@ -16,6 +16,12 @@ export const ROUTE_PATHS = Object.freeze({
|
||||
PasswordResetPage: setRoute("/password-reset", "/_restrict-login-signup/password-reset"),
|
||||
PasswordSetupPage: setRoute("/password-setup", "/_authenticate/password-setup")
|
||||
},
|
||||
Admin: {
|
||||
IntegrationsPage: setRoute(
|
||||
"/admin/integrations",
|
||||
"/_authenticate/_inject-org-details/admin/_admin-layout/integrations"
|
||||
)
|
||||
},
|
||||
Organization: {
|
||||
Settings: {
|
||||
OauthCallbackPage: setRoute(
|
||||
|
@@ -56,6 +56,11 @@ export type TUpdateServerConfigDTO = {
|
||||
microsoftTeamsAppId?: string;
|
||||
microsoftTeamsClientSecret?: string;
|
||||
microsoftTeamsBotId?: string;
|
||||
gitHubAppConnectionClientId?: string;
|
||||
gitHubAppConnectionClientSecret?: string;
|
||||
gitHubAppConnectionSlug?: string;
|
||||
gitHubAppConnectionId?: string;
|
||||
gitHubAppConnectionPrivateKey?: string;
|
||||
} & Partial<TServerConfig>;
|
||||
|
||||
export type TCreateAdminUserDTO = {
|
||||
@@ -100,6 +105,13 @@ export type AdminIntegrationsConfig = {
|
||||
clientSecret: string;
|
||||
botId: string;
|
||||
};
|
||||
gitHubAppConnection: {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
appSlug: string;
|
||||
appId: string;
|
||||
privateKey: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type TGetServerRootKmsEncryptionDetails = {
|
||||
|
@@ -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's settings page.
|
||||
</div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="clientId"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Client ID"
|
||||
className="w-96"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value || ""}
|
||||
type="text"
|
||||
onChange={(e) => field.onChange(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="clientSecret"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Client Secret"
|
||||
tooltipText="You can find your Client Secret in the GitHub App's settings under 'Client secrets'."
|
||||
className="w-96"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value || ""}
|
||||
type={isGitHubAppClientSecretFocused ? "text" : "password"}
|
||||
onFocus={() => setIsGitHubAppClientSecretFocused.on()}
|
||||
onBlur={() => setIsGitHubAppClientSecretFocused.off()}
|
||||
onChange={(e) => field.onChange(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="appSlug"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="App Slug"
|
||||
tooltipText="The GitHub App slug from the app's URL (e.g., 'my-app' from github.com/apps/my-app)."
|
||||
className="w-96"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value || ""}
|
||||
type="text"
|
||||
onChange={(e) => field.onChange(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="appId"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="App ID"
|
||||
tooltipText="The numeric App ID found in your GitHub App's settings."
|
||||
className="w-96"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value || ""}
|
||||
type="text"
|
||||
onChange={(e) => field.onChange(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="privateKey"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Private Key"
|
||||
tooltipText="The private key generated for your GitHub App (PEM format)."
|
||||
className="w-96"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<TextArea
|
||||
{...field}
|
||||
value={field.value || ""}
|
||||
className="min-h-32"
|
||||
onChange={(e) => field.onChange(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div>
|
||||
<Button
|
||||
className="mt-2"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting || !isDirty}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</form>
|
||||
);
|
||||
};
|
@@ -1,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 { SlackIntegrationForm } from "./SlackIntegrationForm";
|
||||
|
||||
enum IntegrationTabSections {
|
||||
Workflow = "workflow",
|
||||
AppConnections = "app-connections"
|
||||
}
|
||||
|
||||
interface WorkflowTabProps {
|
||||
adminIntegrationsConfig: AdminIntegrationsConfig;
|
||||
}
|
||||
|
||||
interface AppConnectionsTabProps {
|
||||
adminIntegrationsConfig: AdminIntegrationsConfig;
|
||||
}
|
||||
|
||||
const WorkflowTab = ({ adminIntegrationsConfig }: WorkflowTabProps) => (
|
||||
<div className="flex flex-col gap-2">
|
||||
<SlackIntegrationForm adminIntegrationsConfig={adminIntegrationsConfig} />
|
||||
<MicrosoftTeamsIntegrationForm adminIntegrationsConfig={adminIntegrationsConfig} />
|
||||
</div>
|
||||
);
|
||||
|
||||
const AppConnectionsTab = ({ adminIntegrationsConfig }: AppConnectionsTabProps) => (
|
||||
<div className="flex flex-col gap-2">
|
||||
<GitHubAppConnectionForm adminIntegrationsConfig={adminIntegrationsConfig} />
|
||||
</div>
|
||||
);
|
||||
|
||||
export const IntegrationsPageForm = () => {
|
||||
const { data: adminIntegrationsConfig } = useGetAdminIntegrationsConfig();
|
||||
|
||||
const navigate = useNavigate({
|
||||
from: ROUTE_PATHS.Admin.IntegrationsPage.path
|
||||
});
|
||||
|
||||
const selectedTab = useSearch({
|
||||
from: ROUTE_PATHS.Admin.IntegrationsPage.id,
|
||||
select: (el: { selectedTab?: string }) => el.selectedTab || IntegrationTabSections.Workflow,
|
||||
structuralSharing: true
|
||||
});
|
||||
|
||||
const updateSelectedTab = (tab: string) => {
|
||||
navigate({
|
||||
search: { selectedTab: tab }
|
||||
});
|
||||
};
|
||||
|
||||
const tabSections = [
|
||||
{
|
||||
key: IntegrationTabSections.Workflow,
|
||||
label: "Workflows",
|
||||
component: WorkflowTab
|
||||
},
|
||||
{
|
||||
key: IntegrationTabSections.AppConnections,
|
||||
label: "App Connections",
|
||||
component: AppConnectionsTab
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="mb-6 min-h-64 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4">
|
||||
<div className="text-xl font-semibold text-mineshaft-100">Integrations</div>
|
||||
<div className="text-sm text-mineshaft-300">
|
||||
Configure your instance-wide settings to enable integration with Slack and Microsoft
|
||||
Teams.
|
||||
Configure your instance-wide settings to enable integration with third-party services.
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<SlackIntegrationForm adminIntegrationsConfig={adminIntegrationsConfig} />
|
||||
<MicrosoftTeamsIntegrationForm adminIntegrationsConfig={adminIntegrationsConfig} />
|
||||
</div>
|
||||
<Tabs value={selectedTab} onValueChange={updateSelectedTab}>
|
||||
<TabList>
|
||||
{tabSections.map((section) => (
|
||||
<Tab value={section.key} key={`integration-tab-${section.key}`}>
|
||||
{section.label}
|
||||
</Tab>
|
||||
))}
|
||||
</TabList>
|
||||
{tabSections.map(({ key, component: Component }) => (
|
||||
<TabPanel value={key} key={`integration-tab-panel-${key}`}>
|
||||
<Component adminIntegrationsConfig={adminIntegrationsConfig!} />
|
||||
</TabPanel>
|
||||
))}
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@@ -75,7 +75,7 @@ export const MicrosoftTeamsIntegrationForm = ({ adminIntegrationsConfig }: Props
|
||||
<AccordionTrigger className="flex h-fit w-full justify-start rounded-md border border-mineshaft-500 bg-mineshaft-700 px-4 py-6 text-sm transition-colors data-[state=open]:rounded-b-none">
|
||||
<div className="text-md group order-1 ml-3 flex items-center gap-2">
|
||||
<BsMicrosoftTeams className="text-lg group-hover:text-primary-400" />
|
||||
<div className="text-[15px] font-semibold">Microsoft Teams Integration</div>
|
||||
<div className="text-[15px] font-semibold">Microsoft Teams</div>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent childrenClassName="px-0 py-0">
|
||||
|
@@ -108,7 +108,7 @@ export const SlackIntegrationForm = ({ adminIntegrationsConfig }: Props) => {
|
||||
<AccordionTrigger className="flex h-fit w-full justify-start rounded-md border border-mineshaft-500 bg-mineshaft-700 px-4 py-6 text-sm transition-colors data-[state=open]:rounded-b-none">
|
||||
<div className="text-md group order-1 ml-3 flex items-center gap-2">
|
||||
<BsSlack className="text-lg group-hover:text-primary-400" />
|
||||
<div className="text-[15px] font-semibold">Slack Integration</div>
|
||||
<div className="text-[15px] font-semibold">Slack</div>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent childrenClassName="px-0 py-0">
|
||||
|
@@ -111,10 +111,10 @@ export const GitHubConnectionForm = ({ appConnection }: Props) => {
|
||||
}. This field cannot be changed after creation.`}
|
||||
errorText={
|
||||
!isLoading && isMissingConfig
|
||||
? `Environment variables have not been configured. ${
|
||||
? `Credentials have not been configured. ${
|
||||
isInfisicalCloud()
|
||||
? "Please contact Infisical."
|
||||
: `See Docs to configure GitHub ${methodDetails.name} Connections.`
|
||||
: `See Docs to configure Github ${methodDetails.name} Connections.`
|
||||
}`
|
||||
: error?.message
|
||||
}
|
||||
|
@@ -19,41 +19,38 @@ import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
const taxIDTypes = [
|
||||
{ label: "Australia ABN", value: "au_abn" },
|
||||
{ label: "Australia ARN", value: "au_arn" },
|
||||
{ label: "Bulgaria UIC", value: "bg_uic" },
|
||||
{ label: "Brazil CNPJ", value: "br_cnpj" },
|
||||
{ label: "Brazil CPF", value: "br_cpf" },
|
||||
{ label: "Bulgaria UIC", value: "bg_uic" },
|
||||
{ label: "Canada BN", value: "ca_bn" },
|
||||
{ label: "Canada GST/HST", value: "ca_gst_hst" },
|
||||
{ label: "Canada PST BC", value: "ca_pst_bc" },
|
||||
{ label: "Canada PST MB", value: "ca_pst_mb" },
|
||||
{ label: "Canada PST SK", value: "ca_pst_sk" },
|
||||
{ label: "Canada QST", value: "ca_qst" },
|
||||
{ label: "Switzerland VAT", value: "ch_vat" },
|
||||
{ label: "Chile TIN", value: "cl_tin" },
|
||||
{ label: "Egypt TIN", value: "eg_tin" },
|
||||
{ label: "Spain CIF", value: "es_cif" },
|
||||
{ label: "EU OSS VAT", value: "eu_oss_vat" },
|
||||
{ label: "EU VAT", value: "eu_vat" },
|
||||
{ label: "GB VAT", value: "gb_vat" },
|
||||
{ label: "Georgia VAT", value: "ge_vat" },
|
||||
{ label: "Hong Kong BR", value: "hk_br" },
|
||||
{ label: "Hungary TIN", value: "hu_tin" },
|
||||
{ label: "Iceland VAT", value: "is_vat" },
|
||||
{ label: "India GST", value: "in_gst" },
|
||||
{ label: "Indonesia NPWP", value: "id_npwp" },
|
||||
{ label: "Israel VAT", value: "il_vat" },
|
||||
{ label: "India GST", value: "in_gst" },
|
||||
{ label: "Iceland VAT", value: "is_vat" },
|
||||
{ label: "Japan CN", value: "jp_cn" },
|
||||
{ label: "Japan RN", value: "jp_rn" },
|
||||
{ label: "Japan TRN", value: "jp_trn" },
|
||||
{ label: "Kenya PIN", value: "ke_pin" },
|
||||
{ label: "South Korea BRN", value: "kr_brn" },
|
||||
{ label: "Liechtenstein UID", value: "li_uid" },
|
||||
{ label: "Mexico RFC", value: "mx_rfc" },
|
||||
{ label: "Malaysia FRP", value: "my_frp" },
|
||||
{ label: "Malaysia ITN", value: "my_itn" },
|
||||
{ label: "Malaysia SST", value: "my_sst" },
|
||||
{ label: "Norway VAT", value: "no_vat" },
|
||||
{ label: "Mexico RFC", value: "mx_rfc" },
|
||||
{ label: "New Zealand GST", value: "nz_gst" },
|
||||
{ label: "Norway VAT", value: "no_vat" },
|
||||
{ label: "Philippines TIN", value: "ph_tin" },
|
||||
{ label: "Russia INN", value: "ru_inn" },
|
||||
{ label: "Russia KPP", value: "ru_kpp" },
|
||||
@@ -61,12 +58,15 @@ const taxIDTypes = [
|
||||
{ label: "Singapore GST", value: "sg_gst" },
|
||||
{ label: "Singapore UEN", value: "sg_uen" },
|
||||
{ label: "Slovenia TIN", value: "si_tin" },
|
||||
{ label: "South Africa VAT", value: "za_vat" },
|
||||
{ label: "South Korea BRN", value: "kr_brn" },
|
||||
{ label: "Spain CIF", value: "es_cif" },
|
||||
{ label: "Switzerland VAT", value: "ch_vat" },
|
||||
{ label: "Taiwan VAT", value: "tw_vat" },
|
||||
{ label: "Thailand VAT", value: "th_vat" },
|
||||
{ label: "Turkey TIN", value: "tr_tin" },
|
||||
{ label: "Taiwan VAT", value: "tw_vat" },
|
||||
{ label: "Ukraine VAT", value: "ua_vat" },
|
||||
{ label: "US EIN", value: "us_ein" },
|
||||
{ label: "South Africa VAT", value: "za_vat" }
|
||||
{ label: "Ukraine VAT", value: "ua_vat" }
|
||||
];
|
||||
|
||||
const schema = z
|
||||
|
Reference in New Issue
Block a user