mirror of
https://github.com/Infisical/infisical.git
synced 2025-08-24 20:43:19 +00:00
Compare commits
118 Commits
revise-com
...
sid/merge-
Author | SHA1 | Date | |
---|---|---|---|
|
25fc1ac06e | ||
|
de39548d6b | ||
|
81756bb37a | ||
|
5c632db282 | ||
|
461deef0d5 | ||
|
7748e03612 | ||
|
2389c64e69 | ||
|
de5ad47f77 | ||
|
e0161cd06f | ||
|
7c12fa3a4c | ||
|
0af53e82da | ||
|
f0c080187e | ||
|
47118bcf19 | ||
|
bb1975491f | ||
|
28cc919ff7 | ||
|
5c21ac3182 | ||
|
6204b181e7 | ||
|
06de9d06c9 | ||
|
3cceec86c8 | ||
|
ff043f990f | ||
|
9e177c1e45 | ||
|
5aeb823c9e | ||
|
ef6f79f7a6 | ||
|
43752e1888 | ||
|
d587e779f5 | ||
|
f9a9565630 | ||
|
05ba0abadd | ||
|
fff9a96204 | ||
|
bd72129d8c | ||
|
bf10b2f58a | ||
|
d24f5a57a8 | ||
|
166104e523 | ||
|
a7847f177c | ||
|
48e5f550e9 | ||
|
4a4a7fd325 | ||
|
91b8ed8015 | ||
|
6cf978b593 | ||
|
68fbb399fc | ||
|
97366f6e95 | ||
|
c83d4af7a3 | ||
|
f78556c85f | ||
|
13aa380cac | ||
|
c35c937c63 | ||
|
b10752acb5 | ||
|
eb9b75d930 | ||
|
f2a9a57c95 | ||
|
6384fa6dba | ||
|
c34ec8de09 | ||
|
ef8a7f1233 | ||
|
273a7b9657 | ||
|
a3b6fa9a53 | ||
|
f60dd528e8 | ||
|
8ffef1da8e | ||
|
f352f98374 | ||
|
91a76f50ca | ||
|
ea4bb0a062 | ||
|
3d6be7b1b2 | ||
|
09db98db50 | ||
|
a37f1eb1f8 | ||
|
2113abcfdc | ||
|
ea2707651c | ||
|
12558e8614 | ||
|
b986ff9a21 | ||
|
106833328b | ||
|
987f87e562 | ||
|
4d06d5cbb0 | ||
|
bad934de48 | ||
|
90b93fbd15 | ||
|
c2db2a0bc7 | ||
|
b0d24de008 | ||
|
0473fb0ddb | ||
|
4ccb5dc9b0 | ||
|
930425d5dc | ||
|
f77a53bd8e | ||
|
4bd61e5607 | ||
|
aa4dbfa073 | ||
|
b479406ba0 | ||
|
7cf9d933da | ||
|
ca2825ba95 | ||
|
b8fa4d5255 | ||
|
0d3cb2d41a | ||
|
e0d19d7b65 | ||
|
f5a0d8be78 | ||
|
c7ae7be493 | ||
|
18881749fd | ||
|
fa54c406dc | ||
|
1a2eef3ba6 | ||
|
0c562150f5 | ||
|
6fde132804 | ||
|
799721782a | ||
|
86d430f911 | ||
|
3cec1b4021 | ||
|
97b2c534a7 | ||
|
d71362ccc3 | ||
|
e4d90eb055 | ||
|
55607a4886 | ||
|
97dac1da94 | ||
|
f9f989c8af | ||
|
385c75c543 | ||
|
458dcd31c1 | ||
|
352ef050c3 | ||
|
b6b9fb6ef5 | ||
|
02ee418763 | ||
|
faca20c00c | ||
|
69c3687add | ||
|
1645534b54 | ||
|
dca0b0c614 | ||
|
d3d0d44778 | ||
|
67abcbfe7a | ||
|
fc772e6b89 | ||
|
c8108ff49a | ||
|
806165b9e9 | ||
|
9fde0a5787 | ||
|
9ee2581659 | ||
|
2deff0ef55 | ||
|
4312378589 | ||
|
d749a9621f | ||
|
9686d14e7f |
1
backend/src/@types/fastify.d.ts
vendored
1
backend/src/@types/fastify.d.ts
vendored
@@ -148,6 +148,7 @@ declare module "fastify" {
|
||||
interface Session {
|
||||
callbackPort: string;
|
||||
isAdminLogin: boolean;
|
||||
orgSlug?: string;
|
||||
}
|
||||
|
||||
interface FastifyRequest {
|
||||
|
@@ -84,6 +84,9 @@ const up = async (knex: Knex): Promise<void> => {
|
||||
t.index("expiresAt");
|
||||
t.index("orgId");
|
||||
t.index("projectId");
|
||||
t.index("eventType");
|
||||
t.index("userAgentType");
|
||||
t.index("actor");
|
||||
});
|
||||
|
||||
console.log("Adding GIN indices...");
|
||||
@@ -119,8 +122,8 @@ const up = async (knex: Knex): Promise<void> => {
|
||||
console.log("Creating audit log partitions ahead of time... next date:", nextDateStr);
|
||||
await createAuditLogPartition(knex, nextDate, new Date(nextDate.getFullYear(), nextDate.getMonth() + 1));
|
||||
|
||||
// create partitions 4 years ahead
|
||||
const partitionMonths = 4 * 12;
|
||||
// create partitions 20 years ahead
|
||||
const partitionMonths = 20 * 12;
|
||||
const partitionPromises: Promise<void>[] = [];
|
||||
for (let x = 1; x <= partitionMonths; x += 1) {
|
||||
partitionPromises.push(
|
||||
|
@@ -0,0 +1,38 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "@app/db/schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasEditNoteCol = await knex.schema.hasColumn(TableName.AccessApprovalRequest, "editNote");
|
||||
const hasEditedByUserId = await knex.schema.hasColumn(TableName.AccessApprovalRequest, "editedByUserId");
|
||||
|
||||
if (!hasEditNoteCol || !hasEditedByUserId) {
|
||||
await knex.schema.alterTable(TableName.AccessApprovalRequest, (t) => {
|
||||
if (!hasEditedByUserId) {
|
||||
t.uuid("editedByUserId").nullable();
|
||||
t.foreign("editedByUserId").references("id").inTable(TableName.Users).onDelete("SET NULL");
|
||||
}
|
||||
|
||||
if (!hasEditNoteCol) {
|
||||
t.string("editNote").nullable();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasEditNoteCol = await knex.schema.hasColumn(TableName.AccessApprovalRequest, "editNote");
|
||||
const hasEditedByUserId = await knex.schema.hasColumn(TableName.AccessApprovalRequest, "editedByUserId");
|
||||
|
||||
if (hasEditNoteCol || hasEditedByUserId) {
|
||||
await knex.schema.alterTable(TableName.AccessApprovalRequest, (t) => {
|
||||
if (hasEditedByUserId) {
|
||||
t.dropColumn("editedByUserId");
|
||||
}
|
||||
|
||||
if (hasEditNoteCol) {
|
||||
t.dropColumn("editNote");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@@ -0,0 +1,39 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
const GOOGLE_SSO_AUTH_ENFORCED_COLUMN_NAME = "googleSsoAuthEnforced";
|
||||
const GOOGLE_SSO_AUTH_LAST_USED_COLUMN_NAME = "googleSsoAuthLastUsed";
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasGoogleSsoAuthEnforcedColumn = await knex.schema.hasColumn(
|
||||
TableName.Organization,
|
||||
GOOGLE_SSO_AUTH_ENFORCED_COLUMN_NAME
|
||||
);
|
||||
const hasGoogleSsoAuthLastUsedColumn = await knex.schema.hasColumn(
|
||||
TableName.Organization,
|
||||
GOOGLE_SSO_AUTH_LAST_USED_COLUMN_NAME
|
||||
);
|
||||
|
||||
await knex.schema.alterTable(TableName.Organization, (table) => {
|
||||
if (!hasGoogleSsoAuthEnforcedColumn)
|
||||
table.boolean(GOOGLE_SSO_AUTH_ENFORCED_COLUMN_NAME).defaultTo(false).notNullable();
|
||||
if (!hasGoogleSsoAuthLastUsedColumn) table.timestamp(GOOGLE_SSO_AUTH_LAST_USED_COLUMN_NAME).nullable();
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasGoogleSsoAuthEnforcedColumn = await knex.schema.hasColumn(
|
||||
TableName.Organization,
|
||||
GOOGLE_SSO_AUTH_ENFORCED_COLUMN_NAME
|
||||
);
|
||||
|
||||
const hasGoogleSsoAuthLastUsedColumn = await knex.schema.hasColumn(
|
||||
TableName.Organization,
|
||||
GOOGLE_SSO_AUTH_LAST_USED_COLUMN_NAME
|
||||
);
|
||||
|
||||
await knex.schema.alterTable(TableName.Organization, (table) => {
|
||||
if (hasGoogleSsoAuthEnforcedColumn) table.dropColumn(GOOGLE_SSO_AUTH_ENFORCED_COLUMN_NAME);
|
||||
if (hasGoogleSsoAuthLastUsedColumn) table.dropColumn(GOOGLE_SSO_AUTH_LAST_USED_COLUMN_NAME);
|
||||
});
|
||||
}
|
@@ -20,7 +20,9 @@ export const AccessApprovalRequestsSchema = z.object({
|
||||
requestedByUserId: z.string().uuid(),
|
||||
note: z.string().nullable().optional(),
|
||||
privilegeDeletedAt: z.date().nullable().optional(),
|
||||
status: z.string().default("pending")
|
||||
status: z.string().default("pending"),
|
||||
editedByUserId: z.string().uuid().nullable().optional(),
|
||||
editNote: z.string().nullable().optional()
|
||||
});
|
||||
|
||||
export type TAccessApprovalRequests = z.infer<typeof AccessApprovalRequestsSchema>;
|
||||
|
@@ -36,7 +36,9 @@ export const OrganizationsSchema = z.object({
|
||||
scannerProductEnabled: z.boolean().default(true).nullable().optional(),
|
||||
shareSecretsProductEnabled: z.boolean().default(true).nullable().optional(),
|
||||
maxSharedSecretLifetime: z.number().default(2592000).nullable().optional(),
|
||||
maxSharedSecretViewLimit: z.number().nullable().optional()
|
||||
maxSharedSecretViewLimit: z.number().nullable().optional(),
|
||||
googleSsoAuthEnforced: z.boolean().default(false),
|
||||
googleSsoAuthLastUsed: z.date().nullable().optional()
|
||||
});
|
||||
|
||||
export type TOrganizations = z.infer<typeof OrganizationsSchema>;
|
||||
|
@@ -2,6 +2,7 @@ import { z } from "zod";
|
||||
|
||||
import { AccessApprovalRequestsReviewersSchema, AccessApprovalRequestsSchema, UsersSchema } from "@app/db/schemas";
|
||||
import { ApprovalStatus } from "@app/ee/services/access-approval-request/access-approval-request-types";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
@@ -26,7 +27,23 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
|
||||
body: z.object({
|
||||
permissions: z.any().array(),
|
||||
isTemporary: z.boolean(),
|
||||
temporaryRange: z.string().optional(),
|
||||
temporaryRange: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((val, ctx) => {
|
||||
if (!val || val === "permanent") return undefined;
|
||||
|
||||
const parsedMs = ms(val);
|
||||
|
||||
if (typeof parsedMs !== "number" || parsedMs <= 0) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Invalid time period format or value. Must be a positive duration (e.g., '1h', '30m', '2d')."
|
||||
});
|
||||
return z.NEVER;
|
||||
}
|
||||
return val;
|
||||
}),
|
||||
note: z.string().max(255).optional()
|
||||
}),
|
||||
querystring: z.object({
|
||||
@@ -190,4 +207,47 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
|
||||
return { review };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:requestId",
|
||||
method: "PATCH",
|
||||
schema: {
|
||||
params: z.object({
|
||||
requestId: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
temporaryRange: z.string().transform((val, ctx) => {
|
||||
const parsedMs = ms(val);
|
||||
|
||||
if (typeof parsedMs !== "number" || parsedMs <= 0) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Invalid time period format or value. Must be a positive duration (e.g., '1h', '30m', '2d')."
|
||||
});
|
||||
return z.NEVER;
|
||||
}
|
||||
return val;
|
||||
}),
|
||||
editNote: z.string().max(255)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
approval: AccessApprovalRequestsSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { request } = await server.services.accessApprovalRequest.updateAccessApprovalRequest({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
temporaryRange: req.body.temporaryRange,
|
||||
editNote: req.body.editNote,
|
||||
requestId: req.params.requestId
|
||||
});
|
||||
return { approval: request };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@@ -54,7 +54,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
|
||||
accessApprovalPolicyDAL: Pick<TAccessApprovalPolicyDALFactory, "findOne" | "find" | "findLastValidPolicy">;
|
||||
accessApprovalRequestReviewerDAL: Pick<
|
||||
TAccessApprovalRequestReviewerDALFactory,
|
||||
"create" | "find" | "findOne" | "transaction"
|
||||
"create" | "find" | "findOne" | "transaction" | "delete"
|
||||
>;
|
||||
groupDAL: Pick<TGroupDALFactory, "findAllGroupPossibleMembers">;
|
||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findById">;
|
||||
@@ -301,6 +301,155 @@ export const accessApprovalRequestServiceFactory = ({
|
||||
return { request: approval };
|
||||
};
|
||||
|
||||
const updateAccessApprovalRequest: TAccessApprovalRequestServiceFactory["updateAccessApprovalRequest"] = async ({
|
||||
temporaryRange,
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
editNote,
|
||||
requestId
|
||||
}) => {
|
||||
const cfg = getConfig();
|
||||
|
||||
const accessApprovalRequest = await accessApprovalRequestDAL.findById(requestId);
|
||||
if (!accessApprovalRequest) {
|
||||
throw new NotFoundError({ message: `Access request with ID '${requestId}' not found` });
|
||||
}
|
||||
|
||||
const { policy, requestedByUser } = accessApprovalRequest;
|
||||
if (policy.deletedAt) {
|
||||
throw new BadRequestError({
|
||||
message: "The policy associated with this access request has been deleted."
|
||||
});
|
||||
}
|
||||
|
||||
const { membership, hasRole } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: accessApprovalRequest.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SecretManager
|
||||
});
|
||||
|
||||
if (!membership) {
|
||||
throw new ForbiddenRequestError({ message: "You are not a member of this project" });
|
||||
}
|
||||
|
||||
const isApprover = policy.approvers.find((approver) => approver.userId === actorId);
|
||||
|
||||
if (!hasRole(ProjectMembershipRole.Admin) && !isApprover) {
|
||||
throw new ForbiddenRequestError({ message: "You are not authorized to modify this request" });
|
||||
}
|
||||
|
||||
const project = await projectDAL.findById(accessApprovalRequest.projectId);
|
||||
|
||||
if (!project) {
|
||||
throw new NotFoundError({
|
||||
message: `The project associated with this access request was not found. [projectId=${accessApprovalRequest.projectId}]`
|
||||
});
|
||||
}
|
||||
|
||||
if (accessApprovalRequest.status !== ApprovalStatus.PENDING) {
|
||||
throw new BadRequestError({ message: "The request has been closed" });
|
||||
}
|
||||
|
||||
const editedByUser = await userDAL.findById(actorId);
|
||||
|
||||
if (!editedByUser) throw new NotFoundError({ message: "Editing user not found" });
|
||||
|
||||
if (accessApprovalRequest.isTemporary && accessApprovalRequest.temporaryRange) {
|
||||
if (ms(temporaryRange) > ms(accessApprovalRequest.temporaryRange)) {
|
||||
throw new BadRequestError({ message: "Updated access duration must be less than current access duration" });
|
||||
}
|
||||
}
|
||||
|
||||
const { envSlug, secretPath, accessTypes } = verifyRequestedPermissions({
|
||||
permissions: accessApprovalRequest.permissions
|
||||
});
|
||||
|
||||
const approval = await accessApprovalRequestDAL.transaction(async (tx) => {
|
||||
const approvalRequest = await accessApprovalRequestDAL.updateById(
|
||||
requestId,
|
||||
{
|
||||
temporaryRange,
|
||||
isTemporary: true,
|
||||
editNote,
|
||||
editedByUserId: actorId
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
// reset review progress
|
||||
await accessApprovalRequestReviewerDAL.delete(
|
||||
{
|
||||
requestId
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
const requesterFullName = `${requestedByUser.firstName} ${requestedByUser.lastName}`;
|
||||
const editorFullName = `${editedByUser.firstName} ${editedByUser.lastName}`;
|
||||
const approvalUrl = `${cfg.SITE_URL}/projects/secret-management/${project.id}/approval`;
|
||||
|
||||
await triggerWorkflowIntegrationNotification({
|
||||
input: {
|
||||
notification: {
|
||||
type: TriggerFeature.ACCESS_REQUEST_UPDATED,
|
||||
payload: {
|
||||
projectName: project.name,
|
||||
requesterFullName,
|
||||
isTemporary: true,
|
||||
requesterEmail: requestedByUser.email as string,
|
||||
secretPath,
|
||||
environment: envSlug,
|
||||
permissions: accessTypes,
|
||||
approvalUrl,
|
||||
editNote,
|
||||
editorEmail: editedByUser.email as string,
|
||||
editorFullName
|
||||
}
|
||||
},
|
||||
projectId: project.id
|
||||
},
|
||||
dependencies: {
|
||||
projectDAL,
|
||||
projectSlackConfigDAL,
|
||||
kmsService,
|
||||
microsoftTeamsService,
|
||||
projectMicrosoftTeamsConfigDAL
|
||||
}
|
||||
});
|
||||
|
||||
await smtpService.sendMail({
|
||||
recipients: policy.approvers
|
||||
.filter((approver) => Boolean(approver.email) && approver.userId !== editedByUser.id)
|
||||
.map((approver) => approver.email!),
|
||||
subjectLine: "Access Approval Request Updated",
|
||||
substitutions: {
|
||||
projectName: project.name,
|
||||
requesterFullName,
|
||||
requesterEmail: requestedByUser.email,
|
||||
isTemporary: true,
|
||||
expiresIn: msFn(ms(temporaryRange || ""), { long: true }),
|
||||
secretPath,
|
||||
environment: envSlug,
|
||||
permissions: accessTypes,
|
||||
approvalUrl,
|
||||
editNote,
|
||||
editorFullName,
|
||||
editorEmail: editedByUser.email
|
||||
},
|
||||
template: SmtpTemplates.AccessApprovalRequestUpdated
|
||||
});
|
||||
|
||||
return approvalRequest;
|
||||
});
|
||||
|
||||
return { request: approval };
|
||||
};
|
||||
|
||||
const listApprovalRequests: TAccessApprovalRequestServiceFactory["listApprovalRequests"] = async ({
|
||||
projectSlug,
|
||||
authorUserId,
|
||||
@@ -650,6 +799,7 @@ export const accessApprovalRequestServiceFactory = ({
|
||||
|
||||
return {
|
||||
createAccessApprovalRequest,
|
||||
updateAccessApprovalRequest,
|
||||
listApprovalRequests,
|
||||
reviewAccessRequest,
|
||||
getCount
|
||||
|
@@ -30,6 +30,12 @@ export type TCreateAccessApprovalRequestDTO = {
|
||||
note?: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUpdateAccessApprovalRequestDTO = {
|
||||
requestId: string;
|
||||
temporaryRange: string;
|
||||
editNote: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TListApprovalRequestsDTO = {
|
||||
projectSlug: string;
|
||||
authorUserId?: string;
|
||||
@@ -54,6 +60,23 @@ export interface TAccessApprovalRequestServiceFactory {
|
||||
privilegeDeletedAt?: Date | null | undefined;
|
||||
};
|
||||
}>;
|
||||
updateAccessApprovalRequest: (arg: TUpdateAccessApprovalRequestDTO) => Promise<{
|
||||
request: {
|
||||
status: string;
|
||||
id: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
policyId: string;
|
||||
isTemporary: boolean;
|
||||
requestedByUserId: string;
|
||||
privilegeId?: string | null | undefined;
|
||||
requestedBy?: string | null | undefined;
|
||||
temporaryRange?: string | null | undefined;
|
||||
permissions?: unknown;
|
||||
note?: string | null | undefined;
|
||||
privilegeDeletedAt?: Date | null | undefined;
|
||||
};
|
||||
}>;
|
||||
listApprovalRequests: (arg: TListApprovalRequestsDTO) => Promise<{
|
||||
requests: {
|
||||
policy: {
|
||||
|
@@ -14,7 +14,7 @@ import { ActorType } from "@app/services/auth/auth-type";
|
||||
import { EventType, filterableSecretEvents } from "./audit-log-types";
|
||||
|
||||
export interface TAuditLogDALFactory extends Omit<TOrmify<TableName.AuditLog>, "find"> {
|
||||
pruneAuditLog: (tx?: knex.Knex) => Promise<void>;
|
||||
pruneAuditLog: () => Promise<void>;
|
||||
find: (
|
||||
arg: Omit<TFindQuery, "actor" | "eventType"> & {
|
||||
actorId?: string | undefined;
|
||||
@@ -41,6 +41,10 @@ type TFindQuery = {
|
||||
offset?: number;
|
||||
};
|
||||
|
||||
const QUERY_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
|
||||
const AUDIT_LOG_PRUNE_BATCH_SIZE = 10000;
|
||||
const MAX_RETRY_ON_FAILURE = 3;
|
||||
|
||||
export const auditLogDALFactory = (db: TDbClient) => {
|
||||
const auditLogOrm = ormify(db, TableName.AuditLog);
|
||||
|
||||
@@ -151,20 +155,20 @@ export const auditLogDALFactory = (db: TDbClient) => {
|
||||
};
|
||||
|
||||
// delete all audit log that have expired
|
||||
const pruneAuditLog: TAuditLogDALFactory["pruneAuditLog"] = async (tx) => {
|
||||
const runPrune = async (dbClient: knex.Knex) => {
|
||||
const AUDIT_LOG_PRUNE_BATCH_SIZE = 10000;
|
||||
const MAX_RETRY_ON_FAILURE = 3;
|
||||
const pruneAuditLog: TAuditLogDALFactory["pruneAuditLog"] = async () => {
|
||||
const today = new Date();
|
||||
let deletedAuditLogIds: { id: string }[] = [];
|
||||
let numberOfRetryOnFailure = 0;
|
||||
let isRetrying = false;
|
||||
|
||||
const today = new Date();
|
||||
let deletedAuditLogIds: { id: string }[] = [];
|
||||
let numberOfRetryOnFailure = 0;
|
||||
let isRetrying = false;
|
||||
logger.info(`${QueueName.DailyResourceCleanUp}: audit log started`);
|
||||
do {
|
||||
try {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
deletedAuditLogIds = await db.transaction(async (trx) => {
|
||||
await trx.raw(`SET statement_timeout = ${QUERY_TIMEOUT_MS}`);
|
||||
|
||||
logger.info(`${QueueName.DailyResourceCleanUp}: audit log started`);
|
||||
do {
|
||||
try {
|
||||
const findExpiredLogSubQuery = dbClient(TableName.AuditLog)
|
||||
const findExpiredLogSubQuery = trx(TableName.AuditLog)
|
||||
.where("expiresAt", "<", today)
|
||||
.where("createdAt", "<", today) // to use audit log partition
|
||||
.orderBy(`${TableName.AuditLog}.createdAt`, "desc")
|
||||
@@ -172,34 +176,25 @@ export const auditLogDALFactory = (db: TDbClient) => {
|
||||
.limit(AUDIT_LOG_PRUNE_BATCH_SIZE);
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
deletedAuditLogIds = await dbClient(TableName.AuditLog)
|
||||
.whereIn("id", findExpiredLogSubQuery)
|
||||
.del()
|
||||
.returning("id");
|
||||
numberOfRetryOnFailure = 0; // reset
|
||||
} catch (error) {
|
||||
numberOfRetryOnFailure += 1;
|
||||
logger.error(error, "Failed to delete audit log on pruning");
|
||||
} finally {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 10); // time to breathe for db
|
||||
});
|
||||
}
|
||||
isRetrying = numberOfRetryOnFailure > 0;
|
||||
} while (deletedAuditLogIds.length > 0 || (isRetrying && numberOfRetryOnFailure < MAX_RETRY_ON_FAILURE));
|
||||
logger.info(`${QueueName.DailyResourceCleanUp}: audit log completed`);
|
||||
};
|
||||
const results = await trx(TableName.AuditLog).whereIn("id", findExpiredLogSubQuery).del().returning("id");
|
||||
|
||||
if (tx) {
|
||||
await runPrune(tx);
|
||||
} else {
|
||||
const QUERY_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
|
||||
await db.transaction(async (trx) => {
|
||||
await trx.raw(`SET statement_timeout = ${QUERY_TIMEOUT_MS}`);
|
||||
await runPrune(trx);
|
||||
});
|
||||
}
|
||||
return results;
|
||||
});
|
||||
|
||||
numberOfRetryOnFailure = 0; // reset
|
||||
} catch (error) {
|
||||
numberOfRetryOnFailure += 1;
|
||||
deletedAuditLogIds = [];
|
||||
logger.error(error, "Failed to delete audit log on pruning");
|
||||
} finally {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 10); // time to breathe for db
|
||||
});
|
||||
}
|
||||
isRetrying = numberOfRetryOnFailure > 0;
|
||||
} while (deletedAuditLogIds.length > 0 || (isRetrying && numberOfRetryOnFailure < MAX_RETRY_ON_FAILURE));
|
||||
logger.info(`${QueueName.DailyResourceCleanUp}: audit log completed`);
|
||||
};
|
||||
|
||||
const create: TAuditLogDALFactory["create"] = async (tx) => {
|
||||
|
@@ -1,8 +1,6 @@
|
||||
import { AxiosError, RawAxiosRequestHeaders } from "axios";
|
||||
|
||||
import { ProjectType, SecretKeyEncoding } from "@app/db/schemas";
|
||||
import { TEventBusService } from "@app/ee/services/event/event-bus-service";
|
||||
import { TopicName, toPublishableEvent } from "@app/ee/services/event/types";
|
||||
import { SecretKeyEncoding } from "@app/db/schemas";
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { crypto } from "@app/lib/crypto/cryptography";
|
||||
import { logger } from "@app/lib/logger";
|
||||
@@ -22,7 +20,6 @@ type TAuditLogQueueServiceFactoryDep = {
|
||||
queueService: TQueueServiceFactory;
|
||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
eventBusService: TEventBusService;
|
||||
};
|
||||
|
||||
export type TAuditLogQueueServiceFactory = {
|
||||
@@ -38,8 +35,7 @@ export const auditLogQueueServiceFactory = async ({
|
||||
queueService,
|
||||
projectDAL,
|
||||
licenseService,
|
||||
auditLogStreamDAL,
|
||||
eventBusService
|
||||
auditLogStreamDAL
|
||||
}: TAuditLogQueueServiceFactoryDep): Promise<TAuditLogQueueServiceFactory> => {
|
||||
const pushToLog = async (data: TCreateAuditLogDTO) => {
|
||||
await queueService.queue<QueueName.AuditLog>(QueueName.AuditLog, QueueJobs.AuditLog, data, {
|
||||
@@ -145,16 +141,6 @@ export const auditLogQueueServiceFactory = async ({
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const publishable = toPublishableEvent(event);
|
||||
|
||||
if (publishable) {
|
||||
await eventBusService.publish(TopicName.CoreServers, {
|
||||
type: ProjectType.SecretManager,
|
||||
source: "infiscal",
|
||||
data: publishable.data
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
|
289
backend/src/ee/services/dynamic-secret/providers/couchbase.ts
Normal file
289
backend/src/ee/services/dynamic-secret/providers/couchbase.ts
Normal file
@@ -0,0 +1,289 @@
|
||||
import crypto from "node:crypto";
|
||||
|
||||
import axios from "axios";
|
||||
import RE2 from "re2";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { sanitizeString } from "@app/lib/fn";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator/validate-url";
|
||||
|
||||
import { DynamicSecretCouchbaseSchema, PasswordRequirements, TDynamicProviderFns } from "./models";
|
||||
import { compileUsernameTemplate } from "./templateUtils";
|
||||
|
||||
type TCreateCouchbaseUser = {
|
||||
name: string;
|
||||
password: string;
|
||||
access: {
|
||||
privileges: string[];
|
||||
resources: {
|
||||
buckets: {
|
||||
name: string;
|
||||
scopes?: {
|
||||
name: string;
|
||||
collections?: string[];
|
||||
}[];
|
||||
}[];
|
||||
};
|
||||
}[];
|
||||
};
|
||||
|
||||
type CouchbaseUserResponse = {
|
||||
id: string;
|
||||
uuid?: string;
|
||||
};
|
||||
|
||||
const sanitizeCouchbaseUsername = (username: string): string => {
|
||||
// Couchbase username restrictions:
|
||||
// - Cannot contain: ) ( > < , ; : " \ / ] [ ? = } {
|
||||
// - Cannot begin with @ character
|
||||
|
||||
const forbiddenCharsPattern = new RE2('[\\)\\(><,;:"\\\\\\[\\]\\?=\\}\\{]', "g");
|
||||
let sanitized = forbiddenCharsPattern.replace(username, "-");
|
||||
|
||||
const leadingAtPattern = new RE2("^@+");
|
||||
sanitized = leadingAtPattern.replace(sanitized, "");
|
||||
|
||||
if (!sanitized || sanitized.length === 0) {
|
||||
return alphaNumericNanoId(12);
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
};
|
||||
|
||||
/**
|
||||
* Normalizes bucket configuration to handle wildcard (*) access consistently.
|
||||
*
|
||||
* Key behaviors:
|
||||
* - If "*" appears anywhere (string or array), grants access to ALL buckets, scopes, and collections
|
||||
*
|
||||
* @param buckets - Either a string or array of bucket configurations
|
||||
* @returns Normalized bucket resources for Couchbase API
|
||||
*/
|
||||
const normalizeBucketConfiguration = (
|
||||
buckets:
|
||||
| string
|
||||
| Array<{
|
||||
name: string;
|
||||
scopes?: Array<{
|
||||
name: string;
|
||||
collections?: string[];
|
||||
}>;
|
||||
}>
|
||||
) => {
|
||||
if (typeof buckets === "string") {
|
||||
// Simple string format - either "*" or comma-separated bucket names
|
||||
const bucketNames = buckets
|
||||
.split(",")
|
||||
.map((bucket) => bucket.trim())
|
||||
.filter((bucket) => bucket.length > 0);
|
||||
|
||||
// If "*" is present anywhere, grant access to all buckets, scopes, and collections
|
||||
if (bucketNames.includes("*") || buckets === "*") {
|
||||
return [{ name: "*" }];
|
||||
}
|
||||
return bucketNames.map((bucketName) => ({ name: bucketName }));
|
||||
}
|
||||
|
||||
// Array of bucket objects with scopes and collections
|
||||
// Check if any bucket is "*" - if so, grant access to all buckets, scopes, and collections
|
||||
const hasWildcardBucket = buckets.some((bucket) => bucket.name === "*");
|
||||
|
||||
if (hasWildcardBucket) {
|
||||
return [{ name: "*" }];
|
||||
}
|
||||
|
||||
return buckets.map((bucket) => ({
|
||||
name: bucket.name,
|
||||
scopes: bucket.scopes?.map((scope) => ({
|
||||
name: scope.name,
|
||||
collections: scope.collections || []
|
||||
}))
|
||||
}));
|
||||
};
|
||||
|
||||
const generateUsername = (usernameTemplate?: string | null, identity?: { name: string }) => {
|
||||
const randomUsername = alphaNumericNanoId(12);
|
||||
if (!usernameTemplate) return sanitizeCouchbaseUsername(randomUsername);
|
||||
|
||||
const compiledUsername = compileUsernameTemplate({
|
||||
usernameTemplate,
|
||||
randomUsername,
|
||||
identity
|
||||
});
|
||||
|
||||
return sanitizeCouchbaseUsername(compiledUsername);
|
||||
};
|
||||
|
||||
const generatePassword = (requirements?: PasswordRequirements): string => {
|
||||
const {
|
||||
length = 12,
|
||||
required = { lowercase: 1, uppercase: 1, digits: 1, symbols: 1 },
|
||||
allowedSymbols = "!@#$%^()_+-=[]{}:,?/~`"
|
||||
} = requirements || {};
|
||||
|
||||
const lowercase = "abcdefghijklmnopqrstuvwxyz";
|
||||
const uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
const digits = "0123456789";
|
||||
const symbols = allowedSymbols;
|
||||
|
||||
let password = "";
|
||||
let remaining = length;
|
||||
|
||||
// Add required characters
|
||||
for (let i = 0; i < required.lowercase; i += 1) {
|
||||
password += lowercase[crypto.randomInt(lowercase.length)];
|
||||
remaining -= 1;
|
||||
}
|
||||
for (let i = 0; i < required.uppercase; i += 1) {
|
||||
password += uppercase[crypto.randomInt(uppercase.length)];
|
||||
remaining -= 1;
|
||||
}
|
||||
for (let i = 0; i < required.digits; i += 1) {
|
||||
password += digits[crypto.randomInt(digits.length)];
|
||||
remaining -= 1;
|
||||
}
|
||||
for (let i = 0; i < required.symbols; i += 1) {
|
||||
password += symbols[crypto.randomInt(symbols.length)];
|
||||
remaining -= 1;
|
||||
}
|
||||
|
||||
// Fill remaining with random characters from all sets
|
||||
const allChars = lowercase + uppercase + digits + symbols;
|
||||
for (let i = 0; i < remaining; i += 1) {
|
||||
password += allChars[crypto.randomInt(allChars.length)];
|
||||
}
|
||||
|
||||
// Shuffle the password
|
||||
return password
|
||||
.split("")
|
||||
.sort(() => crypto.randomInt(3) - 1)
|
||||
.join("");
|
||||
};
|
||||
|
||||
const couchbaseApiRequest = async (
|
||||
method: string,
|
||||
url: string,
|
||||
apiKey: string,
|
||||
data?: unknown
|
||||
): Promise<CouchbaseUserResponse> => {
|
||||
await blockLocalAndPrivateIpAddresses(url);
|
||||
|
||||
try {
|
||||
const response = await axios({
|
||||
method: method.toLowerCase() as "get" | "post" | "put" | "delete",
|
||||
url,
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
data: data || undefined,
|
||||
timeout: 30000
|
||||
});
|
||||
|
||||
return response.data as CouchbaseUserResponse;
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [apiKey]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to connect with provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const CouchbaseProvider = (): TDynamicProviderFns => {
|
||||
const validateProviderInputs = async (inputs: object) => {
|
||||
const providerInputs = DynamicSecretCouchbaseSchema.parse(inputs);
|
||||
|
||||
await blockLocalAndPrivateIpAddresses(providerInputs.url);
|
||||
|
||||
return providerInputs;
|
||||
};
|
||||
|
||||
const validateConnection = async (inputs: unknown): Promise<boolean> => {
|
||||
try {
|
||||
const providerInputs = await validateProviderInputs(inputs as object);
|
||||
|
||||
// Test connection by trying to get organization info
|
||||
const url = `${providerInputs.url}/v4/organizations/${providerInputs.orgId}`;
|
||||
await couchbaseApiRequest("GET", url, providerInputs.auth.apiKey);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to connect to Couchbase: ${error instanceof Error ? error.message : "Unknown error"}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const create = async ({
|
||||
inputs,
|
||||
usernameTemplate,
|
||||
identity
|
||||
}: {
|
||||
inputs: unknown;
|
||||
usernameTemplate?: string | null;
|
||||
identity?: { name: string };
|
||||
}) => {
|
||||
const providerInputs = await validateProviderInputs(inputs as object);
|
||||
|
||||
const username = generateUsername(usernameTemplate, identity);
|
||||
|
||||
const password = generatePassword(providerInputs.passwordRequirements);
|
||||
|
||||
const createUserUrl = `${providerInputs.url}/v4/organizations/${providerInputs.orgId}/projects/${providerInputs.projectId}/clusters/${providerInputs.clusterId}/users`;
|
||||
|
||||
const bucketResources = normalizeBucketConfiguration(providerInputs.buckets);
|
||||
|
||||
const userData: TCreateCouchbaseUser = {
|
||||
name: username,
|
||||
password,
|
||||
access: [
|
||||
{
|
||||
privileges: providerInputs.roles,
|
||||
resources: {
|
||||
buckets: bucketResources
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const response = await couchbaseApiRequest("POST", createUserUrl, providerInputs.auth.apiKey, userData);
|
||||
|
||||
const userUuid = response?.id || response?.uuid || username;
|
||||
|
||||
return {
|
||||
entityId: userUuid,
|
||||
data: {
|
||||
username,
|
||||
password
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const revoke = async (inputs: unknown, entityId: string) => {
|
||||
const providerInputs = await validateProviderInputs(inputs as object);
|
||||
|
||||
const deleteUserUrl = `${providerInputs.url}/v4/organizations/${providerInputs.orgId}/projects/${providerInputs.projectId}/clusters/${providerInputs.clusterId}/users/${encodeURIComponent(entityId)}`;
|
||||
|
||||
await couchbaseApiRequest("DELETE", deleteUserUrl, providerInputs.auth.apiKey);
|
||||
|
||||
return { entityId };
|
||||
};
|
||||
|
||||
const renew = async (_inputs: unknown, entityId: string) => {
|
||||
// Couchbase Cloud API doesn't support renewing user credentials
|
||||
// The user remains valid until explicitly deleted
|
||||
return { entityId };
|
||||
};
|
||||
|
||||
return {
|
||||
validateProviderInputs,
|
||||
validateConnection,
|
||||
create,
|
||||
revoke,
|
||||
renew
|
||||
};
|
||||
};
|
@@ -5,6 +5,7 @@ import { AwsElastiCacheDatabaseProvider } from "./aws-elasticache";
|
||||
import { AwsIamProvider } from "./aws-iam";
|
||||
import { AzureEntraIDProvider } from "./azure-entra-id";
|
||||
import { CassandraProvider } from "./cassandra";
|
||||
import { CouchbaseProvider } from "./couchbase";
|
||||
import { ElasticSearchProvider } from "./elastic-search";
|
||||
import { GcpIamProvider } from "./gcp-iam";
|
||||
import { GithubProvider } from "./github";
|
||||
@@ -46,5 +47,6 @@ export const buildDynamicSecretProviders = ({
|
||||
[DynamicSecretProviders.Kubernetes]: KubernetesProvider({ gatewayService }),
|
||||
[DynamicSecretProviders.Vertica]: VerticaProvider({ gatewayService }),
|
||||
[DynamicSecretProviders.GcpIam]: GcpIamProvider(),
|
||||
[DynamicSecretProviders.Github]: GithubProvider()
|
||||
[DynamicSecretProviders.Github]: GithubProvider(),
|
||||
[DynamicSecretProviders.Couchbase]: CouchbaseProvider()
|
||||
});
|
||||
|
@@ -505,6 +505,91 @@ export const DynamicSecretGithubSchema = z.object({
|
||||
.describe("The private key generated for your GitHub App.")
|
||||
});
|
||||
|
||||
export const DynamicSecretCouchbaseSchema = z.object({
|
||||
url: z.string().url().trim().min(1).describe("Couchbase Cloud API URL"),
|
||||
orgId: z.string().trim().min(1).describe("Organization ID"),
|
||||
projectId: z.string().trim().min(1).describe("Project ID"),
|
||||
clusterId: z.string().trim().min(1).describe("Cluster ID"),
|
||||
roles: z.array(z.string().trim().min(1)).min(1).describe("Roles to assign to the user"),
|
||||
buckets: z
|
||||
.union([
|
||||
z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.default("*")
|
||||
.refine((val) => {
|
||||
if (val.includes(",")) {
|
||||
const buckets = val
|
||||
.split(",")
|
||||
.map((b) => b.trim())
|
||||
.filter((b) => b.length > 0);
|
||||
if (buckets.includes("*") && buckets.length > 1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}, "Cannot combine '*' with other bucket names"),
|
||||
z
|
||||
.array(
|
||||
z.object({
|
||||
name: z.string().trim().min(1).describe("Bucket name"),
|
||||
scopes: z
|
||||
.array(
|
||||
z.object({
|
||||
name: z.string().trim().min(1).describe("Scope name"),
|
||||
collections: z.array(z.string().trim().min(1)).optional().describe("Collection names")
|
||||
})
|
||||
)
|
||||
.optional()
|
||||
.describe("Scopes within the bucket")
|
||||
})
|
||||
)
|
||||
.refine((buckets) => {
|
||||
const hasWildcard = buckets.some((bucket) => bucket.name === "*");
|
||||
if (hasWildcard && buckets.length > 1) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}, "Cannot combine '*' bucket with other buckets")
|
||||
])
|
||||
.default("*")
|
||||
.describe(
|
||||
"Bucket configuration: '*' for all buckets, scopes, and collections or array of bucket objects with specific scopes and collections"
|
||||
),
|
||||
passwordRequirements: z
|
||||
.object({
|
||||
length: z.number().min(8, "Password must be at least 8 characters").max(128),
|
||||
required: z
|
||||
.object({
|
||||
lowercase: z.number().min(1, "At least 1 lowercase character required"),
|
||||
uppercase: z.number().min(1, "At least 1 uppercase character required"),
|
||||
digits: z.number().min(1, "At least 1 digit required"),
|
||||
symbols: z.number().min(1, "At least 1 special character required")
|
||||
})
|
||||
.refine((data) => {
|
||||
const total = Object.values(data).reduce((sum, count) => sum + count, 0);
|
||||
return total <= 128;
|
||||
}, "Sum of required characters cannot exceed 128"),
|
||||
allowedSymbols: z
|
||||
.string()
|
||||
.refine((symbols) => {
|
||||
const forbiddenChars = ["<", ">", ";", ".", "*", "&", "|", "£"];
|
||||
return !forbiddenChars.some((char) => symbols?.includes(char));
|
||||
}, "Cannot contain: < > ; . * & | £")
|
||||
.optional()
|
||||
})
|
||||
.refine((data) => {
|
||||
const total = Object.values(data.required).reduce((sum, count) => sum + count, 0);
|
||||
return total <= data.length;
|
||||
}, "Sum of required characters cannot exceed the total length")
|
||||
.optional()
|
||||
.describe("Password generation requirements for Couchbase"),
|
||||
auth: z.object({
|
||||
apiKey: z.string().trim().min(1).describe("Couchbase Cloud API Key")
|
||||
})
|
||||
});
|
||||
|
||||
export enum DynamicSecretProviders {
|
||||
SqlDatabase = "sql-database",
|
||||
Cassandra = "cassandra",
|
||||
@@ -524,7 +609,8 @@ export enum DynamicSecretProviders {
|
||||
Kubernetes = "kubernetes",
|
||||
Vertica = "vertica",
|
||||
GcpIam = "gcp-iam",
|
||||
Github = "github"
|
||||
Github = "github",
|
||||
Couchbase = "couchbase"
|
||||
}
|
||||
|
||||
export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
|
||||
@@ -546,7 +632,8 @@ export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
|
||||
z.object({ type: z.literal(DynamicSecretProviders.Kubernetes), inputs: DynamicSecretKubernetesSchema }),
|
||||
z.object({ type: z.literal(DynamicSecretProviders.Vertica), inputs: DynamicSecretVerticaSchema }),
|
||||
z.object({ type: z.literal(DynamicSecretProviders.GcpIam), inputs: DynamicSecretGcpIamSchema }),
|
||||
z.object({ type: z.literal(DynamicSecretProviders.Github), inputs: DynamicSecretGithubSchema })
|
||||
z.object({ type: z.literal(DynamicSecretProviders.Github), inputs: DynamicSecretGithubSchema }),
|
||||
z.object({ type: z.literal(DynamicSecretProviders.Couchbase), inputs: DynamicSecretCouchbaseSchema })
|
||||
]);
|
||||
|
||||
export type TDynamicProviderFns = {
|
||||
|
@@ -3,7 +3,7 @@ import { z } from "zod";
|
||||
|
||||
import { logger } from "@app/lib/logger";
|
||||
|
||||
import { EventSchema, TopicName } from "./types";
|
||||
import { BusEventSchema, TopicName } from "./types";
|
||||
|
||||
export const eventBusFactory = (redis: Redis) => {
|
||||
const publisher = redis.duplicate();
|
||||
@@ -28,7 +28,7 @@ export const eventBusFactory = (redis: Redis) => {
|
||||
* @param topic - The topic to publish the event to.
|
||||
* @param event - The event data to publish.
|
||||
*/
|
||||
const publish = async <T extends z.input<typeof EventSchema>>(topic: TopicName, event: T) => {
|
||||
const publish = async <T extends z.input<typeof BusEventSchema>>(topic: TopicName, event: T) => {
|
||||
const json = JSON.stringify(event);
|
||||
|
||||
return publisher.publish(topic, json, (err) => {
|
||||
@@ -44,7 +44,7 @@ export const eventBusFactory = (redis: Redis) => {
|
||||
* @template T - The type of the event data, which should match the schema defined in EventSchema.
|
||||
* @returns A function that can be called to unsubscribe from the event bus.
|
||||
*/
|
||||
const subscribe = <T extends z.infer<typeof EventSchema>>(fn: (data: T) => Promise<void> | void) => {
|
||||
const subscribe = <T extends z.infer<typeof BusEventSchema>>(fn: (data: T) => Promise<void> | void) => {
|
||||
// Not using async await cause redis client's `on` method does not expect async listeners.
|
||||
const listener = (channel: string, message: string) => {
|
||||
try {
|
||||
|
@@ -7,7 +7,7 @@ import { logger } from "@app/lib/logger";
|
||||
|
||||
import { TEventBusService } from "./event-bus-service";
|
||||
import { createEventStreamClient, EventStreamClient, IEventStreamClientOpts } from "./event-sse-stream";
|
||||
import { EventData, RegisteredEvent, toBusEventName } from "./types";
|
||||
import { BusEvent, RegisteredEvent } from "./types";
|
||||
|
||||
const AUTH_REFRESH_INTERVAL = 60 * 1000;
|
||||
const HEART_BEAT_INTERVAL = 15 * 1000;
|
||||
@@ -69,8 +69,8 @@ export const sseServiceFactory = (bus: TEventBusService, redis: Redis) => {
|
||||
}
|
||||
};
|
||||
|
||||
function filterEventsForClient(client: EventStreamClient, event: EventData, registered: RegisteredEvent[]) {
|
||||
const eventType = toBusEventName(event.data.eventType);
|
||||
function filterEventsForClient(client: EventStreamClient, event: BusEvent, registered: RegisteredEvent[]) {
|
||||
const eventType = event.data.event;
|
||||
const match = registered.find((r) => r.event === eventType);
|
||||
if (!match) return;
|
||||
|
||||
|
@@ -12,7 +12,7 @@ import { KeyStorePrefixes } from "@app/keystore/keystore";
|
||||
import { conditionsMatcher } from "@app/lib/casl";
|
||||
import { logger } from "@app/lib/logger";
|
||||
|
||||
import { EventData, RegisteredEvent } from "./types";
|
||||
import { BusEvent, RegisteredEvent } from "./types";
|
||||
|
||||
export const getServerSentEventsHeaders = () =>
|
||||
({
|
||||
@@ -55,7 +55,7 @@ export type EventStreamClient = {
|
||||
id: string;
|
||||
stream: Readable;
|
||||
open: () => Promise<void>;
|
||||
send: (data: EventMessage | EventData) => void;
|
||||
send: (data: EventMessage | BusEvent) => void;
|
||||
ping: () => Promise<void>;
|
||||
refresh: () => Promise<void>;
|
||||
close: () => void;
|
||||
@@ -73,15 +73,12 @@ export function createEventStreamClient(redis: Redis, options: IEventStreamClien
|
||||
return {
|
||||
subject: options.type,
|
||||
action: "subscribe",
|
||||
conditions: {
|
||||
eventType: r.event,
|
||||
...(hasConditions
|
||||
? {
|
||||
environment: r.conditions?.environmentSlug ?? "",
|
||||
secretPath: { $glob: secretPath }
|
||||
}
|
||||
: {})
|
||||
}
|
||||
conditions: hasConditions
|
||||
? {
|
||||
environment: r.conditions?.environmentSlug ?? "",
|
||||
secretPath: { $glob: secretPath }
|
||||
}
|
||||
: undefined
|
||||
};
|
||||
});
|
||||
|
||||
@@ -98,7 +95,7 @@ export function createEventStreamClient(redis: Redis, options: IEventStreamClien
|
||||
// We will manually push data to the stream
|
||||
stream._read = () => {};
|
||||
|
||||
const send = (data: EventMessage | EventData) => {
|
||||
const send = (data: EventMessage | BusEvent) => {
|
||||
const chunk = serializeSseEvent(data);
|
||||
if (!stream.push(chunk)) {
|
||||
logger.debug("Backpressure detected: dropped manual event");
|
||||
@@ -126,7 +123,7 @@ export function createEventStreamClient(redis: Redis, options: IEventStreamClien
|
||||
|
||||
await redis.set(key, "1", "EX", 60);
|
||||
|
||||
stream.push("1");
|
||||
send({ type: "ping" });
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
|
@@ -1,7 +1,8 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { ProjectType } from "@app/db/schemas";
|
||||
import { Event, EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
|
||||
import { ProjectPermissionSecretEventActions } from "../permission/project-permission";
|
||||
|
||||
export enum TopicName {
|
||||
CoreServers = "infisical::core-servers"
|
||||
@@ -10,84 +11,44 @@ export enum TopicName {
|
||||
export enum BusEventName {
|
||||
CreateSecret = "secret:create",
|
||||
UpdateSecret = "secret:update",
|
||||
DeleteSecret = "secret:delete"
|
||||
DeleteSecret = "secret:delete",
|
||||
ImportMutation = "secret:import-mutation"
|
||||
}
|
||||
|
||||
type PublisableEventTypes =
|
||||
| EventType.CREATE_SECRET
|
||||
| EventType.CREATE_SECRETS
|
||||
| EventType.DELETE_SECRET
|
||||
| EventType.DELETE_SECRETS
|
||||
| EventType.UPDATE_SECRETS
|
||||
| EventType.UPDATE_SECRET;
|
||||
|
||||
export function toBusEventName(input: EventType) {
|
||||
switch (input) {
|
||||
case EventType.CREATE_SECRET:
|
||||
case EventType.CREATE_SECRETS:
|
||||
return BusEventName.CreateSecret;
|
||||
case EventType.UPDATE_SECRET:
|
||||
case EventType.UPDATE_SECRETS:
|
||||
return BusEventName.UpdateSecret;
|
||||
case EventType.DELETE_SECRET:
|
||||
case EventType.DELETE_SECRETS:
|
||||
return BusEventName.DeleteSecret;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const isBulkEvent = (event: Event): event is Extract<Event, { metadata: { secrets: Array<unknown> } }> => {
|
||||
return event.type.endsWith("-secrets"); // Feels so wrong
|
||||
};
|
||||
|
||||
export const toPublishableEvent = (event: Event) => {
|
||||
const name = toBusEventName(event.type);
|
||||
|
||||
if (!name) return null;
|
||||
|
||||
const e = event as Extract<Event, { type: PublisableEventTypes }>;
|
||||
|
||||
if (isBulkEvent(e)) {
|
||||
return {
|
||||
name,
|
||||
isBulk: true,
|
||||
data: {
|
||||
eventType: e.type,
|
||||
payload: e.metadata.secrets.map((s) => ({
|
||||
environment: e.metadata.environment,
|
||||
secretPath: e.metadata.secretPath,
|
||||
...s
|
||||
}))
|
||||
}
|
||||
} as const;
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
isBulk: false,
|
||||
data: {
|
||||
eventType: e.type,
|
||||
payload: {
|
||||
...e.metadata,
|
||||
environment: e.metadata.environment
|
||||
}
|
||||
export const Mappings = {
|
||||
BusEventToAction(input: BusEventName) {
|
||||
switch (input) {
|
||||
case BusEventName.CreateSecret:
|
||||
return ProjectPermissionSecretEventActions.SubscribeCreated;
|
||||
case BusEventName.DeleteSecret:
|
||||
return ProjectPermissionSecretEventActions.SubscribeDeleted;
|
||||
case BusEventName.ImportMutation:
|
||||
return ProjectPermissionSecretEventActions.SubscribeImportMutations;
|
||||
case BusEventName.UpdateSecret:
|
||||
return ProjectPermissionSecretEventActions.SubscribeUpdated;
|
||||
default:
|
||||
throw new Error("Unknown bus event name");
|
||||
}
|
||||
} as const;
|
||||
}
|
||||
};
|
||||
|
||||
export const EventName = z.nativeEnum(BusEventName);
|
||||
|
||||
const EventSecretPayload = z.object({
|
||||
secretPath: z.string().optional(),
|
||||
secretId: z.string(),
|
||||
secretPath: z.string().optional(),
|
||||
secretKey: z.string(),
|
||||
environment: z.string()
|
||||
});
|
||||
|
||||
const EventImportMutationPayload = z.object({
|
||||
secretPath: z.string(),
|
||||
environment: z.string()
|
||||
});
|
||||
|
||||
export type EventSecret = z.infer<typeof EventSecretPayload>;
|
||||
|
||||
export const EventSchema = z.object({
|
||||
export const BusEventSchema = z.object({
|
||||
datacontenttype: z.literal("application/json").optional().default("application/json"),
|
||||
type: z.nativeEnum(ProjectType),
|
||||
source: z.string(),
|
||||
@@ -95,25 +56,38 @@ export const EventSchema = z.object({
|
||||
.string()
|
||||
.optional()
|
||||
.default(() => new Date().toISOString()),
|
||||
data: z.discriminatedUnion("eventType", [
|
||||
data: z.discriminatedUnion("event", [
|
||||
z.object({
|
||||
specversion: z.number().optional().default(1),
|
||||
eventType: z.enum([EventType.CREATE_SECRET, EventType.UPDATE_SECRET, EventType.DELETE_SECRET]),
|
||||
payload: EventSecretPayload
|
||||
event: z.enum([BusEventName.CreateSecret, BusEventName.DeleteSecret, BusEventName.UpdateSecret]),
|
||||
payload: z.union([EventSecretPayload, EventSecretPayload.array()])
|
||||
}),
|
||||
z.object({
|
||||
specversion: z.number().optional().default(1),
|
||||
eventType: z.enum([EventType.CREATE_SECRETS, EventType.UPDATE_SECRETS, EventType.DELETE_SECRETS]),
|
||||
payload: EventSecretPayload.array()
|
||||
event: z.enum([BusEventName.ImportMutation]),
|
||||
payload: z.union([EventImportMutationPayload, EventImportMutationPayload.array()])
|
||||
})
|
||||
// Add more event types as needed
|
||||
])
|
||||
});
|
||||
|
||||
export type EventData = z.infer<typeof EventSchema>;
|
||||
export type BusEvent = z.infer<typeof BusEventSchema>;
|
||||
|
||||
type PublishableEventPayload = z.input<typeof BusEventSchema>["data"];
|
||||
type PublishableSecretEvent = Extract<
|
||||
PublishableEventPayload,
|
||||
{ event: Exclude<BusEventName, BusEventName.ImportMutation> }
|
||||
>["payload"];
|
||||
|
||||
export type PublishableEvent = {
|
||||
created?: PublishableSecretEvent;
|
||||
updated?: PublishableSecretEvent;
|
||||
deleted?: PublishableSecretEvent;
|
||||
importMutation?: Extract<PublishableEventPayload, { event: BusEventName.ImportMutation }>["payload"];
|
||||
};
|
||||
|
||||
export const EventRegisterSchema = z.object({
|
||||
event: EventName,
|
||||
event: z.nativeEnum(BusEventName),
|
||||
conditions: z
|
||||
.object({
|
||||
secretPath: z.string().optional().default("/"),
|
||||
|
@@ -32,6 +32,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
||||
auditLogStreams: false,
|
||||
auditLogStreamLimit: 3,
|
||||
samlSSO: false,
|
||||
enforceGoogleSSO: false,
|
||||
hsm: false,
|
||||
oidcSSO: false,
|
||||
scim: false,
|
||||
|
@@ -47,6 +47,7 @@ export type TFeatureSet = {
|
||||
auditLogStreamLimit: 3;
|
||||
githubOrgSync: false;
|
||||
samlSSO: false;
|
||||
enforceGoogleSSO: false;
|
||||
hsm: false;
|
||||
oidcSSO: false;
|
||||
secretAccessInsights: false;
|
||||
|
@@ -13,6 +13,7 @@ import {
|
||||
ProjectPermissionPkiSubscriberActions,
|
||||
ProjectPermissionPkiTemplateActions,
|
||||
ProjectPermissionSecretActions,
|
||||
ProjectPermissionSecretEventActions,
|
||||
ProjectPermissionSecretRotationActions,
|
||||
ProjectPermissionSecretScanningConfigActions,
|
||||
ProjectPermissionSecretScanningDataSourceActions,
|
||||
@@ -161,8 +162,7 @@ const buildAdminPermissionRules = () => {
|
||||
ProjectPermissionSecretActions.ReadValue,
|
||||
ProjectPermissionSecretActions.Create,
|
||||
ProjectPermissionSecretActions.Edit,
|
||||
ProjectPermissionSecretActions.Delete,
|
||||
ProjectPermissionSecretActions.Subscribe
|
||||
ProjectPermissionSecretActions.Delete
|
||||
],
|
||||
ProjectPermissionSub.Secrets
|
||||
);
|
||||
@@ -253,6 +253,16 @@ const buildAdminPermissionRules = () => {
|
||||
ProjectPermissionSub.SecretScanningConfigs
|
||||
);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionSecretEventActions.SubscribeCreated,
|
||||
ProjectPermissionSecretEventActions.SubscribeDeleted,
|
||||
ProjectPermissionSecretEventActions.SubscribeUpdated,
|
||||
ProjectPermissionSecretEventActions.SubscribeImportMutations
|
||||
],
|
||||
ProjectPermissionSub.SecretEvents
|
||||
);
|
||||
|
||||
return rules;
|
||||
};
|
||||
|
||||
@@ -266,8 +276,7 @@ const buildMemberPermissionRules = () => {
|
||||
ProjectPermissionSecretActions.ReadValue,
|
||||
ProjectPermissionSecretActions.Edit,
|
||||
ProjectPermissionSecretActions.Create,
|
||||
ProjectPermissionSecretActions.Delete,
|
||||
ProjectPermissionSecretActions.Subscribe
|
||||
ProjectPermissionSecretActions.Delete
|
||||
],
|
||||
ProjectPermissionSub.Secrets
|
||||
);
|
||||
@@ -457,6 +466,16 @@ const buildMemberPermissionRules = () => {
|
||||
|
||||
can([ProjectPermissionSecretScanningConfigActions.Read], ProjectPermissionSub.SecretScanningConfigs);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionSecretEventActions.SubscribeCreated,
|
||||
ProjectPermissionSecretEventActions.SubscribeDeleted,
|
||||
ProjectPermissionSecretEventActions.SubscribeUpdated,
|
||||
ProjectPermissionSecretEventActions.SubscribeImportMutations
|
||||
],
|
||||
ProjectPermissionSub.SecretEvents
|
||||
);
|
||||
|
||||
return rules;
|
||||
};
|
||||
|
||||
@@ -507,6 +526,16 @@ const buildViewerPermissionRules = () => {
|
||||
|
||||
can([ProjectPermissionSecretScanningConfigActions.Read], ProjectPermissionSub.SecretScanningConfigs);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionSecretEventActions.SubscribeCreated,
|
||||
ProjectPermissionSecretEventActions.SubscribeDeleted,
|
||||
ProjectPermissionSecretEventActions.SubscribeUpdated,
|
||||
ProjectPermissionSecretEventActions.SubscribeImportMutations
|
||||
],
|
||||
ProjectPermissionSub.SecretEvents
|
||||
);
|
||||
|
||||
return rules;
|
||||
};
|
||||
|
||||
|
@@ -35,6 +35,7 @@ export interface TPermissionDALFactory {
|
||||
projectFavorites?: string[] | null | undefined;
|
||||
customRoleSlug?: string | null | undefined;
|
||||
orgAuthEnforced?: boolean | null | undefined;
|
||||
orgGoogleSsoAuthEnforced: boolean;
|
||||
} & {
|
||||
groups: {
|
||||
id: string;
|
||||
@@ -87,6 +88,7 @@ export interface TPermissionDALFactory {
|
||||
}[];
|
||||
orgId: string;
|
||||
orgAuthEnforced: boolean | null | undefined;
|
||||
orgGoogleSsoAuthEnforced: boolean;
|
||||
orgRole: OrgMembershipRole;
|
||||
userId: string;
|
||||
projectId: string;
|
||||
@@ -350,6 +352,7 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => {
|
||||
db.ref("slug").withSchema(TableName.OrgRoles).withSchema(TableName.OrgRoles).as("customRoleSlug"),
|
||||
db.ref("permissions").withSchema(TableName.OrgRoles),
|
||||
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
|
||||
db.ref("googleSsoAuthEnforced").withSchema(TableName.Organization).as("orgGoogleSsoAuthEnforced"),
|
||||
db.ref("bypassOrgAuthEnabled").withSchema(TableName.Organization).as("bypassOrgAuthEnabled"),
|
||||
db.ref("groupId").withSchema("userGroups"),
|
||||
db.ref("groupOrgId").withSchema("userGroups"),
|
||||
@@ -369,6 +372,7 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => {
|
||||
OrgMembershipsSchema.extend({
|
||||
permissions: z.unknown(),
|
||||
orgAuthEnforced: z.boolean().optional().nullable(),
|
||||
orgGoogleSsoAuthEnforced: z.boolean(),
|
||||
bypassOrgAuthEnabled: z.boolean(),
|
||||
customRoleSlug: z.string().optional().nullable(),
|
||||
shouldUseNewPrivilegeSystem: z.boolean()
|
||||
@@ -988,6 +992,7 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => {
|
||||
db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"),
|
||||
db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue"),
|
||||
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
|
||||
db.ref("googleSsoAuthEnforced").withSchema(TableName.Organization).as("orgGoogleSsoAuthEnforced"),
|
||||
db.ref("bypassOrgAuthEnabled").withSchema(TableName.Organization).as("bypassOrgAuthEnabled"),
|
||||
db.ref("role").withSchema(TableName.OrgMembership).as("orgRole"),
|
||||
db.ref("orgId").withSchema(TableName.Project),
|
||||
@@ -1003,6 +1008,7 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => {
|
||||
orgId,
|
||||
username,
|
||||
orgAuthEnforced,
|
||||
orgGoogleSsoAuthEnforced,
|
||||
orgRole,
|
||||
membershipId,
|
||||
groupMembershipId,
|
||||
@@ -1016,6 +1022,7 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => {
|
||||
}) => ({
|
||||
orgId,
|
||||
orgAuthEnforced,
|
||||
orgGoogleSsoAuthEnforced,
|
||||
orgRole: orgRole as OrgMembershipRole,
|
||||
userId,
|
||||
projectId,
|
||||
|
@@ -121,6 +121,7 @@ function isAuthMethodSaml(actorAuthMethod: ActorAuthMethod) {
|
||||
function validateOrgSSO(
|
||||
actorAuthMethod: ActorAuthMethod,
|
||||
isOrgSsoEnforced: TOrganizations["authEnforced"],
|
||||
isOrgGoogleSsoEnforced: TOrganizations["googleSsoAuthEnforced"],
|
||||
isOrgSsoBypassEnabled: TOrganizations["bypassOrgAuthEnabled"],
|
||||
orgRole: OrgMembershipRole
|
||||
) {
|
||||
@@ -128,10 +129,16 @@ function validateOrgSSO(
|
||||
throw new UnauthorizedError({ name: "No auth method defined" });
|
||||
}
|
||||
|
||||
if (isOrgSsoEnforced && isOrgSsoBypassEnabled && orgRole === OrgMembershipRole.Admin) {
|
||||
if ((isOrgSsoEnforced || isOrgGoogleSsoEnforced) && isOrgSsoBypassEnabled && orgRole === OrgMembershipRole.Admin) {
|
||||
return;
|
||||
}
|
||||
|
||||
// case: google sso is enforced, but the actor is not using google sso
|
||||
if (isOrgGoogleSsoEnforced && actorAuthMethod !== null && actorAuthMethod !== AuthMethod.GOOGLE) {
|
||||
throw new ForbiddenRequestError({ name: "Org auth enforced. Cannot access org-scoped resource" });
|
||||
}
|
||||
|
||||
// case: SAML SSO is enforced, but the actor is not using SAML SSO
|
||||
if (
|
||||
isOrgSsoEnforced &&
|
||||
actorAuthMethod !== null &&
|
||||
|
@@ -146,6 +146,7 @@ export const permissionServiceFactory = ({
|
||||
validateOrgSSO(
|
||||
authMethod,
|
||||
membership.orgAuthEnforced,
|
||||
membership.orgGoogleSsoAuthEnforced,
|
||||
membership.bypassOrgAuthEnabled,
|
||||
membership.role as OrgMembershipRole
|
||||
);
|
||||
@@ -238,6 +239,7 @@ export const permissionServiceFactory = ({
|
||||
validateOrgSSO(
|
||||
authMethod,
|
||||
userProjectPermission.orgAuthEnforced,
|
||||
userProjectPermission.orgGoogleSsoAuthEnforced,
|
||||
userProjectPermission.bypassOrgAuthEnabled,
|
||||
userProjectPermission.orgRole
|
||||
);
|
||||
|
@@ -36,8 +36,7 @@ export enum ProjectPermissionSecretActions {
|
||||
ReadValue = "readValue",
|
||||
Create = "create",
|
||||
Edit = "edit",
|
||||
Delete = "delete",
|
||||
Subscribe = "subscribe"
|
||||
Delete = "delete"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionCmekActions {
|
||||
@@ -158,6 +157,13 @@ export enum ProjectPermissionSecretScanningConfigActions {
|
||||
Update = "update-configs"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionSecretEventActions {
|
||||
SubscribeCreated = "subscribe-on-created",
|
||||
SubscribeUpdated = "subscribe-on-updated",
|
||||
SubscribeDeleted = "subscribe-on-deleted",
|
||||
SubscribeImportMutations = "subscribe-on-import-mutations"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionSub {
|
||||
Role = "role",
|
||||
Member = "member",
|
||||
@@ -197,7 +203,8 @@ export enum ProjectPermissionSub {
|
||||
Kmip = "kmip",
|
||||
SecretScanningDataSources = "secret-scanning-data-sources",
|
||||
SecretScanningFindings = "secret-scanning-findings",
|
||||
SecretScanningConfigs = "secret-scanning-configs"
|
||||
SecretScanningConfigs = "secret-scanning-configs",
|
||||
SecretEvents = "secret-events"
|
||||
}
|
||||
|
||||
export type SecretSubjectFields = {
|
||||
@@ -205,7 +212,13 @@ export type SecretSubjectFields = {
|
||||
secretPath: string;
|
||||
secretName?: string;
|
||||
secretTags?: string[];
|
||||
eventType?: string;
|
||||
};
|
||||
|
||||
export type SecretEventSubjectFields = {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
secretName?: string;
|
||||
secretTags?: string[];
|
||||
};
|
||||
|
||||
export type SecretFolderSubjectFields = {
|
||||
@@ -344,7 +357,11 @@ export type ProjectPermissionSet =
|
||||
| [ProjectPermissionCommitsActions, ProjectPermissionSub.Commits]
|
||||
| [ProjectPermissionSecretScanningDataSourceActions, ProjectPermissionSub.SecretScanningDataSources]
|
||||
| [ProjectPermissionSecretScanningFindingActions, ProjectPermissionSub.SecretScanningFindings]
|
||||
| [ProjectPermissionSecretScanningConfigActions, ProjectPermissionSub.SecretScanningConfigs];
|
||||
| [ProjectPermissionSecretScanningConfigActions, ProjectPermissionSub.SecretScanningConfigs]
|
||||
| [
|
||||
ProjectPermissionSecretEventActions,
|
||||
ProjectPermissionSub.SecretEvents | (ForcedSubject<ProjectPermissionSub.SecretEvents> & SecretEventSubjectFields)
|
||||
];
|
||||
|
||||
const SECRET_PATH_MISSING_SLASH_ERR_MSG = "Invalid Secret Path; it must start with a '/'";
|
||||
const SECRET_PATH_PERMISSION_OPERATOR_SCHEMA = z.union([
|
||||
@@ -877,7 +894,16 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [
|
||||
"When specified, only matching conditions will be allowed to access given resource."
|
||||
).optional()
|
||||
}),
|
||||
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.SecretEvents).describe("The entity this permission pertains to."),
|
||||
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionSecretEventActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
),
|
||||
conditions: SecretSyncConditionV2Schema.describe(
|
||||
"When specified, only matching conditions will be allowed to access given resource."
|
||||
).optional()
|
||||
}),
|
||||
...GeneralPermissionSchema
|
||||
]);
|
||||
|
||||
|
@@ -952,13 +952,39 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
if (!folder) {
|
||||
throw new NotFoundError({ message: `Folder with ID '${folderId}' not found in project with ID '${projectId}'` });
|
||||
}
|
||||
|
||||
const { secrets } = mergeStatus;
|
||||
|
||||
await secretQueueService.syncSecrets({
|
||||
projectId,
|
||||
orgId: actorOrgId,
|
||||
secretPath: folder.path,
|
||||
environmentSlug: folder.environmentSlug,
|
||||
actorId,
|
||||
actor
|
||||
actor,
|
||||
event: {
|
||||
created: secrets.created.map((el) => ({
|
||||
environment: folder.environmentSlug,
|
||||
secretPath: folder.path,
|
||||
secretId: el.id,
|
||||
// @ts-expect-error - not present on V1 secrets
|
||||
secretKey: el.key as string
|
||||
})),
|
||||
updated: secrets.updated.map((el) => ({
|
||||
environment: folder.environmentSlug,
|
||||
secretPath: folder.path,
|
||||
secretId: el.id,
|
||||
// @ts-expect-error - not present on V1 secrets
|
||||
secretKey: el.key as string
|
||||
})),
|
||||
deleted: secrets.deleted.map((el) => ({
|
||||
environment: folder.environmentSlug,
|
||||
secretPath: folder.path,
|
||||
secretId: el.id,
|
||||
// @ts-expect-error - not present on V1 secrets
|
||||
secretKey: el.key as string
|
||||
}))
|
||||
}
|
||||
});
|
||||
|
||||
if (isSoftEnforcement) {
|
||||
|
@@ -2491,6 +2491,7 @@ export const SecretSyncs = {
|
||||
},
|
||||
RENDER: {
|
||||
serviceId: "The ID of the Render service to sync secrets to.",
|
||||
environmentGroupId: "The ID of the Render environment group to sync secrets to.",
|
||||
scope: "The Render scope that secrets should be synced to.",
|
||||
type: "The Render resource type to sync secrets to."
|
||||
},
|
||||
|
@@ -20,7 +20,10 @@ export const triggerWorkflowIntegrationNotification = async (dto: TTriggerWorkfl
|
||||
const slackConfig = await projectSlackConfigDAL.getIntegrationDetailsByProject(projectId);
|
||||
|
||||
if (slackConfig) {
|
||||
if (notification.type === TriggerFeature.ACCESS_REQUEST) {
|
||||
if (
|
||||
notification.type === TriggerFeature.ACCESS_REQUEST ||
|
||||
notification.type === TriggerFeature.ACCESS_REQUEST_UPDATED
|
||||
) {
|
||||
const targetChannelIds = slackConfig.accessRequestChannels?.split(", ") || [];
|
||||
if (targetChannelIds.length && slackConfig.isAccessRequestNotificationEnabled) {
|
||||
await sendSlackNotification({
|
||||
@@ -50,7 +53,10 @@ export const triggerWorkflowIntegrationNotification = async (dto: TTriggerWorkfl
|
||||
}
|
||||
|
||||
if (microsoftTeamsConfig) {
|
||||
if (notification.type === TriggerFeature.ACCESS_REQUEST) {
|
||||
if (
|
||||
notification.type === TriggerFeature.ACCESS_REQUEST ||
|
||||
notification.type === TriggerFeature.ACCESS_REQUEST_UPDATED
|
||||
) {
|
||||
if (microsoftTeamsConfig.isAccessRequestNotificationEnabled && microsoftTeamsConfig.accessRequestChannels) {
|
||||
const { success, data } = validateMicrosoftTeamsChannelsSchema.safeParse(
|
||||
microsoftTeamsConfig.accessRequestChannels
|
||||
|
@@ -6,7 +6,8 @@ import { TProjectSlackConfigDALFactory } from "@app/services/slack/project-slack
|
||||
|
||||
export enum TriggerFeature {
|
||||
SECRET_APPROVAL = "secret-approval",
|
||||
ACCESS_REQUEST = "access-request"
|
||||
ACCESS_REQUEST = "access-request",
|
||||
ACCESS_REQUEST_UPDATED = "access-request-updated"
|
||||
}
|
||||
|
||||
export type TNotification =
|
||||
@@ -34,6 +35,22 @@ export type TNotification =
|
||||
approvalUrl: string;
|
||||
note?: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: TriggerFeature.ACCESS_REQUEST_UPDATED;
|
||||
payload: {
|
||||
requesterFullName: string;
|
||||
requesterEmail: string;
|
||||
isTemporary: boolean;
|
||||
secretPath: string;
|
||||
environment: string;
|
||||
projectName: string;
|
||||
permissions: string[];
|
||||
approvalUrl: string;
|
||||
editNote?: string;
|
||||
editorFullName?: string;
|
||||
editorEmail?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type TTriggerWorkflowNotificationDTO = {
|
||||
|
@@ -560,8 +560,7 @@ export const registerRoutes = async (
|
||||
queueService,
|
||||
projectDAL,
|
||||
licenseService,
|
||||
auditLogStreamDAL,
|
||||
eventBusService
|
||||
auditLogStreamDAL
|
||||
});
|
||||
|
||||
const auditLogService = auditLogServiceFactory({ auditLogDAL, permissionService, auditLogQueue });
|
||||
@@ -1121,7 +1120,9 @@ export const registerRoutes = async (
|
||||
resourceMetadataDAL,
|
||||
folderCommitService,
|
||||
secretSyncQueue,
|
||||
reminderService
|
||||
reminderService,
|
||||
eventBusService,
|
||||
licenseService
|
||||
});
|
||||
|
||||
const projectService = projectServiceFactory({
|
||||
|
@@ -583,16 +583,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
||||
email: z.string().email().trim(),
|
||||
password: z.string().trim(),
|
||||
firstName: z.string().trim(),
|
||||
lastName: z.string().trim().optional(),
|
||||
protectedKey: z.string().trim(),
|
||||
protectedKeyIV: z.string().trim(),
|
||||
protectedKeyTag: z.string().trim(),
|
||||
publicKey: z.string().trim(),
|
||||
encryptedPrivateKey: z.string().trim(),
|
||||
encryptedPrivateKeyIV: z.string().trim(),
|
||||
encryptedPrivateKeyTag: z.string().trim(),
|
||||
salt: z.string().trim(),
|
||||
verifier: z.string().trim()
|
||||
lastName: z.string().trim().optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
|
@@ -49,4 +49,32 @@ export const registerRenderConnectionRouter = async (server: FastifyZodProvider)
|
||||
return services;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: `/:connectionId/environment-groups`,
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
connectionId: z.string().uuid()
|
||||
}),
|
||||
response: {
|
||||
200: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string()
|
||||
})
|
||||
.array()
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { connectionId } = req.params;
|
||||
const groups = await server.services.appConnection.render.listEnvironmentGroups(connectionId, req.permission);
|
||||
|
||||
return groups;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@@ -67,7 +67,7 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
onRequest: verifyAuth([AuthMode.JWT], { requireOrg: false }),
|
||||
handler: () => ({ message: "Authenticated" as const })
|
||||
});
|
||||
|
||||
|
@@ -5,8 +5,8 @@ import { z } from "zod";
|
||||
|
||||
import { ActionProjectType, ProjectType } from "@app/db/schemas";
|
||||
import { getServerSentEventsHeaders } from "@app/ee/services/event/event-sse-stream";
|
||||
import { EventRegisterSchema } from "@app/ee/services/event/types";
|
||||
import { ProjectPermissionSecretActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { EventRegisterSchema, Mappings } from "@app/ee/services/event/types";
|
||||
import { ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { ApiDocsTags, EventSubscriptions } from "@app/lib/api-docs";
|
||||
import { BadRequestError, ForbiddenRequestError, RateLimitError } from "@app/lib/errors";
|
||||
import { readLimit } from "@app/server/config/rateLimiter";
|
||||
@@ -82,21 +82,19 @@ export const registerEventRouter = async (server: FastifyZodProvider) => {
|
||||
req.body.register.forEach((r) => {
|
||||
const fields = {
|
||||
environment: r.conditions?.environmentSlug ?? "",
|
||||
secretPath: r.conditions?.secretPath ?? "/",
|
||||
eventType: r.event
|
||||
secretPath: r.conditions?.secretPath ?? "/"
|
||||
};
|
||||
|
||||
const allowed = info.permission.can(
|
||||
ProjectPermissionSecretActions.Subscribe,
|
||||
subject(ProjectPermissionSub.Secrets, fields)
|
||||
);
|
||||
const action = Mappings.BusEventToAction(r.event);
|
||||
|
||||
const allowed = info.permission.can(action, subject(ProjectPermissionSub.SecretEvents, fields));
|
||||
|
||||
if (!allowed) {
|
||||
throw new ForbiddenRequestError({
|
||||
name: "PermissionDenied",
|
||||
message: `You are not allowed to subscribe on secrets`,
|
||||
message: `You are not allowed to subscribe on ${ProjectPermissionSub.SecretEvents}`,
|
||||
details: {
|
||||
event: fields.eventType,
|
||||
action,
|
||||
environmentSlug: fields.environment,
|
||||
secretPath: fields.secretPath
|
||||
}
|
||||
|
@@ -478,4 +478,30 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
|
||||
return { identityMemberships };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/details",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
response: {
|
||||
200: z.object({
|
||||
identityDetails: z.object({
|
||||
organization: z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
slug: z.string()
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN], { requireOrg: false }),
|
||||
handler: async (req) => {
|
||||
const organization = await server.services.org.findIdentityOrganization(req.permission.id);
|
||||
return { identityDetails: { organization } };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@@ -279,6 +279,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
||||
name: GenericResourceNameSchema.optional(),
|
||||
slug: slugSchema({ max: 64 }).optional(),
|
||||
authEnforced: z.boolean().optional(),
|
||||
googleSsoAuthEnforced: z.boolean().optional(),
|
||||
scimEnabled: z.boolean().optional(),
|
||||
defaultMembershipRoleSlug: slugSchema({ max: 64, field: "Default Membership Role" }).optional(),
|
||||
enforceMfa: z.boolean().optional(),
|
||||
|
@@ -54,6 +54,8 @@ export const registerOauthMiddlewares = (server: FastifyZodProvider) => {
|
||||
try {
|
||||
// @ts-expect-error this is because this is express type and not fastify
|
||||
const callbackPort = req.session.get("callbackPort");
|
||||
// @ts-expect-error this is because this is express type and not fastify
|
||||
const orgSlug = req.session.get("orgSlug");
|
||||
|
||||
const email = profile?.emails?.[0]?.value;
|
||||
if (!email)
|
||||
@@ -67,7 +69,8 @@ export const registerOauthMiddlewares = (server: FastifyZodProvider) => {
|
||||
firstName: profile?.name?.givenName || "",
|
||||
lastName: profile?.name?.familyName || "",
|
||||
authMethod: AuthMethod.GOOGLE,
|
||||
callbackPort
|
||||
callbackPort,
|
||||
orgSlug
|
||||
});
|
||||
cb(null, { isUserCompleted, providerAuthToken });
|
||||
} catch (error) {
|
||||
@@ -215,6 +218,7 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
callback_port: z.string().optional(),
|
||||
org_slug: z.string().optional(),
|
||||
is_admin_login: z
|
||||
.string()
|
||||
.optional()
|
||||
@@ -223,12 +227,15 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
preValidation: [
|
||||
async (req, res) => {
|
||||
const { callback_port: callbackPort, is_admin_login: isAdminLogin } = req.query;
|
||||
const { callback_port: callbackPort, is_admin_login: isAdminLogin, org_slug: orgSlug } = req.query;
|
||||
// ensure fresh session state per login attempt
|
||||
await req.session.regenerate();
|
||||
if (callbackPort) {
|
||||
req.session.set("callbackPort", callbackPort);
|
||||
}
|
||||
if (orgSlug) {
|
||||
req.session.set("orgSlug", orgSlug);
|
||||
}
|
||||
if (isAdminLogin) {
|
||||
req.session.set("isAdminLogin", isAdminLogin);
|
||||
}
|
||||
|
@@ -283,6 +283,14 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.Projects],
|
||||
description: "Get project details by slug",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
slug: slugSchema({ max: 36 }).describe("The slug of the project to get.")
|
||||
}),
|
||||
|
@@ -142,16 +142,27 @@ export const getGitHubAppAuthToken = async (appConnection: TGitHubConnection) =>
|
||||
return token;
|
||||
};
|
||||
|
||||
const parseGitHubLinkHeader = (linkHeader: string | undefined): Record<string, string> => {
|
||||
if (!linkHeader) return {};
|
||||
|
||||
const links: Record<string, string> = {};
|
||||
const segments = linkHeader.split(",");
|
||||
const re = new RE2(/<([^>]+)>;\s*rel="([^"]+)"/);
|
||||
|
||||
for (const segment of segments) {
|
||||
const match = re.exec(segment.trim());
|
||||
if (match) {
|
||||
const url = match[1];
|
||||
const rel = match[2];
|
||||
links[rel] = url;
|
||||
}
|
||||
}
|
||||
return links;
|
||||
};
|
||||
|
||||
function extractNextPageUrl(linkHeader: string | undefined): string | null {
|
||||
if (!linkHeader) return null;
|
||||
|
||||
const links = linkHeader.split(",");
|
||||
const nextLink = links.find((link) => link.includes('rel="next"'));
|
||||
|
||||
if (!nextLink) return null;
|
||||
|
||||
const match = new RE2(/<([^>]+)>/).exec(nextLink);
|
||||
return match ? match[1] : null;
|
||||
const links = parseGitHubLinkHeader(linkHeader);
|
||||
return links.next || null;
|
||||
}
|
||||
|
||||
export const makePaginatedGitHubRequest = async <T, R = T[]>(
|
||||
@@ -164,27 +175,83 @@ export const makePaginatedGitHubRequest = async <T, R = T[]>(
|
||||
|
||||
const token =
|
||||
method === GitHubConnectionMethod.OAuth ? credentials.accessToken : await getGitHubAppAuthToken(appConnection);
|
||||
let url: string | null = `https://${await getGitHubInstanceApiUrl(appConnection)}${path}`;
|
||||
|
||||
const baseUrl = `https://${await getGitHubInstanceApiUrl(appConnection)}${path}`;
|
||||
const initialUrlObj = new URL(baseUrl);
|
||||
initialUrlObj.searchParams.set("per_page", "100");
|
||||
|
||||
let results: T[] = [];
|
||||
let i = 0;
|
||||
const maxIterations = 1000;
|
||||
|
||||
while (url && i < 1000) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const response: AxiosResponse<R> = await requestWithGitHubGateway<R>(appConnection, gatewayService, {
|
||||
url,
|
||||
method: "GET",
|
||||
headers: {
|
||||
Accept: "application/vnd.github+json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
"X-GitHub-Api-Version": "2022-11-28"
|
||||
}
|
||||
});
|
||||
// Make initial request to get link header
|
||||
const firstResponse: AxiosResponse<R> = await requestWithGitHubGateway<R>(appConnection, gatewayService, {
|
||||
url: initialUrlObj.toString(),
|
||||
method: "GET",
|
||||
headers: {
|
||||
Accept: "application/vnd.github+json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
"X-GitHub-Api-Version": "2022-11-28"
|
||||
}
|
||||
});
|
||||
|
||||
const items = dataMapper ? dataMapper(response.data) : (response.data as unknown as T[]);
|
||||
results = results.concat(items);
|
||||
const firstPageItems = dataMapper ? dataMapper(firstResponse.data) : (firstResponse.data as unknown as T[]);
|
||||
results = results.concat(firstPageItems);
|
||||
|
||||
url = extractNextPageUrl(response.headers.link as string | undefined);
|
||||
i += 1;
|
||||
const linkHeader = parseGitHubLinkHeader(firstResponse.headers.link as string | undefined);
|
||||
const lastPageUrl = linkHeader.last;
|
||||
|
||||
// If there's a last page URL, get its page number and concurrently fetch every page starting from 2 to last
|
||||
if (lastPageUrl) {
|
||||
const lastPageParam = new URL(lastPageUrl).searchParams.get("page");
|
||||
const totalPages = lastPageParam ? parseInt(lastPageParam, 10) : 1;
|
||||
|
||||
const pageRequests: Promise<AxiosResponse<R>>[] = [];
|
||||
|
||||
for (let pageNum = 2; pageNum <= totalPages && pageNum - 1 < maxIterations; pageNum += 1) {
|
||||
const pageUrlObj = new URL(initialUrlObj.toString());
|
||||
pageUrlObj.searchParams.set("page", pageNum.toString());
|
||||
|
||||
pageRequests.push(
|
||||
requestWithGitHubGateway<R>(appConnection, gatewayService, {
|
||||
url: pageUrlObj.toString(),
|
||||
method: "GET",
|
||||
headers: {
|
||||
Accept: "application/vnd.github+json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
"X-GitHub-Api-Version": "2022-11-28"
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
const responses = await Promise.all(pageRequests);
|
||||
|
||||
for (const response of responses) {
|
||||
const items = dataMapper ? dataMapper(response.data) : (response.data as unknown as T[]);
|
||||
results = results.concat(items);
|
||||
}
|
||||
} else {
|
||||
// Fallback in case last link isn't present
|
||||
let url: string | null = extractNextPageUrl(firstResponse.headers.link as string | undefined);
|
||||
let i = 1;
|
||||
|
||||
while (url && i < maxIterations) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const response: AxiosResponse<R> = await requestWithGitHubGateway<R>(appConnection, gatewayService, {
|
||||
url,
|
||||
method: "GET",
|
||||
headers: {
|
||||
Accept: "application/vnd.github+json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
"X-GitHub-Api-Version": "2022-11-28"
|
||||
}
|
||||
});
|
||||
|
||||
const items = dataMapper ? dataMapper(response.data) : (response.data as unknown as T[]);
|
||||
results = results.concat(items);
|
||||
|
||||
url = extractNextPageUrl(response.headers.link as string | undefined);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
|
@@ -8,9 +8,11 @@ import { IntegrationUrls } from "@app/services/integration-auth/integration-list
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import { RenderConnectionMethod } from "./render-connection-enums";
|
||||
import {
|
||||
TRawRenderEnvironmentGroup,
|
||||
TRawRenderService,
|
||||
TRenderConnection,
|
||||
TRenderConnectionConfig,
|
||||
TRenderEnvironmentGroup,
|
||||
TRenderService
|
||||
} from "./render-connection-types";
|
||||
|
||||
@@ -32,7 +34,11 @@ export const listRenderServices = async (appConnection: TRenderConnection): Prom
|
||||
const perPage = 100;
|
||||
let cursor;
|
||||
|
||||
let maxIterations = 10;
|
||||
|
||||
while (hasMorePages) {
|
||||
if (maxIterations <= 0) break;
|
||||
|
||||
const res: TRawRenderService[] = (
|
||||
await request.get<TRawRenderService[]>(`${IntegrationUrls.RENDER_API_URL}/v1/services`, {
|
||||
params: new URLSearchParams({
|
||||
@@ -59,6 +65,8 @@ export const listRenderServices = async (appConnection: TRenderConnection): Prom
|
||||
} else {
|
||||
cursor = res[res.length - 1].cursor;
|
||||
}
|
||||
|
||||
maxIterations -= 1;
|
||||
}
|
||||
|
||||
return services;
|
||||
@@ -86,3 +94,52 @@ export const validateRenderConnectionCredentials = async (config: TRenderConnect
|
||||
|
||||
return inputCredentials;
|
||||
};
|
||||
|
||||
export const listRenderEnvironmentGroups = async (
|
||||
appConnection: TRenderConnection
|
||||
): Promise<TRenderEnvironmentGroup[]> => {
|
||||
const {
|
||||
credentials: { apiKey }
|
||||
} = appConnection;
|
||||
|
||||
const groups: TRenderEnvironmentGroup[] = [];
|
||||
let hasMorePages = true;
|
||||
const perPage = 100;
|
||||
let cursor;
|
||||
let maxIterations = 10;
|
||||
|
||||
while (hasMorePages) {
|
||||
if (maxIterations <= 0) break;
|
||||
|
||||
const res: TRawRenderEnvironmentGroup[] = (
|
||||
await request.get<TRawRenderEnvironmentGroup[]>(`${IntegrationUrls.RENDER_API_URL}/v1/env-groups`, {
|
||||
params: new URLSearchParams({
|
||||
...(cursor ? { cursor: String(cursor) } : {}),
|
||||
limit: String(perPage)
|
||||
}),
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
Accept: "application/json",
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
})
|
||||
).data;
|
||||
|
||||
res.forEach((item) => {
|
||||
groups.push({
|
||||
name: item.envGroup.name,
|
||||
id: item.envGroup.id
|
||||
});
|
||||
});
|
||||
|
||||
if (res.length < perPage) {
|
||||
hasMorePages = false;
|
||||
} else {
|
||||
cursor = res[res.length - 1].cursor;
|
||||
}
|
||||
|
||||
maxIterations -= 1;
|
||||
}
|
||||
|
||||
return groups;
|
||||
};
|
||||
|
@@ -2,7 +2,7 @@ import { logger } from "@app/lib/logger";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import { listRenderServices } from "./render-connection-fns";
|
||||
import { listRenderEnvironmentGroups, listRenderServices } from "./render-connection-fns";
|
||||
import { TRenderConnection } from "./render-connection-types";
|
||||
|
||||
type TGetAppConnectionFunc = (
|
||||
@@ -24,7 +24,20 @@ export const renderConnectionService = (getAppConnection: TGetAppConnectionFunc)
|
||||
}
|
||||
};
|
||||
|
||||
const listEnvironmentGroups = async (connectionId: string, actor: OrgServiceActor) => {
|
||||
const appConnection = await getAppConnection(AppConnection.Render, connectionId, actor);
|
||||
try {
|
||||
const groups = await listRenderEnvironmentGroups(appConnection);
|
||||
|
||||
return groups;
|
||||
} catch (error) {
|
||||
logger.error(error, "Failed to list environment groups for Render connection");
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
listServices
|
||||
listServices,
|
||||
listEnvironmentGroups
|
||||
};
|
||||
};
|
||||
|
@@ -33,3 +33,16 @@ export type TRawRenderService = {
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type TRenderEnvironmentGroup = {
|
||||
name: string;
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type TRawRenderEnvironmentGroup = {
|
||||
cursor: string;
|
||||
envGroup: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
|
@@ -448,15 +448,34 @@ export const authLoginServiceFactory = ({
|
||||
|
||||
// Check if the user actually has access to the specified organization.
|
||||
const userOrgs = await orgDAL.findAllOrgsByUserId(user.id);
|
||||
const hasOrganizationMembership = userOrgs.some((org) => org.id === organizationId && org.userStatus !== "invited");
|
||||
|
||||
const selectedOrgMembership = userOrgs.find((org) => org.id === organizationId && org.userStatus !== "invited");
|
||||
|
||||
const selectedOrg = await orgDAL.findById(organizationId);
|
||||
|
||||
if (!hasOrganizationMembership) {
|
||||
if (!selectedOrgMembership) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: `User does not have access to the organization named ${selectedOrg?.name}`
|
||||
});
|
||||
}
|
||||
|
||||
if (selectedOrg.googleSsoAuthEnforced && decodedToken.authMethod !== AuthMethod.GOOGLE) {
|
||||
const canBypass = selectedOrg.bypassOrgAuthEnabled && selectedOrgMembership.userRole === OrgMembershipRole.Admin;
|
||||
|
||||
if (!canBypass) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Google SSO is enforced for this organization. Please use Google SSO to login.",
|
||||
error: "GoogleSsoEnforced"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (decodedToken.authMethod === AuthMethod.GOOGLE) {
|
||||
await orgDAL.updateById(selectedOrg.id, {
|
||||
googleSsoAuthLastUsed: new Date()
|
||||
});
|
||||
}
|
||||
|
||||
const shouldCheckMfa = selectedOrg.enforceMfa || user.isMfaEnabled;
|
||||
const orgMfaMethod = selectedOrg.enforceMfa ? (selectedOrg.selectedMfaMethod ?? MfaMethod.EMAIL) : undefined;
|
||||
const userMfaMethod = user.isMfaEnabled ? (user.selectedMfaMethod ?? MfaMethod.EMAIL) : undefined;
|
||||
@@ -502,7 +521,8 @@ export const authLoginServiceFactory = ({
|
||||
selectedOrg.authEnforced &&
|
||||
selectedOrg.bypassOrgAuthEnabled &&
|
||||
!isAuthMethodSaml(decodedToken.authMethod) &&
|
||||
decodedToken.authMethod !== AuthMethod.OIDC
|
||||
decodedToken.authMethod !== AuthMethod.OIDC &&
|
||||
decodedToken.authMethod !== AuthMethod.GOOGLE
|
||||
) {
|
||||
await auditLogService.createAuditLog({
|
||||
orgId: organizationId,
|
||||
@@ -705,7 +725,7 @@ export const authLoginServiceFactory = ({
|
||||
/*
|
||||
* OAuth2 login for google,github, and other oauth2 provider
|
||||
* */
|
||||
const oauth2Login = async ({ email, firstName, lastName, authMethod, callbackPort }: TOauthLoginDTO) => {
|
||||
const oauth2Login = async ({ email, firstName, lastName, authMethod, callbackPort, orgSlug }: TOauthLoginDTO) => {
|
||||
// akhilmhdh: case sensitive email resolution
|
||||
const usersByUsername = await userDAL.findUserByUsername(email);
|
||||
let user = usersByUsername?.length > 1 ? usersByUsername.find((el) => el.username === email) : usersByUsername?.[0];
|
||||
@@ -759,6 +779,8 @@ export const authLoginServiceFactory = ({
|
||||
|
||||
const appCfg = getConfig();
|
||||
|
||||
let orgId = "";
|
||||
let orgName: undefined | string;
|
||||
if (!user) {
|
||||
// Create a new user based on oAuth
|
||||
if (!serverCfg?.allowSignUp) throw new BadRequestError({ message: "Sign up disabled", name: "Oauth 2 login" });
|
||||
@@ -784,7 +806,6 @@ export const authLoginServiceFactory = ({
|
||||
});
|
||||
|
||||
if (authMethod === AuthMethod.GITHUB && serverCfg.defaultAuthOrgId && !appCfg.isCloud) {
|
||||
let orgId = "";
|
||||
const defaultOrg = await orgDAL.findOrgById(serverCfg.defaultAuthOrgId);
|
||||
if (!defaultOrg) {
|
||||
throw new BadRequestError({
|
||||
@@ -824,11 +845,39 @@ export const authLoginServiceFactory = ({
|
||||
}
|
||||
}
|
||||
|
||||
if (!orgId && orgSlug) {
|
||||
const org = await orgDAL.findOrgBySlug(orgSlug);
|
||||
|
||||
if (org) {
|
||||
// checks for the membership and only sets the orgId / orgName if the user is a member of the specified org
|
||||
const orgMembership = await orgDAL.findMembership({
|
||||
[`${TableName.OrgMembership}.userId` as "userId"]: user.id,
|
||||
[`${TableName.OrgMembership}.orgId` as "orgId"]: org.id,
|
||||
[`${TableName.OrgMembership}.isActive` as "isActive"]: true,
|
||||
[`${TableName.OrgMembership}.status` as "status"]: OrgMembershipStatus.Accepted
|
||||
});
|
||||
|
||||
if (orgMembership) {
|
||||
orgId = org.id;
|
||||
orgName = org.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const isUserCompleted = user.isAccepted;
|
||||
const providerAuthToken = crypto.jwt().sign(
|
||||
{
|
||||
authTokenType: AuthTokenType.PROVIDER_TOKEN,
|
||||
userId: user.id,
|
||||
|
||||
...(orgId && orgSlug && orgName !== undefined
|
||||
? {
|
||||
organizationId: orgId,
|
||||
organizationName: orgName,
|
||||
organizationSlug: orgSlug
|
||||
}
|
||||
: {}),
|
||||
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
isEmailVerified: user.isEmailVerified,
|
||||
|
@@ -32,6 +32,7 @@ export type TOauthLoginDTO = {
|
||||
lastName?: string;
|
||||
authMethod: AuthMethod;
|
||||
callbackPort?: string;
|
||||
orgSlug?: string;
|
||||
};
|
||||
|
||||
export type TOauthTokenExchangeDTO = {
|
||||
|
@@ -462,6 +462,54 @@ export const buildTeamsPayload = (notification: TNotification) => {
|
||||
};
|
||||
}
|
||||
|
||||
case TriggerFeature.ACCESS_REQUEST_UPDATED: {
|
||||
const { payload } = notification;
|
||||
|
||||
const adaptiveCard = {
|
||||
type: "AdaptiveCard",
|
||||
$schema: "http://adaptivecards.io/schemas/adaptive-card.json",
|
||||
version: "1.5",
|
||||
body: [
|
||||
{
|
||||
type: "TextBlock",
|
||||
text: "Updated access approval request pending for review",
|
||||
weight: "Bolder",
|
||||
size: "Large"
|
||||
},
|
||||
{
|
||||
type: "TextBlock",
|
||||
text: `${payload.editorFullName} (${payload.editorEmail}) has updated the ${
|
||||
payload.isTemporary ? "temporary" : "permanent"
|
||||
} access request from ${payload.requesterFullName} (${payload.requesterEmail}) to ${payload.secretPath} in the ${payload.environment} environment of ${payload.projectName}.`,
|
||||
wrap: true
|
||||
},
|
||||
{
|
||||
type: "TextBlock",
|
||||
text: `The following permissions are requested: ${payload.permissions.join(", ")}`,
|
||||
wrap: true
|
||||
},
|
||||
payload.editNote
|
||||
? {
|
||||
type: "TextBlock",
|
||||
text: `**Editor Note**: ${payload.editNote}`,
|
||||
wrap: true
|
||||
}
|
||||
: null
|
||||
].filter(Boolean),
|
||||
actions: [
|
||||
{
|
||||
type: "Action.OpenUrl",
|
||||
title: "View request in Infisical",
|
||||
url: payload.approvalUrl
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
return {
|
||||
adaptiveCard
|
||||
};
|
||||
}
|
||||
|
||||
default: {
|
||||
throw new BadRequestError({
|
||||
message: "Teams notification type not supported."
|
||||
|
@@ -630,6 +630,25 @@ export const orgDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const findIdentityOrganization = async (
|
||||
identityId: string
|
||||
): Promise<{ id: string; name: string; slug: string; role: string }> => {
|
||||
try {
|
||||
const org = await db
|
||||
.replicaNode()(TableName.IdentityOrgMembership)
|
||||
.where({ identityId })
|
||||
.join(TableName.Organization, `${TableName.IdentityOrgMembership}.orgId`, `${TableName.Organization}.id`)
|
||||
.select(db.ref("id").withSchema(TableName.Organization).as("id"))
|
||||
.select(db.ref("name").withSchema(TableName.Organization).as("name"))
|
||||
.select(db.ref("slug").withSchema(TableName.Organization).as("slug"))
|
||||
.select(db.ref("role").withSchema(TableName.IdentityOrgMembership).as("role"));
|
||||
|
||||
return org?.[0];
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find identity organization" });
|
||||
}
|
||||
};
|
||||
|
||||
return withTransaction(db, {
|
||||
...orgOrm,
|
||||
findOrgByProjectId,
|
||||
@@ -652,6 +671,7 @@ export const orgDALFactory = (db: TDbClient) => {
|
||||
updateMembershipById,
|
||||
deleteMembershipById,
|
||||
deleteMembershipsById,
|
||||
updateMembership
|
||||
updateMembership,
|
||||
findIdentityOrganization
|
||||
});
|
||||
};
|
||||
|
@@ -8,6 +8,7 @@ export const sanitizedOrganizationSchema = OrganizationsSchema.pick({
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
authEnforced: true,
|
||||
googleSsoAuthEnforced: true,
|
||||
scimEnabled: true,
|
||||
kmsDefaultKeyId: true,
|
||||
defaultMembershipRole: true,
|
||||
|
@@ -198,6 +198,15 @@ export const orgServiceFactory = ({
|
||||
// Filter out orgs where the membership object is an invitation
|
||||
return orgs.filter((org) => org.userStatus !== "invited");
|
||||
};
|
||||
|
||||
/*
|
||||
* Get all organization an identity is part of
|
||||
* */
|
||||
const findIdentityOrganization = async (identityId: string) => {
|
||||
const org = await orgDAL.findIdentityOrganization(identityId);
|
||||
|
||||
return org;
|
||||
};
|
||||
/*
|
||||
* Get all workspace members
|
||||
* */
|
||||
@@ -355,6 +364,7 @@ export const orgServiceFactory = ({
|
||||
name,
|
||||
slug,
|
||||
authEnforced,
|
||||
googleSsoAuthEnforced,
|
||||
scimEnabled,
|
||||
defaultMembershipRoleSlug,
|
||||
enforceMfa,
|
||||
@@ -421,6 +431,21 @@ export const orgServiceFactory = ({
|
||||
}
|
||||
}
|
||||
|
||||
if (googleSsoAuthEnforced !== undefined) {
|
||||
if (!plan.enforceGoogleSSO) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to enforce Google SSO due to plan restriction. Upgrade plan to enforce Google SSO."
|
||||
});
|
||||
}
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso);
|
||||
}
|
||||
|
||||
if (authEnforced && googleSsoAuthEnforced) {
|
||||
throw new BadRequestError({
|
||||
message: "SAML/OIDC auth enforcement and Google SSO auth enforcement cannot be enabled at the same time."
|
||||
});
|
||||
}
|
||||
|
||||
if (authEnforced) {
|
||||
const samlCfg = await samlConfigDAL.findOne({
|
||||
orgId,
|
||||
@@ -451,6 +476,21 @@ export const orgServiceFactory = ({
|
||||
}
|
||||
}
|
||||
|
||||
if (googleSsoAuthEnforced) {
|
||||
if (googleSsoAuthEnforced && currentOrg.authEnforced) {
|
||||
throw new BadRequestError({
|
||||
message: "Google SSO auth enforcement cannot be enabled when SAML/OIDC auth enforcement is enabled."
|
||||
});
|
||||
}
|
||||
|
||||
if (!currentOrg.googleSsoAuthLastUsed) {
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Google SSO auth enforcement cannot be enabled because Google SSO has not been used yet. Please log in via Google SSO at least once before enforcing it for your organization."
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let defaultMembershipRole: string | undefined;
|
||||
if (defaultMembershipRoleSlug) {
|
||||
defaultMembershipRole = await getDefaultOrgMembershipRoleForUpdateOrg({
|
||||
@@ -465,6 +505,7 @@ export const orgServiceFactory = ({
|
||||
name,
|
||||
slug: slug ? slugify(slug) : undefined,
|
||||
authEnforced,
|
||||
googleSsoAuthEnforced,
|
||||
scimEnabled,
|
||||
defaultMembershipRole,
|
||||
enforceMfa,
|
||||
@@ -1403,6 +1444,7 @@ export const orgServiceFactory = ({
|
||||
findOrganizationById,
|
||||
findAllOrgMembers,
|
||||
findAllOrganizationOfUser,
|
||||
findIdentityOrganization,
|
||||
inviteUserToOrganization,
|
||||
verifyUserToOrg,
|
||||
updateOrg,
|
||||
|
@@ -74,6 +74,7 @@ export type TUpdateOrgDTO = {
|
||||
name: string;
|
||||
slug: string;
|
||||
authEnforced: boolean;
|
||||
googleSsoAuthEnforced: boolean;
|
||||
scimEnabled: boolean;
|
||||
defaultMembershipRoleSlug: string;
|
||||
enforceMfa: boolean;
|
||||
|
@@ -177,6 +177,18 @@ export const projectEnvServiceFactory = ({
|
||||
}
|
||||
}
|
||||
|
||||
const envs = await projectEnvDAL.find({ projectId });
|
||||
const project = await projectDAL.findById(projectId);
|
||||
const plan = await licenseService.getPlan(project.orgId);
|
||||
if (plan.environmentLimit !== null && envs.length > plan.environmentLimit) {
|
||||
// case: limit imposed on number of environments allowed
|
||||
// case: number of environments used exceeds the number of environments allowed
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Failed to update environment due to environment limit exceeded. To update an environment, please upgrade your plan or remove unused environments."
|
||||
});
|
||||
}
|
||||
|
||||
const env = await projectEnvDAL.transaction(async (tx) => {
|
||||
if (position) {
|
||||
const existingEnvWithPosition = await projectEnvDAL.findOne({ projectId, position }, tx);
|
||||
|
@@ -181,7 +181,13 @@ export const secretImportServiceFactory = ({
|
||||
projectId,
|
||||
environmentSlug: environment,
|
||||
actorId,
|
||||
actor
|
||||
actor,
|
||||
event: {
|
||||
importMutation: {
|
||||
secretPath,
|
||||
environment
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -356,7 +362,13 @@ export const secretImportServiceFactory = ({
|
||||
projectId,
|
||||
environmentSlug: environment,
|
||||
actor,
|
||||
actorId
|
||||
actorId,
|
||||
event: {
|
||||
importMutation: {
|
||||
secretPath,
|
||||
environment
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await secretV2BridgeDAL.invalidateSecretCacheByProjectId(projectId);
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import AWS, { AWSError } from "aws-sdk";
|
||||
import handlebars from "handlebars";
|
||||
|
||||
import { getAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-fns";
|
||||
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
|
||||
@@ -34,18 +35,51 @@ const sleep = async () =>
|
||||
setTimeout(resolve, 1000);
|
||||
});
|
||||
|
||||
const getParametersByPath = async (ssm: AWS.SSM, path: string): Promise<TAWSParameterStoreRecord> => {
|
||||
const getFullPath = ({ path, keySchema, environment }: { path: string; keySchema?: string; environment: string }) => {
|
||||
if (!keySchema || !keySchema.includes("/")) return path;
|
||||
|
||||
if (keySchema.startsWith("/")) {
|
||||
throw new SecretSyncError({ message: `Key schema cannot contain leading '/'`, shouldRetry: false });
|
||||
}
|
||||
|
||||
const keySchemaSegments = handlebars
|
||||
.compile(keySchema)({
|
||||
environment,
|
||||
secretKey: "{{secretKey}}"
|
||||
})
|
||||
.split("/");
|
||||
|
||||
const pathSegments = keySchemaSegments.slice(0, keySchemaSegments.length - 1);
|
||||
|
||||
if (pathSegments.some((segment) => segment.includes("{{secretKey}}"))) {
|
||||
throw new SecretSyncError({
|
||||
message: "Key schema cannot contain '/' after {{secretKey}}",
|
||||
shouldRetry: false
|
||||
});
|
||||
}
|
||||
|
||||
return `${path}${pathSegments.join("/")}/`;
|
||||
};
|
||||
|
||||
const getParametersByPath = async (
|
||||
ssm: AWS.SSM,
|
||||
path: string,
|
||||
keySchema: string | undefined,
|
||||
environment: string
|
||||
): Promise<TAWSParameterStoreRecord> => {
|
||||
const awsParameterStoreSecretsRecord: TAWSParameterStoreRecord = {};
|
||||
let hasNext = true;
|
||||
let nextToken: string | undefined;
|
||||
let attempt = 0;
|
||||
|
||||
const fullPath = getFullPath({ path, keySchema, environment });
|
||||
|
||||
while (hasNext) {
|
||||
try {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const parameters = await ssm
|
||||
.getParametersByPath({
|
||||
Path: path,
|
||||
Path: fullPath,
|
||||
Recursive: false,
|
||||
WithDecryption: true,
|
||||
MaxResults: BATCH_SIZE,
|
||||
@@ -59,7 +93,7 @@ const getParametersByPath = async (ssm: AWS.SSM, path: string): Promise<TAWSPara
|
||||
parameters.Parameters.forEach((parameter) => {
|
||||
if (parameter.Name) {
|
||||
// no leading slash if path is '/'
|
||||
const secKey = path.length > 1 ? parameter.Name.substring(path.length) : parameter.Name;
|
||||
const secKey = fullPath.length > 1 ? parameter.Name.substring(path.length) : parameter.Name;
|
||||
awsParameterStoreSecretsRecord[secKey] = parameter;
|
||||
}
|
||||
});
|
||||
@@ -83,12 +117,19 @@ const getParametersByPath = async (ssm: AWS.SSM, path: string): Promise<TAWSPara
|
||||
return awsParameterStoreSecretsRecord;
|
||||
};
|
||||
|
||||
const getParameterMetadataByPath = async (ssm: AWS.SSM, path: string): Promise<TAWSParameterStoreMetadataRecord> => {
|
||||
const getParameterMetadataByPath = async (
|
||||
ssm: AWS.SSM,
|
||||
path: string,
|
||||
keySchema: string | undefined,
|
||||
environment: string
|
||||
): Promise<TAWSParameterStoreMetadataRecord> => {
|
||||
const awsParameterStoreMetadataRecord: TAWSParameterStoreMetadataRecord = {};
|
||||
let hasNext = true;
|
||||
let nextToken: string | undefined;
|
||||
let attempt = 0;
|
||||
|
||||
const fullPath = getFullPath({ path, keySchema, environment });
|
||||
|
||||
while (hasNext) {
|
||||
try {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
@@ -100,7 +141,7 @@ const getParameterMetadataByPath = async (ssm: AWS.SSM, path: string): Promise<T
|
||||
{
|
||||
Key: "Path",
|
||||
Option: "OneLevel",
|
||||
Values: [path]
|
||||
Values: [fullPath]
|
||||
}
|
||||
]
|
||||
})
|
||||
@@ -112,7 +153,7 @@ const getParameterMetadataByPath = async (ssm: AWS.SSM, path: string): Promise<T
|
||||
parameters.Parameters.forEach((parameter) => {
|
||||
if (parameter.Name) {
|
||||
// no leading slash if path is '/'
|
||||
const secKey = path.length > 1 ? parameter.Name.substring(path.length) : parameter.Name;
|
||||
const secKey = fullPath.length > 1 ? parameter.Name.substring(path.length) : parameter.Name;
|
||||
awsParameterStoreMetadataRecord[secKey] = parameter;
|
||||
}
|
||||
});
|
||||
@@ -298,9 +339,19 @@ export const AwsParameterStoreSyncFns = {
|
||||
|
||||
const ssm = await getSSM(secretSync);
|
||||
|
||||
const awsParameterStoreSecretsRecord = await getParametersByPath(ssm, destinationConfig.path);
|
||||
const awsParameterStoreSecretsRecord = await getParametersByPath(
|
||||
ssm,
|
||||
destinationConfig.path,
|
||||
syncOptions.keySchema,
|
||||
environment!.slug
|
||||
);
|
||||
|
||||
const awsParameterStoreMetadataRecord = await getParameterMetadataByPath(ssm, destinationConfig.path);
|
||||
const awsParameterStoreMetadataRecord = await getParameterMetadataByPath(
|
||||
ssm,
|
||||
destinationConfig.path,
|
||||
syncOptions.keySchema,
|
||||
environment!.slug
|
||||
);
|
||||
|
||||
const { shouldManageTags, awsParameterStoreTagsRecord } = await getParameterStoreTagsRecord(
|
||||
ssm,
|
||||
@@ -400,22 +451,32 @@ export const AwsParameterStoreSyncFns = {
|
||||
await deleteParametersBatch(ssm, parametersToDelete);
|
||||
},
|
||||
getSecrets: async (secretSync: TAwsParameterStoreSyncWithCredentials): Promise<TSecretMap> => {
|
||||
const { destinationConfig } = secretSync;
|
||||
const { destinationConfig, syncOptions, environment } = secretSync;
|
||||
|
||||
const ssm = await getSSM(secretSync);
|
||||
|
||||
const awsParameterStoreSecretsRecord = await getParametersByPath(ssm, destinationConfig.path);
|
||||
const awsParameterStoreSecretsRecord = await getParametersByPath(
|
||||
ssm,
|
||||
destinationConfig.path,
|
||||
syncOptions.keySchema,
|
||||
environment!.slug
|
||||
);
|
||||
|
||||
return Object.fromEntries(
|
||||
Object.entries(awsParameterStoreSecretsRecord).map(([key, value]) => [key, { value: value.Value ?? "" }])
|
||||
);
|
||||
},
|
||||
removeSecrets: async (secretSync: TAwsParameterStoreSyncWithCredentials, secretMap: TSecretMap) => {
|
||||
const { destinationConfig } = secretSync;
|
||||
const { destinationConfig, syncOptions, environment } = secretSync;
|
||||
|
||||
const ssm = await getSSM(secretSync);
|
||||
|
||||
const awsParameterStoreSecretsRecord = await getParametersByPath(ssm, destinationConfig.path);
|
||||
const awsParameterStoreSecretsRecord = await getParametersByPath(
|
||||
ssm,
|
||||
destinationConfig.path,
|
||||
syncOptions.keySchema,
|
||||
environment!.slug
|
||||
);
|
||||
|
||||
const parametersToDelete: AWS.SSM.Parameter[] = [];
|
||||
|
||||
|
@@ -1,5 +1,6 @@
|
||||
export enum RenderSyncScope {
|
||||
Service = "service"
|
||||
Service = "service",
|
||||
EnvironmentGroup = "environment-group"
|
||||
}
|
||||
|
||||
export enum RenderSyncType {
|
||||
|
@@ -1,11 +1,13 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import { isAxiosError } from "axios";
|
||||
import { AxiosRequestConfig, isAxiosError } from "axios";
|
||||
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
||||
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
|
||||
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
import { RenderSyncScope } from "./render-sync-enums";
|
||||
import { TRenderSecret, TRenderSyncWithCredentials } from "./render-sync-types";
|
||||
|
||||
const MAX_RETRIES = 5;
|
||||
@@ -27,6 +29,80 @@ const makeRequestWithRetry = async <T>(requestFn: () => Promise<T>, attempt = 0)
|
||||
}
|
||||
};
|
||||
|
||||
async function getSecrets(input: { destination: TRenderSyncWithCredentials["destinationConfig"]; token: string }) {
|
||||
const req: AxiosRequestConfig = {
|
||||
baseURL: `${IntegrationUrls.RENDER_API_URL}/v1`,
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${input.token}`,
|
||||
Accept: "application/json"
|
||||
}
|
||||
};
|
||||
|
||||
switch (input.destination.scope) {
|
||||
case RenderSyncScope.Service: {
|
||||
req.url = `/services/${input.destination.serviceId}/env-vars`;
|
||||
|
||||
const allSecrets: TRenderSecret[] = [];
|
||||
let cursor: string | undefined;
|
||||
|
||||
do {
|
||||
// eslint-disable-next-line @typescript-eslint/no-loop-func
|
||||
const { data } = await makeRequestWithRetry(() =>
|
||||
request.request<
|
||||
{
|
||||
envVar: {
|
||||
key: string;
|
||||
value: string;
|
||||
};
|
||||
cursor: string;
|
||||
}[]
|
||||
>({
|
||||
...req,
|
||||
params: {
|
||||
cursor
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const secrets = data.map((item) => ({
|
||||
key: item.envVar.key,
|
||||
value: item.envVar.value
|
||||
}));
|
||||
|
||||
allSecrets.push(...secrets);
|
||||
|
||||
if (data.length > 0 && data[data.length - 1]?.cursor) {
|
||||
cursor = data[data.length - 1].cursor;
|
||||
} else {
|
||||
cursor = undefined;
|
||||
}
|
||||
} while (cursor);
|
||||
|
||||
return allSecrets;
|
||||
}
|
||||
case RenderSyncScope.EnvironmentGroup: {
|
||||
req.url = `/env-groups/${input.destination.environmentGroupId}`;
|
||||
|
||||
const res = await makeRequestWithRetry(() =>
|
||||
request.request<{
|
||||
envVars: {
|
||||
key: string;
|
||||
value: string;
|
||||
}[];
|
||||
}>(req)
|
||||
);
|
||||
|
||||
return res.data.envVars.map((item) => ({
|
||||
key: item.key,
|
||||
value: item.value
|
||||
}));
|
||||
}
|
||||
default:
|
||||
throw new BadRequestError({ message: "Unknown render sync destination scope" });
|
||||
}
|
||||
}
|
||||
|
||||
const getRenderEnvironmentSecrets = async (secretSync: TRenderSyncWithCredentials): Promise<TRenderSecret[]> => {
|
||||
const {
|
||||
destinationConfig,
|
||||
@@ -35,45 +111,12 @@ const getRenderEnvironmentSecrets = async (secretSync: TRenderSyncWithCredential
|
||||
}
|
||||
} = secretSync;
|
||||
|
||||
const baseUrl = `${IntegrationUrls.RENDER_API_URL}/v1/services/${destinationConfig.serviceId}/env-vars`;
|
||||
const allSecrets: TRenderSecret[] = [];
|
||||
let cursor: string | undefined;
|
||||
const secrets = await getSecrets({
|
||||
destination: destinationConfig,
|
||||
token: apiKey
|
||||
});
|
||||
|
||||
do {
|
||||
const url = cursor ? `${baseUrl}?cursor=${cursor}` : baseUrl;
|
||||
|
||||
const { data } = await makeRequestWithRetry(() =>
|
||||
request.get<
|
||||
{
|
||||
envVar: {
|
||||
key: string;
|
||||
value: string;
|
||||
};
|
||||
cursor: string;
|
||||
}[]
|
||||
>(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
Accept: "application/json"
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const secrets = data.map((item) => ({
|
||||
key: item.envVar.key,
|
||||
value: item.envVar.value
|
||||
}));
|
||||
|
||||
allSecrets.push(...secrets);
|
||||
|
||||
if (data.length > 0 && data[data.length - 1]?.cursor) {
|
||||
cursor = data[data.length - 1].cursor;
|
||||
} else {
|
||||
cursor = undefined;
|
||||
}
|
||||
} while (cursor);
|
||||
|
||||
return allSecrets;
|
||||
return secrets;
|
||||
};
|
||||
|
||||
const batchUpdateEnvironmentSecrets = async (
|
||||
@@ -87,14 +130,91 @@ const batchUpdateEnvironmentSecrets = async (
|
||||
}
|
||||
} = secretSync;
|
||||
|
||||
await makeRequestWithRetry(() =>
|
||||
request.put(`${IntegrationUrls.RENDER_API_URL}/v1/services/${destinationConfig.serviceId}/env-vars`, envVars, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
Accept: "application/json"
|
||||
const req: AxiosRequestConfig = {
|
||||
baseURL: `${IntegrationUrls.RENDER_API_URL}/v1`,
|
||||
method: "PUT",
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
Accept: "application/json"
|
||||
}
|
||||
};
|
||||
|
||||
switch (destinationConfig.scope) {
|
||||
case RenderSyncScope.Service: {
|
||||
await makeRequestWithRetry(() =>
|
||||
request.request({
|
||||
...req,
|
||||
url: `/services/${destinationConfig.serviceId}/env-vars`,
|
||||
data: envVars
|
||||
})
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
case RenderSyncScope.EnvironmentGroup: {
|
||||
for await (const variable of envVars) {
|
||||
await makeRequestWithRetry(() =>
|
||||
request.request({
|
||||
...req,
|
||||
url: `/env-groups/${destinationConfig.environmentGroupId}/env-vars/${variable.key}`,
|
||||
data: {
|
||||
value: variable.value
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
throw new BadRequestError({ message: "Unknown render sync destination scope" });
|
||||
}
|
||||
};
|
||||
|
||||
const deleteEnvironmentSecret = async (
|
||||
secretSync: TRenderSyncWithCredentials,
|
||||
envVar: { key: string; value: string }
|
||||
): Promise<void> => {
|
||||
const {
|
||||
destinationConfig,
|
||||
connection: {
|
||||
credentials: { apiKey }
|
||||
}
|
||||
} = secretSync;
|
||||
|
||||
const req: AxiosRequestConfig = {
|
||||
baseURL: `${IntegrationUrls.RENDER_API_URL}/v1`,
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
Accept: "application/json"
|
||||
}
|
||||
};
|
||||
|
||||
switch (destinationConfig.scope) {
|
||||
case RenderSyncScope.Service: {
|
||||
await makeRequestWithRetry(() =>
|
||||
request.request({
|
||||
...req,
|
||||
url: `/services/${destinationConfig.serviceId}/env-vars/${envVar.key}`
|
||||
})
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
case RenderSyncScope.EnvironmentGroup: {
|
||||
await makeRequestWithRetry(() =>
|
||||
request.request({
|
||||
...req,
|
||||
url: `/env-groups/${destinationConfig.environmentGroupId}/env-vars/${envVar.key}`
|
||||
})
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
throw new BadRequestError({ message: "Unknown render sync destination scope" });
|
||||
}
|
||||
};
|
||||
|
||||
const redeployService = async (secretSync: TRenderSyncWithCredentials) => {
|
||||
@@ -105,18 +225,50 @@ const redeployService = async (secretSync: TRenderSyncWithCredentials) => {
|
||||
}
|
||||
} = secretSync;
|
||||
|
||||
await makeRequestWithRetry(() =>
|
||||
request.post(
|
||||
`${IntegrationUrls.RENDER_API_URL}/v1/services/${destinationConfig.serviceId}/deploys`,
|
||||
{},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
Accept: "application/json"
|
||||
}
|
||||
const req: AxiosRequestConfig = {
|
||||
baseURL: `${IntegrationUrls.RENDER_API_URL}/v1`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
Accept: "application/json"
|
||||
}
|
||||
};
|
||||
|
||||
switch (destinationConfig.scope) {
|
||||
case RenderSyncScope.Service: {
|
||||
await makeRequestWithRetry(() =>
|
||||
request.request({
|
||||
...req,
|
||||
method: "POST",
|
||||
url: `/services/${destinationConfig.serviceId}/deploys`,
|
||||
data: {}
|
||||
})
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
case RenderSyncScope.EnvironmentGroup: {
|
||||
const { data } = await request.request<{ serviceLinks: { id: string }[] }>({
|
||||
...req,
|
||||
method: "GET",
|
||||
url: `/env-groups/${destinationConfig.environmentGroupId}`
|
||||
});
|
||||
|
||||
for await (const link of data.serviceLinks) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-loop-func
|
||||
await makeRequestWithRetry(() =>
|
||||
request.request({
|
||||
...req,
|
||||
url: `/services/${link.id}/deploys`,
|
||||
data: {}
|
||||
})
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
throw new BadRequestError({ message: "Unknown render sync destination scope" });
|
||||
}
|
||||
};
|
||||
|
||||
export const RenderSyncFns = {
|
||||
@@ -169,14 +321,15 @@ export const RenderSyncFns = {
|
||||
const finalEnvVars: Array<{ key: string; value: string }> = [];
|
||||
|
||||
for (const renderSecret of renderSecrets) {
|
||||
if (!(renderSecret.key in secretMap)) {
|
||||
if (renderSecret.key in secretMap) {
|
||||
finalEnvVars.push({
|
||||
key: renderSecret.key,
|
||||
value: renderSecret.value
|
||||
});
|
||||
}
|
||||
}
|
||||
await batchUpdateEnvironmentSecrets(secretSync, finalEnvVars);
|
||||
|
||||
await Promise.all(finalEnvVars.map((el) => deleteEnvironmentSecret(secretSync, el)));
|
||||
|
||||
if (secretSync.syncOptions.autoRedeployServices) {
|
||||
await redeployService(secretSync);
|
||||
|
@@ -17,6 +17,14 @@ const RenderSyncDestinationConfigSchema = z.discriminatedUnion("scope", [
|
||||
scope: z.literal(RenderSyncScope.Service).describe(SecretSyncs.DESTINATION_CONFIG.RENDER.scope),
|
||||
serviceId: z.string().min(1, "Service ID is required").describe(SecretSyncs.DESTINATION_CONFIG.RENDER.serviceId),
|
||||
type: z.nativeEnum(RenderSyncType).describe(SecretSyncs.DESTINATION_CONFIG.RENDER.type)
|
||||
}),
|
||||
z.object({
|
||||
scope: z.literal(RenderSyncScope.EnvironmentGroup).describe(SecretSyncs.DESTINATION_CONFIG.RENDER.scope),
|
||||
environmentGroupId: z
|
||||
.string()
|
||||
.min(1, "Environment Group ID is required")
|
||||
.describe(SecretSyncs.DESTINATION_CONFIG.RENDER.environmentGroupId),
|
||||
type: z.nativeEnum(RenderSyncType).describe(SecretSyncs.DESTINATION_CONFIG.RENDER.type)
|
||||
})
|
||||
]);
|
||||
|
||||
|
@@ -386,7 +386,15 @@ export const secretV2BridgeServiceFactory = ({
|
||||
actorId,
|
||||
actor,
|
||||
projectId,
|
||||
environmentSlug: folder.environment.slug
|
||||
environmentSlug: folder.environment.slug,
|
||||
event: {
|
||||
created: {
|
||||
secretId: secret.id,
|
||||
environment: folder.environment.slug,
|
||||
secretKey: secret.key,
|
||||
secretPath
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -616,7 +624,15 @@ export const secretV2BridgeServiceFactory = ({
|
||||
actor,
|
||||
projectId,
|
||||
orgId: actorOrgId,
|
||||
environmentSlug: folder.environment.slug
|
||||
environmentSlug: folder.environment.slug,
|
||||
event: {
|
||||
updated: {
|
||||
secretId: secret.id,
|
||||
environment: folder.environment.slug,
|
||||
secretKey: secret.key,
|
||||
secretPath
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -728,7 +744,15 @@ export const secretV2BridgeServiceFactory = ({
|
||||
actor,
|
||||
projectId,
|
||||
orgId: actorOrgId,
|
||||
environmentSlug: folder.environment.slug
|
||||
environmentSlug: folder.environment.slug,
|
||||
event: {
|
||||
deleted: {
|
||||
secretId: secretToDelete.id,
|
||||
environment: folder.environment.slug,
|
||||
secretKey: secretToDelete.key,
|
||||
secretPath
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1708,7 +1732,15 @@ export const secretV2BridgeServiceFactory = ({
|
||||
secretPath,
|
||||
projectId,
|
||||
orgId: actorOrgId,
|
||||
environmentSlug: folder.environment.slug
|
||||
environmentSlug: folder.environment.slug,
|
||||
event: {
|
||||
created: newSecrets.map((el) => ({
|
||||
secretId: el.id,
|
||||
secretKey: el.key,
|
||||
secretPath,
|
||||
environment: folder.environment.slug
|
||||
}))
|
||||
}
|
||||
});
|
||||
|
||||
return newSecrets.map((el) => {
|
||||
@@ -2075,7 +2107,15 @@ export const secretV2BridgeServiceFactory = ({
|
||||
secretPath: el.path,
|
||||
projectId,
|
||||
orgId: actorOrgId,
|
||||
environmentSlug: environment
|
||||
environmentSlug: environment,
|
||||
event: {
|
||||
updated: updatedSecrets.map((sec) => ({
|
||||
secretId: sec.id,
|
||||
secretKey: sec.key,
|
||||
secretPath: sec.secretPath,
|
||||
environment
|
||||
}))
|
||||
}
|
||||
})
|
||||
: undefined
|
||||
)
|
||||
@@ -2214,7 +2254,15 @@ export const secretV2BridgeServiceFactory = ({
|
||||
secretPath,
|
||||
projectId,
|
||||
orgId: actorOrgId,
|
||||
environmentSlug: folder.environment.slug
|
||||
environmentSlug: folder.environment.slug,
|
||||
event: {
|
||||
deleted: secretsDeleted.map((el) => ({
|
||||
secretId: el.id,
|
||||
secretKey: el.key,
|
||||
secretPath,
|
||||
environment: folder.environment.slug
|
||||
}))
|
||||
}
|
||||
});
|
||||
|
||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
@@ -2751,7 +2799,13 @@ export const secretV2BridgeServiceFactory = ({
|
||||
secretPath: destinationFolder.path,
|
||||
environmentSlug: destinationFolder.environment.slug,
|
||||
actorId,
|
||||
actor
|
||||
actor,
|
||||
event: {
|
||||
importMutation: {
|
||||
secretPath: sourceFolder.path,
|
||||
environment: sourceFolder.environment.slug
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2763,7 +2817,13 @@ export const secretV2BridgeServiceFactory = ({
|
||||
secretPath: sourceFolder.path,
|
||||
environmentSlug: sourceFolder.environment.slug,
|
||||
actorId,
|
||||
actor
|
||||
actor,
|
||||
event: {
|
||||
importMutation: {
|
||||
secretPath: sourceFolder.path,
|
||||
environment: sourceFolder.environment.slug
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -5,6 +5,7 @@ import { Knex } from "knex";
|
||||
|
||||
import {
|
||||
ProjectMembershipRole,
|
||||
ProjectType,
|
||||
ProjectUpgradeStatus,
|
||||
ProjectVersion,
|
||||
SecretType,
|
||||
@@ -12,6 +13,9 @@ import {
|
||||
TSecretVersionsV2
|
||||
} from "@app/db/schemas";
|
||||
import { Actor, EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { TEventBusService } from "@app/ee/services/event/event-bus-service";
|
||||
import { BusEventName, PublishableEvent, TopicName } from "@app/ee/services/event/types";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal";
|
||||
import { TSecretRotationDALFactory } from "@app/ee/services/secret-rotation/secret-rotation-dal";
|
||||
import { TSnapshotDALFactory } from "@app/ee/services/secret-snapshot/snapshot-dal";
|
||||
@@ -111,6 +115,8 @@ type TSecretQueueFactoryDep = {
|
||||
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
|
||||
secretSyncQueue: Pick<TSecretSyncQueueFactory, "queueSecretSyncsSyncSecretsByPath">;
|
||||
reminderService: Pick<TReminderServiceFactory, "createReminderInternal" | "deleteReminderBySecretId">;
|
||||
eventBusService: TEventBusService;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
};
|
||||
|
||||
export type TGetSecrets = {
|
||||
@@ -172,7 +178,9 @@ export const secretQueueFactory = ({
|
||||
resourceMetadataDAL,
|
||||
secretSyncQueue,
|
||||
folderCommitService,
|
||||
reminderService
|
||||
reminderService,
|
||||
eventBusService,
|
||||
licenseService
|
||||
}: TSecretQueueFactoryDep) => {
|
||||
const integrationMeter = opentelemetry.metrics.getMeter("Integrations");
|
||||
const errorHistogram = integrationMeter.createHistogram("integration_secret_sync_errors", {
|
||||
@@ -534,17 +542,70 @@ export const secretQueueFactory = ({
|
||||
});
|
||||
};
|
||||
|
||||
const publishEvents = async (event: PublishableEvent) => {
|
||||
if (event.created) {
|
||||
await eventBusService.publish(TopicName.CoreServers, {
|
||||
type: ProjectType.SecretManager,
|
||||
source: "infiscal",
|
||||
data: {
|
||||
event: BusEventName.CreateSecret,
|
||||
payload: event.created
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (event.updated) {
|
||||
await eventBusService.publish(TopicName.CoreServers, {
|
||||
type: ProjectType.SecretManager,
|
||||
source: "infiscal",
|
||||
data: {
|
||||
event: BusEventName.UpdateSecret,
|
||||
payload: event.updated
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (event.deleted) {
|
||||
await eventBusService.publish(TopicName.CoreServers, {
|
||||
type: ProjectType.SecretManager,
|
||||
source: "infiscal",
|
||||
data: {
|
||||
event: BusEventName.DeleteSecret,
|
||||
payload: event.deleted
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (event.importMutation) {
|
||||
await eventBusService.publish(TopicName.CoreServers, {
|
||||
type: ProjectType.SecretManager,
|
||||
source: "infiscal",
|
||||
data: {
|
||||
event: BusEventName.ImportMutation,
|
||||
payload: event.importMutation
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const syncSecrets = async <T extends boolean = false>({
|
||||
// seperate de-dupe queue for integration sync and replication sync
|
||||
_deDupeQueue: deDupeQueue = {},
|
||||
_depth: depth = 0,
|
||||
_deDupeReplicationQueue: deDupeReplicationQueue = {},
|
||||
event,
|
||||
...dto
|
||||
}: TSyncSecretsDTO<T>) => {
|
||||
}: TSyncSecretsDTO<T> & { event?: PublishableEvent }) => {
|
||||
logger.info(
|
||||
`syncSecrets: syncing project secrets where [projectId=${dto.projectId}] [environment=${dto.environmentSlug}] [path=${dto.secretPath}]`
|
||||
);
|
||||
|
||||
const plan = await licenseService.getPlan(dto.orgId);
|
||||
|
||||
if (event && plan.eventSubscriptions) {
|
||||
await publishEvents(event);
|
||||
}
|
||||
|
||||
const deDuplicationKey = uniqueSecretQueueKey(dto.environmentSlug, dto.secretPath);
|
||||
if (
|
||||
!dto.excludeReplication
|
||||
@@ -565,7 +626,7 @@ export const secretQueueFactory = ({
|
||||
_deDupeQueue: deDupeQueue,
|
||||
_deDupeReplicationQueue: deDupeReplicationQueue,
|
||||
_depth: depth
|
||||
} as TSyncSecretsDTO,
|
||||
} as unknown as TSyncSecretsDTO,
|
||||
{
|
||||
removeOnFail: true,
|
||||
removeOnComplete: true,
|
||||
@@ -689,6 +750,7 @@ export const secretQueueFactory = ({
|
||||
isManual,
|
||||
projectId,
|
||||
secretPath,
|
||||
|
||||
depth = 1,
|
||||
deDupeQueue = {}
|
||||
} = job.data as TIntegrationSyncPayload;
|
||||
@@ -738,7 +800,13 @@ export const secretQueueFactory = ({
|
||||
environmentSlug: foldersGroupedById[folderId][0]?.environmentSlug as string,
|
||||
_deDupeQueue: deDupeQueue,
|
||||
_depth: depth + 1,
|
||||
excludeReplication: true
|
||||
excludeReplication: true,
|
||||
event: {
|
||||
importMutation: {
|
||||
secretPath: foldersGroupedById[folderId][0]?.path as string,
|
||||
environment: foldersGroupedById[folderId][0]?.environmentSlug as string
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
@@ -791,7 +859,13 @@ export const secretQueueFactory = ({
|
||||
environmentSlug: referencedFoldersGroupedById[folderId][0]?.environmentSlug as string,
|
||||
_deDupeQueue: deDupeQueue,
|
||||
_depth: depth + 1,
|
||||
excludeReplication: true
|
||||
excludeReplication: true,
|
||||
event: {
|
||||
importMutation: {
|
||||
secretPath: referencedFoldersGroupedById[folderId][0]?.path as string,
|
||||
environment: referencedFoldersGroupedById[folderId][0]?.environmentSlug as string
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
|
@@ -115,6 +115,44 @@ User Note: ${payload.note}`
|
||||
payloadBlocks
|
||||
};
|
||||
}
|
||||
case TriggerFeature.ACCESS_REQUEST_UPDATED: {
|
||||
const { payload } = notification;
|
||||
const messageBody = `${payload.editorFullName} (${payload.editorEmail}) has updated the ${
|
||||
payload.isTemporary ? "temporary" : "permanent"
|
||||
} access request from ${payload.requesterFullName} (${payload.requesterEmail}) to ${payload.secretPath} in the ${payload.environment} environment of ${payload.projectName}.
|
||||
|
||||
The following permissions are requested: ${payload.permissions.join(", ")}
|
||||
|
||||
View the request and approve or deny it <${payload.approvalUrl}|here>.${
|
||||
payload.editNote
|
||||
? `
|
||||
Editor Note: ${payload.editNote}`
|
||||
: ""
|
||||
}`;
|
||||
|
||||
const payloadBlocks = [
|
||||
{
|
||||
type: "header",
|
||||
text: {
|
||||
type: "plain_text",
|
||||
text: "Updated access approval request pending for review",
|
||||
emoji: true
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "section",
|
||||
text: {
|
||||
type: "mrkdwn",
|
||||
text: messageBody
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
return {
|
||||
payloadMessage: messageBody,
|
||||
payloadBlocks
|
||||
};
|
||||
}
|
||||
default: {
|
||||
throw new BadRequestError({
|
||||
message: "Slack notification type not supported."
|
||||
|
@@ -0,0 +1,95 @@
|
||||
import { Heading, Section, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
|
||||
import { BaseButton } from "./BaseButton";
|
||||
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
|
||||
import { BaseLink } from "./BaseLink";
|
||||
|
||||
interface AccessApprovalRequestUpdatedTemplateProps
|
||||
extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
|
||||
projectName: string;
|
||||
requesterFullName: string;
|
||||
requesterEmail: string;
|
||||
isTemporary: boolean;
|
||||
secretPath: string;
|
||||
environment: string;
|
||||
expiresIn: string;
|
||||
permissions: string[];
|
||||
editNote: string;
|
||||
editorFullName: string;
|
||||
editorEmail: string;
|
||||
approvalUrl: string;
|
||||
}
|
||||
|
||||
export const AccessApprovalRequestUpdatedTemplate = ({
|
||||
projectName,
|
||||
siteUrl,
|
||||
requesterFullName,
|
||||
requesterEmail,
|
||||
isTemporary,
|
||||
secretPath,
|
||||
environment,
|
||||
expiresIn,
|
||||
permissions,
|
||||
editNote,
|
||||
editorEmail,
|
||||
editorFullName,
|
||||
approvalUrl
|
||||
}: AccessApprovalRequestUpdatedTemplateProps) => {
|
||||
return (
|
||||
<BaseEmailWrapper
|
||||
title="Access Approval Request Update"
|
||||
preview="An access approval request was updated and requires your review."
|
||||
siteUrl={siteUrl}
|
||||
>
|
||||
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
|
||||
An access approval request was updated and is pending your review for the project <strong>{projectName}</strong>
|
||||
</Heading>
|
||||
<Section className="px-[24px] mb-[28px] mt-[36px] pt-[12px] pb-[8px] border border-solid border-gray-200 rounded-md bg-gray-50">
|
||||
<Text className="text-black text-[14px] leading-[24px]">
|
||||
<strong>{editorFullName}</strong> (<BaseLink href={`mailto:${editorEmail}`}>{editorEmail}</BaseLink>) has
|
||||
updated the access request submitted by <strong>{requesterFullName}</strong> (
|
||||
<BaseLink href={`mailto:${requesterEmail}`}>{requesterEmail}</BaseLink>) for <strong>{secretPath}</strong> in
|
||||
the <strong>{environment}</strong> environment.
|
||||
</Text>
|
||||
|
||||
{isTemporary && (
|
||||
<Text className="text-[14px] text-red-600 leading-[24px]">
|
||||
<strong>This access will expire {expiresIn} after approval.</strong>
|
||||
</Text>
|
||||
)}
|
||||
<Text className="text-[14px] leading-[24px] mb-[4px]">
|
||||
<strong>The following permissions are requested:</strong>
|
||||
</Text>
|
||||
{permissions.map((permission) => (
|
||||
<Text key={permission} className="text-[14px] my-[2px] leading-[24px]">
|
||||
- {permission}
|
||||
</Text>
|
||||
))}
|
||||
<Text className="text-[14px] text-slate-700 leading-[24px]">
|
||||
<strong className="text-black">Editor Note:</strong> "{editNote}"
|
||||
</Text>
|
||||
</Section>
|
||||
<Section className="text-center">
|
||||
<BaseButton href={approvalUrl}>Review Request</BaseButton>
|
||||
</Section>
|
||||
</BaseEmailWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccessApprovalRequestUpdatedTemplate;
|
||||
|
||||
AccessApprovalRequestUpdatedTemplate.PreviewProps = {
|
||||
requesterFullName: "Abigail Williams",
|
||||
requesterEmail: "abigail@infisical.com",
|
||||
isTemporary: true,
|
||||
secretPath: "/api/secrets",
|
||||
environment: "Production",
|
||||
siteUrl: "https://infisical.com",
|
||||
projectName: "Example Project",
|
||||
expiresIn: "1 day",
|
||||
permissions: ["Read Secret", "Delete Project", "Create Dynamic Secret"],
|
||||
editNote: "Too permissive, they only need 3 days",
|
||||
editorEmail: "john@infisical.com",
|
||||
editorFullName: "John Smith"
|
||||
} as AccessApprovalRequestUpdatedTemplateProps;
|
@@ -1,4 +1,5 @@
|
||||
export * from "./AccessApprovalRequestTemplate";
|
||||
export * from "./AccessApprovalRequestUpdatedTemplate";
|
||||
export * from "./EmailMfaTemplate";
|
||||
export * from "./EmailVerificationTemplate";
|
||||
export * from "./ExternalImportFailedTemplate";
|
||||
|
@@ -8,6 +8,7 @@ import { logger } from "@app/lib/logger";
|
||||
|
||||
import {
|
||||
AccessApprovalRequestTemplate,
|
||||
AccessApprovalRequestUpdatedTemplate,
|
||||
EmailMfaTemplate,
|
||||
EmailVerificationTemplate,
|
||||
ExternalImportFailedTemplate,
|
||||
@@ -54,6 +55,7 @@ export enum SmtpTemplates {
|
||||
EmailMfa = "emailMfa",
|
||||
UnlockAccount = "unlockAccount",
|
||||
AccessApprovalRequest = "accessApprovalRequest",
|
||||
AccessApprovalRequestUpdated = "accessApprovalRequestUpdated",
|
||||
AccessSecretRequestBypassed = "accessSecretRequestBypassed",
|
||||
SecretApprovalRequestNeedsReview = "secretApprovalRequestNeedsReview",
|
||||
// HistoricalSecretList = "historicalSecretLeakIncident", not used anymore?
|
||||
@@ -96,6 +98,7 @@ const EmailTemplateMap: Record<SmtpTemplates, React.FC<any>> = {
|
||||
[SmtpTemplates.SignupEmailVerification]: SignupEmailVerificationTemplate,
|
||||
[SmtpTemplates.EmailMfa]: EmailMfaTemplate,
|
||||
[SmtpTemplates.AccessApprovalRequest]: AccessApprovalRequestTemplate,
|
||||
[SmtpTemplates.AccessApprovalRequestUpdated]: AccessApprovalRequestUpdatedTemplate,
|
||||
[SmtpTemplates.EmailVerification]: EmailVerificationTemplate,
|
||||
[SmtpTemplates.ExternalImportFailed]: ExternalImportFailedTemplate,
|
||||
[SmtpTemplates.ExternalImportStarted]: ExternalImportStartedTemplate,
|
||||
|
@@ -11,7 +11,6 @@ import {
|
||||
validateOverrides
|
||||
} from "@app/lib/config/env";
|
||||
import { crypto } from "@app/lib/crypto/cryptography";
|
||||
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";
|
||||
@@ -465,43 +464,15 @@ export const superAdminServiceFactory = ({
|
||||
return updatedServerCfg;
|
||||
};
|
||||
|
||||
const adminSignUp = async ({
|
||||
lastName,
|
||||
firstName,
|
||||
email,
|
||||
salt,
|
||||
password,
|
||||
verifier,
|
||||
publicKey,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
encryptedPrivateKey,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag,
|
||||
ip,
|
||||
userAgent
|
||||
}: TAdminSignUpDTO) => {
|
||||
const adminSignUp = async ({ lastName, firstName, email, password, ip, userAgent }: TAdminSignUpDTO) => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
const sanitizedEmail = email.trim().toLowerCase();
|
||||
const existingUser = await userDAL.findOne({ username: sanitizedEmail });
|
||||
if (existingUser) throw new BadRequestError({ name: "Admin sign up", message: "User already exists" });
|
||||
|
||||
const privateKey = await getUserPrivateKey(password, {
|
||||
encryptionVersion: 2,
|
||||
salt,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
encryptedPrivateKey,
|
||||
iv: encryptedPrivateKeyIV,
|
||||
tag: encryptedPrivateKeyTag
|
||||
});
|
||||
|
||||
const hashedPassword = await crypto.hashing().createHash(password, appCfg.SALT_ROUNDS);
|
||||
|
||||
const { iv, tag, ciphertext, encoding } = crypto.encryption().symmetric().encryptWithRootEncryptionKey(privateKey);
|
||||
const userInfo = await userDAL.transaction(async (tx) => {
|
||||
const newUser = await userDAL.create(
|
||||
{
|
||||
@@ -519,25 +490,13 @@ export const superAdminServiceFactory = ({
|
||||
);
|
||||
const userEnc = await userDAL.createUserEncryption(
|
||||
{
|
||||
salt,
|
||||
encryptionVersion: 2,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
publicKey,
|
||||
encryptedPrivateKey,
|
||||
iv: encryptedPrivateKeyIV,
|
||||
tag: encryptedPrivateKeyTag,
|
||||
verifier,
|
||||
userId: newUser.id,
|
||||
hashedPassword,
|
||||
serverEncryptedPrivateKey: ciphertext,
|
||||
serverEncryptedPrivateKeyIV: iv,
|
||||
serverEncryptedPrivateKeyTag: tag,
|
||||
serverEncryptedPrivateKeyEncoding: encoding
|
||||
hashedPassword
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
return { user: newUser, enc: userEnc };
|
||||
});
|
||||
|
||||
@@ -587,26 +546,14 @@ export const superAdminServiceFactory = ({
|
||||
},
|
||||
tx
|
||||
);
|
||||
const { tag, encoding, ciphertext, iv } = crypto.encryption().symmetric().encryptWithRootEncryptionKey(password);
|
||||
const encKeys = await generateUserSrpKeys(sanitizedEmail, password);
|
||||
|
||||
const hashedPassword = await crypto.hashing().createHash(password, appCfg.SALT_ROUNDS);
|
||||
|
||||
const userEnc = await userDAL.createUserEncryption(
|
||||
{
|
||||
userId: newUser.id,
|
||||
encryptionVersion: 2,
|
||||
protectedKey: encKeys.protectedKey,
|
||||
protectedKeyIV: encKeys.protectedKeyIV,
|
||||
protectedKeyTag: encKeys.protectedKeyTag,
|
||||
publicKey: encKeys.publicKey,
|
||||
encryptedPrivateKey: encKeys.encryptedPrivateKey,
|
||||
iv: encKeys.encryptedPrivateKeyIV,
|
||||
tag: encKeys.encryptedPrivateKeyTag,
|
||||
salt: encKeys.salt,
|
||||
verifier: encKeys.verifier,
|
||||
serverEncryptedPrivateKeyEncoding: encoding,
|
||||
serverEncryptedPrivateKeyTag: tag,
|
||||
serverEncryptedPrivateKeyIV: iv,
|
||||
serverEncryptedPrivateKey: ciphertext
|
||||
hashedPassword
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
@@ -3,17 +3,8 @@ import { TEnvConfig } from "@app/lib/config/env";
|
||||
export type TAdminSignUpDTO = {
|
||||
email: string;
|
||||
password: string;
|
||||
publicKey: string;
|
||||
salt: string;
|
||||
lastName?: string;
|
||||
verifier: string;
|
||||
firstName: string;
|
||||
protectedKey: string;
|
||||
protectedKeyIV: string;
|
||||
protectedKeyTag: string;
|
||||
encryptedPrivateKey: string;
|
||||
encryptedPrivateKeyIV: string;
|
||||
encryptedPrivateKeyTag: string;
|
||||
ip: string;
|
||||
userAgent: string;
|
||||
};
|
||||
|
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get Project By Slug"
|
||||
openapi: "GET /api/v2/workspace/{slug}"
|
||||
---
|
@@ -310,7 +310,8 @@
|
||||
"self-hosting/guides/mongo-to-postgres",
|
||||
"self-hosting/guides/custom-certificates",
|
||||
"self-hosting/guides/automated-bootstrapping",
|
||||
"self-hosting/guides/production-hardening"
|
||||
"self-hosting/guides/production-hardening",
|
||||
"self-hosting/guides/monitoring-telemetry"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -416,6 +417,9 @@
|
||||
"pages": [
|
||||
"documentation/platform/secrets-mgmt/project",
|
||||
"documentation/platform/folder",
|
||||
"documentation/platform/secret-versioning",
|
||||
"documentation/platform/pit-recovery",
|
||||
"documentation/platform/secret-reference",
|
||||
{
|
||||
"group": "Secret Rotation",
|
||||
"pages": [
|
||||
@@ -439,6 +443,7 @@
|
||||
"documentation/platform/dynamic-secrets/aws-iam",
|
||||
"documentation/platform/dynamic-secrets/azure-entra-id",
|
||||
"documentation/platform/dynamic-secrets/cassandra",
|
||||
"documentation/platform/dynamic-secrets/couchbase",
|
||||
"documentation/platform/dynamic-secrets/elastic-search",
|
||||
"documentation/platform/dynamic-secrets/gcp-iam",
|
||||
"documentation/platform/dynamic-secrets/github",
|
||||
@@ -458,7 +463,8 @@
|
||||
"documentation/platform/dynamic-secrets/kubernetes",
|
||||
"documentation/platform/dynamic-secrets/vertica"
|
||||
]
|
||||
}
|
||||
},
|
||||
"documentation/platform/webhooks"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -995,6 +1001,7 @@
|
||||
{
|
||||
"group": "Projects",
|
||||
"pages": [
|
||||
"api-reference/endpoints/workspaces/get-workspace-by-slug",
|
||||
"api-reference/endpoints/workspaces/create-workspace",
|
||||
"api-reference/endpoints/workspaces/delete-workspace",
|
||||
"api-reference/endpoints/workspaces/get-workspace",
|
||||
|
259
docs/documentation/platform/dynamic-secrets/couchbase.mdx
Normal file
259
docs/documentation/platform/dynamic-secrets/couchbase.mdx
Normal file
@@ -0,0 +1,259 @@
|
||||
---
|
||||
title: "Couchbase"
|
||||
description: "Learn how to dynamically generate Couchbase Database user credentials."
|
||||
---
|
||||
|
||||
The Infisical Couchbase dynamic secret allows you to generate Couchbase Cloud Database user credentials on demand based on configured roles and bucket access permissions.
|
||||
|
||||
## Prerequisite
|
||||
|
||||
Create an API Key in your Couchbase Cloud following the [official documentation](https://docs.couchbase.com/cloud/get-started/create-account.html#create-api-key).
|
||||
|
||||
<Info>The API Key must have permission to manage database users in your Couchbase Cloud organization and project.</Info>
|
||||
|
||||
## Set up Dynamic Secrets with Couchbase
|
||||
|
||||
<Steps>
|
||||
<Step title="Open Secret Overview Dashboard">
|
||||
Open the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret.
|
||||
</Step>
|
||||
<Step title="Click on the 'Add Dynamic Secret' button">
|
||||

|
||||
</Step>
|
||||
<Step title="Select Couchbase">
|
||||

|
||||
</Step>
|
||||
<Step title="Provide the inputs for dynamic secret parameters">
|
||||
<ParamField path="Secret Name" type="string" required>
|
||||
Name by which you want the secret to be referenced
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Default TTL" type="string" required>
|
||||
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Max TTL" type="string" required>
|
||||
Maximum time-to-live for a generated secret
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="URL" type="string" required default="https://cloudapi.cloud.couchbase.com">
|
||||
The Couchbase Cloud API URL
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Organization ID" type="string" required>
|
||||
Your Couchbase Cloud organization ID
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Project ID" type="string" required>
|
||||
Your Couchbase Cloud project ID
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Cluster ID" type="string" required>
|
||||
Your Couchbase Cloud cluster ID where users will be created
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Roles" type="array" required>
|
||||
Database credential roles to assign to the generated user. Available options:
|
||||
- **read**: Read access to bucket data (alias for data_reader)
|
||||
- **write**: Read and write access to bucket data (alias for data_writer)
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Bucket Access" type="string" required default="*">
|
||||
Specify bucket access configuration:
|
||||
- Use `*` for access to all buckets
|
||||
- Use comma-separated bucket names (e.g., `bucket1,bucket2,bucket3`) for specific buckets
|
||||
- Use Advanced Bucket Configuration for granular scope and collection access
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="API Key" type="string" required>
|
||||
Your Couchbase Cloud API Key for authentication
|
||||
</ParamField>
|
||||
|
||||

|
||||
|
||||
</Step>
|
||||
<Step title="(Optional) Advanced Configuration">
|
||||
|
||||

|
||||
|
||||
<ParamField path="Advanced Bucket Configuration" type="boolean" default="false">
|
||||
Enable advanced bucket configuration to specify granular access to buckets, scopes, and collections
|
||||
</ParamField>
|
||||
|
||||
When Advanced Bucket Configuration is enabled, you can configure:
|
||||
|
||||
<ParamField path="Buckets" type="array">
|
||||
List of buckets with optional scope and collection specifications:
|
||||
- **Bucket Name**: Name of the bucket (e.g., travel-sample)
|
||||
- **Scopes**: Optional array of scopes within the bucket
|
||||
- **Scope Name**: Name of the scope (e.g., inventory, _default)
|
||||
- **Collections**: Optional array of collection names within the scope
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Username Template" type="string" default="{{randomUsername}}">
|
||||
Specifies a template for generating usernames. This field allows customization of how usernames are automatically created.
|
||||
|
||||
Allowed template variables are:
|
||||
- `{{randomUsername}}`: Random username string
|
||||
- `{{unixTimestamp}}`: Current Unix timestamp
|
||||
- `{{identity.name}}`: Name of the identity that is generating the secret
|
||||
- `{{random N}}`: Random string of N characters
|
||||
|
||||
Allowed template functions are:
|
||||
- `truncate`: Truncates a string to a specified length
|
||||
- `replace`: Replaces a substring with another value
|
||||
|
||||
Examples:
|
||||
```
|
||||
{{randomUsername}} // infisical-3POnzeFyK9gW2nioK0q2gMjr6CZqsRiX
|
||||
{{unixTimestamp}} // 17490641580
|
||||
{{identity.name}} // testuser
|
||||
{{random 5}} // x9k2m
|
||||
{{truncate identity.name 4}} // test
|
||||
{{replace identity.name 'user' 'replace'}} // testreplace
|
||||
```
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Password Configuration" type="object">
|
||||
Optional password generation requirements for Couchbase users:
|
||||
|
||||
<ParamField path="Password Length" type="number" default="12" min="8" max="128">
|
||||
Length of the generated password
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Character Requirements" type="object">
|
||||
Minimum required character counts:
|
||||
- **Lowercase Count**: Minimum lowercase letters (default: 1)
|
||||
- **Uppercase Count**: Minimum uppercase letters (default: 1)
|
||||
- **Digit Count**: Minimum digits (default: 1)
|
||||
- **Symbol Count**: Minimum special characters (default: 1)
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Allowed Symbols" type="string" default="!@#$%^()_+-=[]{}:,?/~`">
|
||||
Special characters allowed in passwords. Cannot contain: `< > ; . * & | £`
|
||||
</ParamField>
|
||||
|
||||
<Info>
|
||||
Couchbase password requirements: minimum 8 characters, maximum 128 characters, at least 1 uppercase, 1 lowercase, 1 digit, and 1 special character. Cannot contain: `< > ; . * & | £`
|
||||
</Info>
|
||||
</ParamField>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Click 'Submit'">
|
||||
After submitting the form, you will see a dynamic secret created in the dashboard.
|
||||
|
||||
<Note>
|
||||
If this step fails, you may need to verify your Couchbase Cloud API key permissions and organization/project/cluster IDs.
|
||||
</Note>
|
||||
|
||||

|
||||
|
||||
</Step>
|
||||
<Step title="Generate dynamic secrets">
|
||||
Once you've successfully configured the dynamic secret, you're ready to generate on-demand credentials.
|
||||
To do this, simply click on the 'Generate' button which appears when hovering over the dynamic secret item.
|
||||
Alternatively, you can initiate the creation of a new lease by selecting 'New Lease' from the dynamic secret lease list section.
|
||||
|
||||

|
||||

|
||||
|
||||
When generating these secrets, it's important to specify a Time-to-Live (TTL) duration. This will dictate how long the credentials are valid for.
|
||||
|
||||

|
||||
|
||||
<Tip>
|
||||
Ensure that the TTL for the lease falls within the maximum TTL defined when configuring the dynamic secret.
|
||||
</Tip>
|
||||
|
||||
Once you click the `Submit` button, a new secret lease will be generated and the credentials for it will be shown to you.
|
||||
|
||||

|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Advanced Bucket Configuration Examples
|
||||
|
||||
The advanced bucket configuration allows you to specify granular access control:
|
||||
|
||||
### Example 1: Specific Bucket Access
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "travel-sample"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Example 2: Bucket with Specific Scopes
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "travel-sample",
|
||||
"scopes": [
|
||||
{
|
||||
"name": "inventory"
|
||||
},
|
||||
{
|
||||
"name": "_default"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Example 3: Bucket with Scopes and Collections
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "travel-sample",
|
||||
"scopes": [
|
||||
{
|
||||
"name": "inventory",
|
||||
"collections": ["airport", "airline"]
|
||||
},
|
||||
{
|
||||
"name": "_default",
|
||||
"collections": ["users"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Audit or Revoke Leases
|
||||
|
||||
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
|
||||
This will allow you to see the expiration time of the lease or delete a lease before its set time to live.
|
||||
|
||||

|
||||
|
||||
## Renew Leases
|
||||
|
||||
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below.
|
||||

|
||||
|
||||
<Warning>
|
||||
Lease renewals cannot exceed the maximum TTL set when configuring the dynamic secret
|
||||
</Warning>
|
||||
|
||||
## Couchbase Roles and Permissions
|
||||
|
||||
The Couchbase dynamic secret integration supports the following database credential roles:
|
||||
|
||||
- **read**: Provides read-only access to bucket data
|
||||
- **write**: Provides read and write access to bucket data
|
||||
|
||||
<Note>
|
||||
These roles are specifically for database credentials and are different from Couchbase's administrative roles. They provide data-level access to buckets, scopes, and collections based on your configuration.
|
||||
</Note>
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Invalid API Key**: Ensure your Couchbase Cloud API key has the necessary permissions to manage database users
|
||||
2. **Invalid Organization/Project/Cluster IDs**: Verify that the provided IDs exist and are accessible with your API key
|
||||
3. **Role Permission Errors**: Make sure you're using only the supported database credential roles (read, write)
|
||||
4. **Bucket Access Issues**: Ensure the specified buckets exist in your cluster and are accessible
|
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: "Delivering Secrets"
|
||||
description: "Learn how to get secrets out of Infisical and into the systems, applications, and environments that need them."
|
||||
title: "Fetching Secrets"
|
||||
description: "Learn how to deliver secrets from Infisical into the systems, applications, and environments that need them."
|
||||
---
|
||||
|
||||
Once secrets are stored and scoped in Infisical, the next step is delivering them securely to the systems and applications that need them.
|
||||
|
Binary file not shown.
After Width: | Height: | Size: 536 KiB |
Binary file not shown.
After Width: | Height: | Size: 517 KiB |
Binary file not shown.
After Width: | Height: | Size: 758 KiB |
Binary file not shown.
After Width: | Height: | Size: 524 KiB |
@@ -22,7 +22,7 @@ It can also automatically reload dependent Deployments resources whenever releva
|
||||
|
||||
## Install
|
||||
|
||||
The operator can be install via [Helm](https://helm.sh). Helm is a package manager for Kubernetes that allows you to define, install, and upgrade Kubernetes applications.
|
||||
The operator can be installed via [Helm](https://helm.sh). Helm is a package manager for Kubernetes that allows you to define, install, and upgrade Kubernetes applications.
|
||||
|
||||
**Install the latest Helm repository**
|
||||
```bash
|
||||
@@ -229,9 +229,9 @@ The managed secret created by the operator will not be deleted when the operator
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Helm">
|
||||
Install Infisical Helm repository
|
||||
Uninstall Infisical Helm repository
|
||||
```bash
|
||||
helm uninstall <release name>
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</Tabs>
|
||||
|
@@ -9,6 +9,10 @@ description: "Learn how to configure an AWS Parameter Store Sync for Infisical."
|
||||
- Create an [AWS Connection](/integrations/app-connections/aws) with the required **Secret Sync** permissions
|
||||
- Ensure your network security policies allow incoming requests from Infisical to this secret sync provider, if network restrictions apply.
|
||||
|
||||
<Note>
|
||||
For workflows involving large amounts of secrets or frequent syncs, we recommend increasing your [AWS Parameter Store throughput quota](https://docs.aws.amazon.com/systems-manager/latest/userguide/parameter-store-throughput.html) to avoid rate limiting.
|
||||
</Note>
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Infisical UI">
|
||||
1. Navigate to **Project** > **Integrations** and select the **Secret Syncs** tab. Click on the **Add Sync** button.
|
||||
|
@@ -30,8 +30,9 @@ description: "Learn how to configure a Render Sync for Infisical."
|
||||

|
||||
|
||||
- **Render Connection**: The Render Connection to authenticate with.
|
||||
- **Scope**: Select **Service**.
|
||||
- **Service**: Choose the Render service you want to sync secrets to.
|
||||
- **Scope**: Select **Service** or **Environment Group**.
|
||||
- **Service**: Choose the Render service you want to sync secrets to.
|
||||
- **Environment Group**: Choose the Render environment group you want to sync secrets to.
|
||||
|
||||
5. Configure the **Sync Options** to specify how secrets should be synced, then click **Next**.
|
||||

|
||||
|
@@ -142,12 +142,12 @@ Below is a comprehensive list of all available project-level subjects and their
|
||||
Supports conditions and permission inversion
|
||||
| Action | Description | Notes |
|
||||
| -------- | ------------------------------- | ----- |
|
||||
| `read` | View secrets and their values | This action is the equivalent of granting both `describeSecret` and `readValue`. The `read` action is considered **legacy**. You should use the `describeSecret` and/or `readValue` actions instead. |
|
||||
| `read` | View secrets and their values | This action is the equivalent of granting both `describeSecret` and `readValue`. The `read` action is considered **legacy**. You should use the `describeSecret` and/or `readValue` actions instead. |
|
||||
| `describeSecret` | View secret details such as key, path, metadata, tags, and more | If you are using the API, you can pass `viewSecretValue: false` to the API call to retrieve secrets without their values. |
|
||||
| `readValue` | View the value of a secret.| In order to read secret values, the `describeSecret` action must also be granted. |
|
||||
| `create` | Add new secrets to the project | |
|
||||
| `edit` | Modify existing secret values | |
|
||||
| `delete` | Remove secrets from the project | |
|
||||
| `create` | Add new secrets to the project | |
|
||||
| `edit` | Modify existing secret values | |
|
||||
| `delete` | Remove secrets from the project | |
|
||||
|
||||
#### Subject: `secret-folders`
|
||||
|
||||
@@ -169,6 +169,15 @@ Supports conditions and permission inversion
|
||||
| `edit` | Modify secret imports |
|
||||
| `delete` | Remove secret imports |
|
||||
|
||||
#### Subject: `secret-events`
|
||||
|
||||
| Action | Description |
|
||||
| ------------------------------- | ------------------------------------------------------------- |
|
||||
| `subscribe-on-created` | Subscribe to events when secrets are created |
|
||||
| `subscribe-on-updated` | Subscribe to events when secrets are updated |
|
||||
| `subscribe-on-deleted` | Subscribe to events when secrets are deleted |
|
||||
| `subscribe-on-import-mutations` | Subscribe to events when secrets are modified through imports |
|
||||
|
||||
#### Subject: `secret-rollback`
|
||||
|
||||
| Action | Description |
|
||||
@@ -178,10 +187,10 @@ Supports conditions and permission inversion
|
||||
|
||||
#### Subject: `commits`
|
||||
|
||||
| Action | Description |
|
||||
| -------- | ---------------------------------- |
|
||||
| `read` | View commits and changes across folders |
|
||||
| `perform-rollback` | Roll back commits changes and restore folders to previous state|
|
||||
| Action | Description |
|
||||
| ------------------ | --------------------------------------------------------------- |
|
||||
| `read` | View commits and changes across folders |
|
||||
| `perform-rollback` | Roll back commits changes and restore folders to previous state |
|
||||
|
||||
#### Subject: `secret-approval`
|
||||
|
||||
@@ -197,14 +206,14 @@ Supports conditions and permission inversion
|
||||
#### Subject: `secret-rotation`
|
||||
|
||||
Supports conditions and permission inversion
|
||||
| Action | Description |
|
||||
| Action | Description |
|
||||
| ------------------------------ | ---------------------------------------------- |
|
||||
| `read` | View secret rotation configurations |
|
||||
| `read-generated-credentials` | View the generated credentials of a rotation |
|
||||
| `create` | Set up secret rotation configurations |
|
||||
| `edit` | Modify secret rotation configurations |
|
||||
| `rotate-secrets` | Rotate the generated credentials of a rotation |
|
||||
| `delete` | Remove secret rotation configurations |
|
||||
| `read` | View secret rotation configurations |
|
||||
| `read-generated-credentials` | View the generated credentials of a rotation |
|
||||
| `create` | Set up secret rotation configurations |
|
||||
| `edit` | Modify secret rotation configurations |
|
||||
| `rotate-secrets` | Rotate the generated credentials of a rotation |
|
||||
| `delete` | Remove secret rotation configurations |
|
||||
|
||||
#### Subject: `secret-syncs`
|
||||
|
||||
@@ -263,12 +272,12 @@ Supports conditions and permission inversion
|
||||
|
||||
#### Subject: `certificates`
|
||||
|
||||
| Action | Description |
|
||||
| -------------------- | ----------------------------- |
|
||||
| `read` | View certificates |
|
||||
| `read-private-key` | Read certificate private key |
|
||||
| `create` | Issue new certificates |
|
||||
| `delete` | Revoke or remove certificates |
|
||||
| Action | Description |
|
||||
| ------------------ | ----------------------------- |
|
||||
| `read` | View certificates |
|
||||
| `read-private-key` | Read certificate private key |
|
||||
| `create` | Issue new certificates |
|
||||
| `delete` | Revoke or remove certificates |
|
||||
|
||||
#### Subject: `certificate-templates`
|
||||
|
||||
@@ -330,8 +339,8 @@ Supports conditions and permission inversion
|
||||
|
||||
#### Subject: `secret-scanning-data-sources`
|
||||
|
||||
| Action | Description |
|
||||
| -------- | ---------------------------------------------------- |
|
||||
| Action | Description |
|
||||
| ---------------------------- | -------------------------------- |
|
||||
| `read-data-sources` | View Data Sources |
|
||||
| `create-data-sources` | Create new Data Sources |
|
||||
| `edit-data-sources` | Modify Data Sources |
|
||||
@@ -342,15 +351,14 @@ Supports conditions and permission inversion
|
||||
|
||||
#### Subject: `secret-scanning-findings`
|
||||
|
||||
| Action | Description |
|
||||
| -------- | --------------------------------- |
|
||||
| `read-findings` | View Secret Scanning Findings |
|
||||
| `update-findings` | Update Secret Scanning Findings |
|
||||
|
||||
| Action | Description |
|
||||
| ----------------- | ------------------------------- |
|
||||
| `read-findings` | View Secret Scanning Findings |
|
||||
| `update-findings` | Update Secret Scanning Findings |
|
||||
|
||||
#### Subject: `secret-scanning-configs`
|
||||
|
||||
| Action | Description |
|
||||
| ---------------- | ------------------------------------------------ |
|
||||
| `read-configs` | View Secret Scanning Project Configuration |
|
||||
| `update-configs` | Update Secret Scanning Project Configuration |
|
||||
| Action | Description |
|
||||
| ---------------- | -------------------------------------------- |
|
||||
| `read-configs` | View Secret Scanning Project Configuration |
|
||||
| `update-configs` | Update Secret Scanning Project Configuration |
|
||||
|
@@ -92,12 +92,11 @@ Infisical Cloud utilizes several strategies to ensure high availability, leverag
|
||||
|
||||
## Cross-Region Replication for Disaster Recovery (Infisical Cloud)
|
||||
|
||||
To handle regional failures, Infisical Cloud keeps standby regions updated and ready to take over when needed.
|
||||
To handle regional failures, Infisical Cloud keeps backups both within AWS and across cloud providers in GCP updated and ready to take over when needed.
|
||||
|
||||
- ElastiCache (Redis): Data is replicated across regions using AWS Global Datastore, keeping cached data consistent and available even if a primary region goes down.
|
||||
- RDS (PostgreSQL): Cross-region read replicas ensure database data is available in multiple locations, allowing for failover in case of a regional outage.
|
||||
- RDS (PostgreSQL): Cross-region read replicas ensure database data is available in multiple AWS locations, with additional replication to GCP for multi-cloud disaster recovery, allowing for failover in case of a regional outage or cloud provider issues.
|
||||
|
||||
With standby regions and automated failovers in place, Infisical Cloud faces minimal service disruptions even during large-scale outages.
|
||||
|
||||
## Penetration testing
|
||||
|
||||
|
@@ -1,6 +1,315 @@
|
||||
---
|
||||
title: "Infisical C++ SDK"
|
||||
sidebarTitle: "C++"
|
||||
url: "https://github.com/Infisical/infisical-cpp-sdk/?tab=readme-ov-file#infisical-c-sdk"
|
||||
icon: "/images/sdks/languages/cpp.svg"
|
||||
---
|
||||
---
|
||||
|
||||
A C++ library implementation for [Infisical](https://infisical.com).
|
||||
|
||||
## Compatible with C++ 17 and later
|
||||
The Infisical C++ SDK is compatible with C++ 17 capable compilers. This implies GCC 8 or newer, and clang 3.8 or newer. Earlier versions of C++ are unsupported.
|
||||
|
||||
## Dependencies
|
||||
- `cURL`: Used internally for crafting HTTP requests.
|
||||
|
||||
## CMake Installation
|
||||
|
||||
```bash
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
project(InfisicalTest)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_PREFIX_PATH ${CMAKE_BINARY_DIR})
|
||||
|
||||
find_package(OpenSSL REQUIRED)
|
||||
|
||||
|
||||
include(FetchContent)
|
||||
|
||||
|
||||
FetchContent_Declare(
|
||||
infisical
|
||||
GIT_REPOSITORY https://github.com/Infisical/infisical-cpp-sdk.git
|
||||
GIT_TAG 1.0.0 # Replace with the desired version
|
||||
)
|
||||
|
||||
FetchContent_MakeAvailable(infisical)
|
||||
FetchContent_GetProperties(infisical)
|
||||
|
||||
|
||||
# Example usage. This will differ based on your project structure.
|
||||
add_executable(my_app src/main.cpp)
|
||||
target_link_libraries(my_app PRIVATE infisical OpenSSL::SSL OpenSSL::Crypto)
|
||||
target_include_directories(my_app PRIVATE ${infisical_SOURCE_DIR}/include)
|
||||
```
|
||||
|
||||
## Manual Installation
|
||||
If you're unable to use the recommended CMake installation approach, you can choose to manually build the library and use it in your project.
|
||||
|
||||
```bash
|
||||
mkdir build
|
||||
cd build
|
||||
cmake ..
|
||||
make
|
||||
```
|
||||
|
||||
## Quick-Start Example
|
||||
|
||||
Below you'll find an example that uses the Infisical SDK to fetch a secret with the key `API_KEY` using [Machine Identity Universal Auth](https://infisical.com/docs/documentation/platform/identities/universal-auth)
|
||||
|
||||
More examples can be found in the [/examples](https://github.com/Infisical/infisical-cpp-sdk/tree/main/examples) folder.
|
||||
|
||||
```cpp
|
||||
#include <iostream>
|
||||
#include <libinfisical/InfisicalClient.h>
|
||||
|
||||
int main() {
|
||||
|
||||
try {
|
||||
Infisical::InfisicalClient client(
|
||||
Infisical::ConfigBuilder()
|
||||
.withHostUrl("https://app.infisical.com") // Optionally change this to your custom Infisical instance URL.
|
||||
.withAuthentication(
|
||||
Infisical::AuthenticationBuilder()
|
||||
.withUniversalAuth("<machine-identity-universal-auth-client-id>", "<machine-identity-universal-auth-client-secret>")
|
||||
.build())
|
||||
.build());
|
||||
|
||||
const auto getSecretOptions = Infisical::Input::GetSecretOptionsBuilder()
|
||||
.withEnvironment("<env-slug>") // dev, staging, prod, etc
|
||||
.withProjectId("<your-project-id>")
|
||||
.withSecretKey("API_KEY")
|
||||
.build();
|
||||
|
||||
const auto apiKeySecret = client.secrets().getSecret(getSecretOptions);
|
||||
|
||||
printf("Secret retrieved, [key=%s] [value=%s]\n", apiKeySecret.getSecretKey().c_str(), apiKeySecret.getSecretValue().c_str());
|
||||
} catch (const Infisical::InfisicalError &e) {
|
||||
std::cerr << "Error: " << e.what() << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
## JSON Serialization
|
||||
The SDK uses [nlohmann/json](https://github.com/nlohmann/json) internally to serialize/deserialize JSON data. This SDK makes no assumptions about which JSON library you use in your project, and you aren't constrained to `nlohmann/json` in any way. Data returned by the SDK is returned as a class, which exposes Getter methods for getting fields such as the secret value or secret key.
|
||||
|
||||
|
||||
## Documentation
|
||||
The Infisical C++ SDK follows a builder pattern for all types of input. Below is a detailed documentation of our currently support methods.
|
||||
|
||||
Everything related to the Infisical SDK lives inside the `Infisical` namespace.
|
||||
|
||||
### InfisicalClient Class
|
||||
`InfisicalClient(Config &config)`
|
||||
|
||||
```cpp
|
||||
Infisical::InfisicalClient client(
|
||||
Infisical::ConfigBuilder()
|
||||
.withHostUrl("https://app.infisical.com")
|
||||
.withAuthentication(
|
||||
Infisical::AuthenticationBuilder()
|
||||
.withUniversalAuth(clientId, clientSecret)
|
||||
.build())
|
||||
.build());
|
||||
```
|
||||
|
||||
Config is created through the `ConfigBuilder` class. See below for more details
|
||||
|
||||
### Config Class
|
||||
|
||||
`Config` defines the configuration of the Infisical Client itself, such as authentication.
|
||||
|
||||
```cpp
|
||||
Infisical::Config config = Infisical::ConfigBuilder()
|
||||
.withHostUrl("https://app.infisical.com")
|
||||
.withAuthentication(
|
||||
Infisical::AuthenticationBuilder()
|
||||
.withUniversalAuth(clientId, clientSecret)
|
||||
.build())
|
||||
.build();
|
||||
|
||||
Infisical::InfisicalClient client(config);
|
||||
```
|
||||
|
||||
- `withHostUrl(string)` _(optional)_: Specify a custom Infisical host URL, pointing to your Infisical instance. Defaults to `https://app.infisical.com`
|
||||
- `withAuthentication(Infisical::Authentication)`: Configure the authentication that will be used by the SDK. See [Authentication Class](#authentication-class) for more details.
|
||||
- `build()`: Returns the `Config` object with the options you configured.
|
||||
|
||||
### Authentication Class
|
||||
```cpp
|
||||
Infisical::Authentication auth = Infisical::AuthenticationBuilder()
|
||||
.withUniversalAuth(clientId, clientSecret)
|
||||
.build();
|
||||
|
||||
Infisical::Config config = Infisical::ConfigBuilder()
|
||||
.withAuthentication(std::move(auth)) // Or use inline declaration
|
||||
.build();
|
||||
```
|
||||
|
||||
- `withUniversalAuth(string, string)`: Specify the Universal Auth Client ID and Client Secret that will be used for authentication.
|
||||
- `build()`: Returns the `Authentication` object with the options you specified.
|
||||
|
||||
### TSecret Class
|
||||
The `TSecret` class is the class that's returned by all secret methods (get/list/delete/update/create). It can come in the form of a `std::vector` or a single instance.
|
||||
|
||||
**Available getter methods:**
|
||||
- `getId(): std::string`: Returns the ID of the secret.
|
||||
- `getWorkspace(): std::string`: Returns the project ID of the secret.
|
||||
- `getEnvironment(): std::string`: Returns the environment slug of the secret.
|
||||
- `getVersion(): unsigned int`: Gets the version of the secret. By default this will always be the latest version unless specified otherwise with `withVersion()`
|
||||
- `getType(): std::string`: Returns the type of the secret. Can only be `shared` or `personal`. Shared secrets are available to everyone with access to the secret. Personal secrets are personal overwrites of the secret, mainly intended for local development purposes.
|
||||
- `getSecretKey(): std::string`: Returns the secret key.
|
||||
- `getSecretValue(): std::string` Returns the secret value.
|
||||
- `getRotationId(): std::string`: If the secret is a rotation secret, this will return the rotation ID of the secret. If it's a regular secret, this will return an empty string.
|
||||
- `getSecretPath(): std::string`: Returns the secret path of the secret.
|
||||
- `getSkipMultilineEncoding(): bool`: Returns whether or not skip multiline encoding is enabled for the secret or not.
|
||||
`getIsRotatedSecret(): bool`: Returns wether or not the secret is a rotated secret. If `true`, then `getRotationId()` returns the ID of the rotation.
|
||||
|
||||
|
||||
|
||||
### Secrets
|
||||
|
||||
#### Create Secret
|
||||
```cpp
|
||||
const auto createSecretOptions = Infisical::Input::CreateSecretOptionsBuilder()
|
||||
.withEnvironment("<env-slug>")
|
||||
.withProjectId("<project-id>")
|
||||
.withSecretKey("SECRET_KEY_TO_CREATE")
|
||||
.withSecretValue("VALUE_TO_CREATE")
|
||||
.withSecretComment("Secret comment to attach") // Optional
|
||||
.withSecretPath("/path/where/to/create/secret") // Optional, defaults to /
|
||||
.withTagIds({"tag-id-1", "tag-id-2"}) // Optional
|
||||
.build();
|
||||
|
||||
const auto secret = client.secrets().createSecret(createSecretOptions);
|
||||
```
|
||||
|
||||
**Parameters**:
|
||||
- `withEnvironment(string)`: Specify the slug of the environment to create the secret in.
|
||||
- `withProjectId(string)`: Specify the ID of the project to create the secret in.
|
||||
- `withSecretPath(string)`: Specify the secret path to create the secret in. Defaults to `/`
|
||||
- `withSecretKey(string)`: The secret key to be created.
|
||||
- `withSecretValue(string)`: The value of the secret to create.
|
||||
- `withSecretComment(string)` _(optional)_: Optionally add a comment to the secret.
|
||||
- `withTagIds(std::vector<std::string>>)` _(optional)_: A list of ID's of tags to attach to the secret.
|
||||
- `build()`: Returns the `CreateSecretOptions` class that can be passed into the `createSecret()` method.
|
||||
|
||||
**Returns**:
|
||||
- Returns the created secret as a `TSecret` class. Read more in the [TSecret Class](#tsecret-class) documentation.
|
||||
|
||||
#### Update Secret
|
||||
|
||||
```cpp
|
||||
const auto updateSecretOptions = Infisical::Input::UpdateSecretOptionsBuilder()
|
||||
.withEnvironment("<env-slug>")
|
||||
.withProjectId("<project-id>")
|
||||
.withSecretKey("<secret-key>")
|
||||
.withNewSecretKey("<new-secret-key>") // Optional
|
||||
.withSecretValue("<new-secret-value>") // Optional
|
||||
.withSecretComment("Updated comment") // Optional
|
||||
.withSecretReminderNote("Updated reminder note") // Optional
|
||||
.withSecretReminderRepeatDays(1) // Optional
|
||||
.withType("shared") // Optional
|
||||
.withTagIds({"tag-id-3", "tag-id-4"}) // Optional
|
||||
.build();
|
||||
|
||||
const auto updatedSecret = client.secrets().updateSecret(updateSecretOptions);
|
||||
```
|
||||
|
||||
**Parameters**:
|
||||
- `withEnvironment(string)`: Specify the slug of the environment where the secret lives in.
|
||||
- `withProjectId(string)`: Specify the ID of the project where the secret to update lives in.
|
||||
- `withSecretPath(string)`: Specify the secret path of the secret to update. Defaults to `/`.
|
||||
- `withType("shared" | "personal")`: _(optional)_: The type of secret to update. Defaults to `shared`.
|
||||
- `withSecretKey(string)`: The key of the secret you wish to update.
|
||||
- `withNewSecretKey(string)` _(optional)_: The new key of the secret you wish to update.
|
||||
- `withSecretValue(string)` _(optional)_: The new value of the secret.
|
||||
- `withSecretReminderNote(string)` _(optional)_: Update the secret reminder note attached to the secret.
|
||||
- `withSecretReminderRepeatDays(unsigned int)` _(optional)_: Update the secret reminder repeat days attached to the secret.
|
||||
- `withTagIds(std::vector<std::string>>)` _(optional)_: A list of ID's of tags to attach to the secret.
|
||||
- `build()`: Returns the `UpdateSecretOptions` class that can be passed into the `updateSecret()` method.
|
||||
|
||||
**Returns**:
|
||||
- Returns the updated secret as a `TSecret` class. Read more in the [TSecret Class](#tsecret-class) documentation.
|
||||
|
||||
#### Get Secret
|
||||
```cpp
|
||||
const auto getSecretOptions = Infisical::Input::GetSecretOptionsBuilder()
|
||||
.withEnvironment("<env-slug>")
|
||||
.withProjectId("<project-id>")
|
||||
.withSecretKey("<secret-key>")
|
||||
.withType("shared")
|
||||
.withVersion(2)
|
||||
.withExpandSecretReferences(true)
|
||||
.build();
|
||||
|
||||
const auto secret = client.secrets().getSecret(getSecretOptions);
|
||||
```
|
||||
**Parameters**:
|
||||
- `withEnvironment(string)`: Specify the slug of the environment where the secret lives in.
|
||||
- `withProjectId(string)`: Specify the ID of the project where the secret lives in.
|
||||
- `withSecretPath(string)`: Specify the secret path of the secret to get. Defaults to `/`
|
||||
- `withType("shared" | "personal")`: _(optional)_: The type of secret to get. Defaults to `shared`.
|
||||
- `withSecretKey(string)`: The key of the secret to get.
|
||||
- `withExpandSecretReferences(bool)` _(optional)_: Whether or not to expand secret references automatically. Defaults to `true`.
|
||||
- `withVersion(unsigned int)` _(optional)_: Optionally fetch a specific version of the secret. If not defined, the latest version of the secret is returned.
|
||||
- `build()`: Returns the `GetSecretOptions` class that can be passed into the `getSecret()` method.
|
||||
|
||||
**Returns**:
|
||||
- Returns the secret as a `TSecret` class. Read more in the [TSecret Class](#tsecret-class) documentation.
|
||||
|
||||
#### Delete Secret
|
||||
|
||||
```cpp
|
||||
const auto deleteSecretOptions = Infisical::Input::DeleteSecretOptionsBuilder()
|
||||
.withEnvironment("<env-slug>")
|
||||
.withProjectId("<project-id>")
|
||||
.withSecretKey("<secret-key>")
|
||||
.withType("shared")
|
||||
.withSecretPath("<secret-path>")
|
||||
.build();
|
||||
|
||||
const auto deletedSecret = client.secrets().deleteSecret(deleteSecretOptions);
|
||||
```
|
||||
|
||||
**Parameters**:
|
||||
- `withEnvironment(string)`: Specify the slug of the environment where the secret to delete lives in.
|
||||
- `withProjectId(string)`: Specify the ID of the project where the secret to delete lives in.
|
||||
- `withSecretPath(string)`: Specify the secret path of the secret to delete. Defaults to `/`
|
||||
- `withType("shared" | "personal")`: _(optional)_: The type of secret to delete. Defaults to `shared`.
|
||||
- `withSecretKey(string)`: The key of the secret to delete.
|
||||
- `build()` Returns the `DeleteSecretOptions` class that can be passed into the `deleteSecret()` method.
|
||||
|
||||
**Returns**:
|
||||
- Returns the deleted secret as a `TSecret` class. Read more in the [TSecret Class](#tsecret-class) documentation.
|
||||
|
||||
|
||||
#### List Secrets
|
||||
```cpp
|
||||
const auto listSecretsOptions = Infisical::Input::ListSecretOptionsBuilder()
|
||||
.withProjectId(projectId)
|
||||
.withEnvironment(environment)
|
||||
.withSecretPath("/")
|
||||
.withRecursive(false)
|
||||
.withAddSecretsToEnvironmentVariables(false)
|
||||
.build();
|
||||
|
||||
const auto secrets = client.secrets().listSecrets(listSecretsOptions);
|
||||
|
||||
```
|
||||
|
||||
**Parameters**:
|
||||
- `withEnvironment(string)`: Specify the slug of the environment to list secrets from.
|
||||
- `withProjectId(string)`: Specify the ID of the project to fetch secrets from.
|
||||
- `withSecretPath(string)`: Specify the secret path to fetch secrets from. Defaults to `/`
|
||||
- `withExpandSecretReferences(bool)` _(optional)_: Whether or not to expand secret references automatically. Defaults to `true`.
|
||||
- `withRecursive(bool)` _(optional)_: Wether or not to recursively fetch secrets from sub-folders. If set to true, all secrets from the secret path specified with `withSecretPath()` and downwards will be fetched.
|
||||
- `withAddSecretsToEnvironmentVariables(bool)` _(optional)_: If set to true, the fetched secrets will be automatically set as environment variables, making them accessible with `std::getenv` or equivalent by secret key.
|
||||
- `build()`: Returns the `ListSecretsOptions` class that can be passed into the `listSecrets()` method.
|
||||
|
||||
**Returns**:
|
||||
- Returns the listed secrets as `std::vector<TSecret>`. Read more in the [TSecret Class](#tsecret-class) documentation.
|
||||
|
@@ -1,594 +1,358 @@
|
||||
---
|
||||
title: "Infisical .NET SDK"
|
||||
sidebarTitle: ".NET"
|
||||
url: "https://github.com/Infisical/infisical-dotnet-sdk?tab=readme-ov-file#infisical-net-sdk"
|
||||
icon: "/images/sdks/languages/dotnet.svg"
|
||||
---
|
||||
{/*
|
||||
If you're working with C#, the official [Infisical C# SDK](https://github.com/Infisical/sdk/tree/main/languages/csharp) package is the easiest way to fetch and work with secrets for your application.
|
||||
|
||||
- [Nuget Package](https://www.nuget.org/packages/Infisical.Sdk)
|
||||
- [Github Repository](https://github.com/Infisical/sdk/tree/main/languages/csharp)
|
||||
The Infisical .NET SDK provides a convenient way to interact with the Infisical API.
|
||||
|
||||
<Warning>
|
||||
**Deprecation Notice**
|
||||
## Installation
|
||||
|
||||
All versions prior to **2.3.9** should be considered deprecated and are no longer supported by Infisical. Please update to version **2.3.9** or newer. All changes are fully backwards compatible with older versions.
|
||||
</Warning>
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```cs
|
||||
using Infisical.Sdk;
|
||||
|
||||
namespace Example
|
||||
{
|
||||
class Program
|
||||
{
|
||||
static void Main(string[] args)
|
||||
{
|
||||
|
||||
ClientSettings settings = new ClientSettings
|
||||
{
|
||||
Auth = new AuthenticationOptions
|
||||
{
|
||||
UniversalAuth = new UniversalAuthMethod
|
||||
{
|
||||
ClientId = "your-client-id",
|
||||
ClientSecret = "your-client-secret"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
var infisicalClient = new InfisicalClient(settings);
|
||||
|
||||
var getSecretOptions = new GetSecretOptions
|
||||
{
|
||||
SecretName = "TEST",
|
||||
ProjectId = "PROJECT_ID",
|
||||
Environment = "dev",
|
||||
};
|
||||
var secret = infisicalClient.GetSecret(getSecretOptions);
|
||||
|
||||
|
||||
Console.WriteLine($"The value of secret '{secret.SecretKey}', is: {secret.SecretValue}");
|
||||
}
|
||||
}
|
||||
}
|
||||
```bash
|
||||
dotnet add package Infisical.Sdk
|
||||
```
|
||||
|
||||
This example demonstrates how to use the Infisical C# SDK in a C# application. The application retrieves a secret named `TEST` from the `dev` environment of the `PROJECT_ID` project.
|
||||
## Getting Started (.NET)
|
||||
|
||||
<Warning>
|
||||
We do not recommend hardcoding your [Machine Identity Tokens](/platform/identities/overview). Setting it as an environment variable would be best.
|
||||
</Warning>
|
||||
|
||||
# Installation
|
||||
|
||||
```console
|
||||
$ dotnet add package Infisical.Sdk
|
||||
```
|
||||
# Configuration
|
||||
|
||||
Import the SDK and create a client instance with your [Machine Identity](/platform/identities/universal-auth).
|
||||
|
||||
```cs
|
||||
using Infisical.Sdk;
|
||||
|
||||
namespace Example
|
||||
{
|
||||
class Program
|
||||
{
|
||||
static void Main(string[] args)
|
||||
{
|
||||
ClientSettings settings = new ClientSettings
|
||||
{
|
||||
Auth = new AuthenticationOptions
|
||||
{
|
||||
UniversalAuth = new UniversalAuthMethod
|
||||
{
|
||||
ClientId = "your-client-id",
|
||||
ClientSecret = "your-client-secret"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
var infisicalClient = new InfisicalClient(settings); // <-- Your SDK client is now ready to use
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ClientSettings methods
|
||||
|
||||
<ParamField query="options" type="object">
|
||||
<Expandable title="properties">
|
||||
<ParamField query="ClientId" deprecated type="string" optional>
|
||||
Your machine identity client ID.
|
||||
</ParamField>
|
||||
<ParamField query="ClientSecret" deprecated type="string" optional>
|
||||
Your machine identity client secret.
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="AccessToken" deprecated type="string" optional>
|
||||
An access token obtained from the machine identity login endpoint.
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="CacheTtl" type="number" default="300" optional>
|
||||
Time-to-live (in seconds) for refreshing cached secrets.
|
||||
If manually set to 0, caching will be disabled, this is not recommended.
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="SiteUrl" type="string" default="https://app.infisical.com" optional>
|
||||
Your self-hosted absolute site URL including the protocol (e.g. `https://app.infisical.com`)
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="SslCertificatePath" optional>
|
||||
Optionally provide a path to a custom SSL certificate file. This can be substituted by setting the `INFISICAL_SSL_CERTIFICATE` environment variable to the contents of the certificate.
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="Auth" type="AuthenticationOptions">
|
||||
The authentication object to use for the client. This is required unless you're using environment variables.
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
|
||||
</ParamField>
|
||||
|
||||
### Authentication
|
||||
|
||||
The SDK supports a variety of authentication methods. The most common authentication method is Universal Auth, which uses a client ID and client secret to authenticate.
|
||||
|
||||
#### Universal Auth
|
||||
|
||||
**Using environment variables**
|
||||
- `INFISICAL_UNIVERSAL_AUTH_CLIENT_ID` - Your machine identity client ID.
|
||||
- `INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET` - Your machine identity client secret.
|
||||
|
||||
**Using the SDK directly**
|
||||
```csharp
|
||||
ClientSettings settings = new ClientSettings
|
||||
{
|
||||
Auth = new AuthenticationOptions
|
||||
{
|
||||
UniversalAuth = new UniversalAuthMethod
|
||||
{
|
||||
ClientId = "your-client-id",
|
||||
ClientSecret = "your-client-secret"
|
||||
}
|
||||
}
|
||||
};
|
||||
namespace Example;
|
||||
|
||||
using Infisical.Sdk;
|
||||
using Infisical.Sdk.Model;
|
||||
|
||||
public class Program {
|
||||
public static void Main(string[] args) {
|
||||
|
||||
var settings = new InfisicalSdkSettingsBuilder()
|
||||
.WithHostUri("http://localhost:8080") // Optional. Will default to https://app.infisical.com
|
||||
.Build();
|
||||
|
||||
var infisicalClient = new InfisicalClient(settings);
|
||||
```
|
||||
|
||||
#### GCP ID Token Auth
|
||||
<Info>
|
||||
Please note that this authentication method will only work if you're running your application on Google Cloud Platform.
|
||||
Please [read more](/documentation/platform/identities/gcp-auth) about this authentication method.
|
||||
</Info>
|
||||
var _ = infisicalClient.Auth().UniversalAuth().LoginAsync("<machine-identity-universal-auth-client-id>", "<machine-identity-universal-auth-client-secret>").Result;
|
||||
|
||||
**Using environment variables**
|
||||
- `INFISICAL_GCP_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID.
|
||||
|
||||
**Using the SDK directly**
|
||||
```csharp
|
||||
ClientSettings settings = new ClientSettings
|
||||
{
|
||||
Auth = new AuthenticationOptions
|
||||
var options = new ListSecretsOptions
|
||||
{
|
||||
GcpIdToken = new GcpIdTokenAuthMethod
|
||||
{
|
||||
IdentityId = "your-machine-identity-id",
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
var infisicalClient = new InfisicalClient(settings);
|
||||
```
|
||||
|
||||
#### GCP IAM Auth
|
||||
|
||||
**Using environment variables**
|
||||
- `INFISICAL_GCP_IAM_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID.
|
||||
- `INFISICAL_GCP_IAM_SERVICE_ACCOUNT_KEY_FILE_PATH` - The path to your GCP service account key file.
|
||||
|
||||
**Using the SDK directly**
|
||||
```csharp
|
||||
ClientSettings settings = new ClientSettings
|
||||
{
|
||||
Auth = new AuthenticationOptions
|
||||
{
|
||||
GcpIam = new GcpIamAuthMethod
|
||||
{
|
||||
IdentityId = "your-machine-identity-id",
|
||||
ServiceAccountKeyFilePath = "./path/to/your/service-account-key.json"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
var infisicalClient = new InfisicalClient(settings);
|
||||
```
|
||||
|
||||
#### AWS IAM Auth
|
||||
<Info>
|
||||
Please note that this authentication method will only work if you're running your application on AWS.
|
||||
Please [read more](/documentation/platform/identities/aws-auth) about this authentication method.
|
||||
</Info>
|
||||
|
||||
**Using environment variables**
|
||||
- `INFISICAL_AWS_IAM_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID.
|
||||
|
||||
**Using the SDK directly**
|
||||
```csharp
|
||||
ClientSettings settings = new ClientSettings
|
||||
{
|
||||
Auth = new AuthenticationOptions
|
||||
{
|
||||
AwsIam = new AwsIamAuthMethod
|
||||
{
|
||||
IdentityId = "your-machine-identity-id",
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
var infisicalClient = new InfisicalClient(settings);
|
||||
```
|
||||
|
||||
|
||||
#### Azure Auth
|
||||
<Info>
|
||||
Please note that this authentication method will only work if you're running your application on Azure.
|
||||
Please [read more](/documentation/platform/identities/azure-auth) about this authentication method.
|
||||
</Info>
|
||||
|
||||
**Using environment variables**
|
||||
- `INFISICAL_AZURE_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID.
|
||||
|
||||
**Using the SDK directly**
|
||||
```csharp
|
||||
ClientSettings settings = new ClientSettings
|
||||
{
|
||||
Auth = new AuthenticationOptions
|
||||
{
|
||||
Azure = new AzureAuthMethod
|
||||
{
|
||||
IdentityId = "YOUR_IDENTITY_ID",
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var infisicalClient = new InfisicalClient(settings);
|
||||
```
|
||||
|
||||
#### Kubernetes Auth
|
||||
<Info>
|
||||
Please note that this authentication method will only work if you're running your application on Kubernetes.
|
||||
Please [read more](/documentation/platform/identities/kubernetes-auth) about this authentication method.
|
||||
</Info>
|
||||
|
||||
**Using environment variables**
|
||||
- `INFISICAL_KUBERNETES_IDENTITY_ID` - Your Infisical Machine Identity ID.
|
||||
- `INFISICAL_KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH_ENV_NAME` - The environment variable name that contains the path to the service account token. This is optional and will default to `/var/run/secrets/kubernetes.io/serviceaccount/token`.
|
||||
|
||||
**Using the SDK directly**
|
||||
```csharp
|
||||
ClientSettings settings = new ClientSettings
|
||||
{
|
||||
Auth = new AuthenticationOptions
|
||||
{
|
||||
Kubernetes = new KubernetesAuthMethod
|
||||
{
|
||||
ServiceAccountTokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token", // Optional
|
||||
IdentityId = "YOUR_IDENTITY_ID",
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var infisicalClient = new InfisicalClient(settings);
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Caching
|
||||
|
||||
To reduce the number of API requests, the SDK temporarily stores secrets it retrieves. By default, a secret remains cached for 5 minutes after it's first fetched. Each time it's fetched again, this 5-minute timer resets. You can adjust this caching duration by setting the "cacheTTL" option when creating the client.
|
||||
|
||||
## Working with Secrets
|
||||
|
||||
### client.ListSecrets(options)
|
||||
|
||||
```cs
|
||||
var options = new ListSecretsOptions
|
||||
{
|
||||
ProjectId = "PROJECT_ID",
|
||||
Environment = "dev",
|
||||
Path = "/foo/bar",
|
||||
AttachToProcessEnv = false,
|
||||
};
|
||||
|
||||
var secrets = infisical.ListSecrets(options);
|
||||
```
|
||||
|
||||
Retrieve all secrets within the Infisical project and environment that client is connected to
|
||||
|
||||
#### Parameters
|
||||
|
||||
<ParamField query="Parameters" type="object">
|
||||
<Expandable title="properties">
|
||||
<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="ProjectId" type="string">
|
||||
The project ID where the secret lives in.
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="Path" type="string" optional>
|
||||
The path from where secrets should be fetched from.
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="AttachToProcessEnv" type="boolean" default="false" optional>
|
||||
Whether or not to set the fetched secrets to the process environment. If true, you can access the secrets like so `System.getenv("SECRET_NAME")`.
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="IncludeImports" 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="Recursive" type="boolean" default="false" optional>
|
||||
Whether or not to fetch secrets recursively from the specified path. Please note that there's a 20-depth limit for recursive fetching.
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="ExpandSecretReferences" 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>
|
||||
|
||||
</ParamField>
|
||||
|
||||
### client.GetSecret(options)
|
||||
|
||||
```cs
|
||||
var options = new GetSecretOptions
|
||||
{
|
||||
SecretName = "AAAA",
|
||||
ProjectId = "659c781eb2d4fe3e307b77bd",
|
||||
Environment = "dev",
|
||||
SetSecretsAsEnvironmentVariables = true,
|
||||
EnvironmentSlug = "<your-env-slug>",
|
||||
SecretPath = "/",
|
||||
ProjectId = "<your-project-id>",
|
||||
};
|
||||
var secret = infisical.GetSecret(options);
|
||||
|
||||
var secrets = infisicalClient.Secrets().ListAsync(options).Result;
|
||||
|
||||
if (secrets == null)
|
||||
{
|
||||
throw new Exception("Failed to fetch secrets, returned null response");
|
||||
}
|
||||
|
||||
foreach (var secret in secrets)
|
||||
{
|
||||
Console.WriteLine($"{secret.SecretKey}: {secret.SecretValue}");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Retrieve a secret from Infisical.
|
||||
## Getting Started (Visual Basic)
|
||||
```vb
|
||||
Imports Infisical.Sdk
|
||||
Imports Infisical.Sdk.Model
|
||||
|
||||
By default, `GetSecret()` fetches and returns a shared secret.
|
||||
|
||||
#### Parameters
|
||||
|
||||
<ParamField query="Parameters" type="object" optional>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="SecretName" type="string" required>
|
||||
The key of the secret to retrieve.
|
||||
</ParamField>
|
||||
<ParamField query="ProjectId" 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 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 "shared".
|
||||
</ParamField>
|
||||
<ParamField query="IncludeImports" type="boolean" optional>
|
||||
Whether or not to include imported secrets from the current path. Read about [secret import](/documentation/platform/secret-reference)
|
||||
</ParamField>
|
||||
<ParamField query="ExpandSecretReferences" type="boolean" optional>
|
||||
Whether or not to expand secret references in the fetched secrets. Read about [secret reference](/documentation/platform/secret-reference)
|
||||
</ParamField>
|
||||
Module Program
|
||||
Sub Main(args As String())
|
||||
Dim settings = New InfisicalSdkSettingsBuilder() _
|
||||
.WithHostUri("https://app.infisical.com") _
|
||||
.Build()
|
||||
|
||||
</Expandable>
|
||||
</ParamField>
|
||||
Dim infisicalClient As New InfisicalClient(settings)
|
||||
|
||||
### client.CreateSecret(options)
|
||||
Dim authResult = infisicalClient.Auth().UniversalAuth() _
|
||||
.LoginAsync("<machine-identity-universal-auth-client-id>", "machine-identity-universal-auth-client-secret").Result
|
||||
|
||||
```cs
|
||||
var options = new CreateSecretOptions {
|
||||
Environment = "dev",
|
||||
ProjectId = "PROJECT_ID",
|
||||
Dim options As New ListSecretsOptions With {
|
||||
.SetSecretsAsEnvironmentVariables = True,
|
||||
.EnvironmentSlug = "<your-env-slug>",
|
||||
.SecretPath = "/",
|
||||
.ProjectId = "<your-project-id>"
|
||||
}
|
||||
|
||||
SecretName = "NEW_SECRET",
|
||||
SecretValue = "NEW_SECRET_VALUE",
|
||||
SecretComment = "This is a new secret",
|
||||
};
|
||||
Dim secrets = infisicalClient.Secrets().ListAsync(options).Result
|
||||
|
||||
var newSecret = infisical.CreateSecret(options);
|
||||
For Each secret In secrets
|
||||
Console.WriteLine(secret.SecretKey)
|
||||
if Environment.GetEnvironmentVariable(secret.SecretKey) IsNot Nothing Then
|
||||
Console.WriteLine("{0} found on environment variables", secret.SecretKey)
|
||||
End If
|
||||
Next
|
||||
|
||||
End Sub
|
||||
End Module
|
||||
```
|
||||
|
||||
Create a new secret in Infisical.
|
||||
## Core Methods
|
||||
|
||||
#### Parameters
|
||||
The SDK methods are organized into the following high-level categories:
|
||||
|
||||
<ParamField query="Parameters" type="object" optional>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="SecretName" type="string" required>
|
||||
The key of the secret to create.
|
||||
</ParamField>
|
||||
<ParamField query="SecretValue" type="string" required>
|
||||
The value of the secret.
|
||||
</ParamField>
|
||||
<ParamField query="ProjectId" 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>
|
||||
1. `Auth()`: Handles authentication methods.
|
||||
2. `Secrets()`: Manages CRUD operations for secrets.
|
||||
3. `Pki()`: Programmatically interact with the Infisical PKI.
|
||||
* `Subscribers()`: Manage PKI Subscribers.
|
||||
|
||||
### client.UpdateSecret(options)
|
||||
### `Auth()`
|
||||
|
||||
The `Auth()` component provides methods for authentication:
|
||||
|
||||
### Universal Auth
|
||||
|
||||
#### Authenticating
|
||||
```cs
|
||||
var options = new UpdateSecretOptions {
|
||||
Environment = "dev",
|
||||
ProjectId = "PROJECT_ID",
|
||||
|
||||
SecretName = "SECRET_TO_UPDATE",
|
||||
SecretValue = "NEW VALUE"
|
||||
};
|
||||
|
||||
var updatedSecret = infisical.UpdateSecret(options);
|
||||
var _ = await sdk.Auth().UniversalAuth().LoginAsync(
|
||||
"CLIENT_ID",
|
||||
"CLIENT_SECRET"
|
||||
);
|
||||
```
|
||||
|
||||
Update an existing secret in Infisical.
|
||||
|
||||
#### Parameters
|
||||
**Parameters:**
|
||||
- `clientId` (string): The client ID of your Machine Identity.
|
||||
- `clientSecret` (string): The client secret of your Machine Identity.
|
||||
|
||||
<ParamField query="Parameters" type="object" optional>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="SecretName" type="string" required>
|
||||
The key of the secret to update.
|
||||
</ParamField>
|
||||
<ParamField query="SecretValue" type="string" required>
|
||||
The new value of the secret.
|
||||
</ParamField>
|
||||
<ParamField query="ProjectId" 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>
|
||||
### `Secrets()`
|
||||
|
||||
### client.DeleteSecret(options)
|
||||
The `Secrets()` sub-class handles operations related to the Infisical secrets management product.
|
||||
|
||||
#### List Secrets
|
||||
|
||||
```cs
|
||||
Task<Secret[]> ListAsync(ListSecretsOptions options);
|
||||
|
||||
throws InfisicalException
|
||||
```
|
||||
|
||||
```csharp
|
||||
var options = new ListSecretsOptions
|
||||
{
|
||||
SetSecretsAsEnvironmentVariables = true,
|
||||
EnvironmentSlug = "dev",
|
||||
SecretPath = "/test",
|
||||
Recursive = true,
|
||||
ExpandSecretReferences = true,
|
||||
ProjectId = projectId,
|
||||
ViewSecretValue = true,
|
||||
};
|
||||
|
||||
Secret[] secrets = await sdk.Secrets().ListAsync(options);
|
||||
```
|
||||
|
||||
**ListSecretsOptions:**
|
||||
- `ProjectId` (string): The ID of your project.
|
||||
- `EnvironmentSlug` (string): The environment in which to list secrets (e.g., "dev").
|
||||
- `SecretPath` (string): The path to the secrets.
|
||||
- `ExpandSecretReferences` (boolean): Whether to expand secret references.
|
||||
- `Recursive` (boolean): Whether to list secrets recursively.
|
||||
- `SetSecretsAsEnvironmentVariables` (boolean): Set the retrieved secrets as environment variables.
|
||||
|
||||
**Returns:**
|
||||
- `Task<Secret[]>`: The response containing the list of secrets.
|
||||
|
||||
#### Create Secret
|
||||
|
||||
```cs
|
||||
public Task<Secret> CreateAsync(CreateSecretOptions options);
|
||||
|
||||
throws InfisicalException
|
||||
```
|
||||
|
||||
```cs
|
||||
|
||||
var options = new CreateSecretOptions
|
||||
{
|
||||
SecretName = "SECRET_NAME",
|
||||
SecretValue = "SECRET_VALUE",
|
||||
EnvironmentSlug = "<environment-slug>",
|
||||
SecretPath = "/",
|
||||
ProjectId = "<your-project-id>",
|
||||
Metadata = new SecretMetadata[] {
|
||||
new SecretMetadata {
|
||||
Key = "metadata-key",
|
||||
Value = "metadata-value"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Task<Secret> newSecret = await sdk.Secrets().CreateAsync(options);
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `SecretName` (string): The name of the secret to create
|
||||
- `SecretValue` (string): The value of the secret.
|
||||
- `ProjectId` (string): The ID of your project.
|
||||
- `EnvironmentSlug` (string): The environment in which to create the secret.
|
||||
- `SecretPath` (string, optional): The path to the secret.
|
||||
- `Metadata` (object, optional): Attach metadata to the secret.
|
||||
- `SecretComment` (string, optional): Attach a secret comment to the secret.
|
||||
- `SecretReminderNote` (string, optional): Attach a secret reminder note to the secret.
|
||||
- `SecretReminderRepeatDays` (int, optional): Set the reminder repeat days on the secret.
|
||||
- `SkipMultilineEncoding` (bool, optional): Whether or not to skip multiline encoding for the secret's value. Defaults to `false`.
|
||||
|
||||
**Returns:**
|
||||
- `Task<Secret>`: The created secret.
|
||||
|
||||
#### Update Secret
|
||||
|
||||
```cs
|
||||
public Task<Secret> UpdateAsync(UpdateSecretOptions options);
|
||||
|
||||
throws InfisicalException
|
||||
```
|
||||
|
||||
|
||||
```cs
|
||||
|
||||
var updateSecretOptions = new UpdateSecretOptions
|
||||
{
|
||||
SecretName = "EXISTING_SECRET_NAME",
|
||||
EnvironmentSlug = "<environment-slug>",
|
||||
SecretPath = "/",
|
||||
NewSecretName = "NEW_SECRET_NAME",
|
||||
NewSecretValue = "new-secret-value",
|
||||
ProjectId = "<project-id>",
|
||||
};
|
||||
|
||||
|
||||
Task<Secret> updatedSecret = await sdk.Secrets().UpdateAsync(updateSecretOptions);
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `SecretName` (string): The name of the secret to update.`
|
||||
- `ProjectId` (string): The ID of your project.
|
||||
- `EnvironmentSlug` (string): The environment in which to update the secret.
|
||||
- `SecretPath` (string): The path to the secret.
|
||||
- `NewSecretValue` (string, optional): The new value of the secret.
|
||||
- `NewSecretName` (string, optional): A new name for the secret.
|
||||
- `NewMetadata` (object, optional): New metadata to attach to the secret.
|
||||
|
||||
**Returns:**
|
||||
- `Task<Secret>`: The updated secret.
|
||||
|
||||
#### Get Secret by Name
|
||||
|
||||
```cs
|
||||
public Task<Secret> GetAsync(GetSecretOptions options);
|
||||
|
||||
throws InfisicalException
|
||||
```
|
||||
|
||||
```cs
|
||||
|
||||
var getSecretOptions = new GetSecretOptions
|
||||
{
|
||||
SecretName = "SECRET_NAME",
|
||||
EnvironmentSlug = "<environment-slug>",
|
||||
SecretPath = "/",
|
||||
ProjectId = "<project-id>",
|
||||
};
|
||||
|
||||
Secret secret = await sdk.Secrets().GetAsync(getSecretOptions);
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `SecretName` (string): The name of the secret to get`
|
||||
- `ProjectId` (string): The ID of your project.
|
||||
- `EnvironmentSlug` (string): The environment in which to retrieve the secret.
|
||||
- `SecretPath` (string): The path to the secret.
|
||||
- `ExpandSecretReferences` (boolean, optional): Whether to expand secret references.
|
||||
- `Type` (SecretType, optional): The type of secret to fetch. Defaults to `Shared`.
|
||||
|
||||
|
||||
**Returns:**
|
||||
- `Task<Secret>`: The fetched secret.
|
||||
|
||||
#### Delete Secret by Name
|
||||
|
||||
```cs
|
||||
public Secret DeleteAsync(DeleteSecretOptions options);
|
||||
|
||||
throws InfisicalException
|
||||
```
|
||||
|
||||
```cs
|
||||
|
||||
var options = new DeleteSecretOptions
|
||||
{
|
||||
Environment = "dev",
|
||||
ProjectId = "PROJECT_ID",
|
||||
SecretName = "NEW_SECRET",
|
||||
SecretName = "SECRET_TO_DELETE",
|
||||
EnvironmentSlug = "<environment-slug>",
|
||||
SecretPath = "/",
|
||||
ProjectId = "<project-id>",
|
||||
};
|
||||
|
||||
var deletedSecret = infisical.DeleteSecret(options);
|
||||
|
||||
Secret deletedSecret = await sdk.Secrets().DeleteAsync(options);
|
||||
```
|
||||
|
||||
Delete a secret in Infisical.
|
||||
**Parameters:**
|
||||
- `SecretName` (string): The name of the secret to delete.
|
||||
- `ProjectId` (string): The ID of your project.
|
||||
- `EnvironmentSlug` (string): The environment in which to delete the secret.
|
||||
- `SecretPath` (string, optional): The path to the secret.
|
||||
|
||||
#### Parameters
|
||||
**Returns:**
|
||||
- `Task<Secret>`: The deleted secret.
|
||||
|
||||
<ParamField query="Parameters" type="object" optional>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="SecretName" type="string">
|
||||
The key of the secret to update.
|
||||
</ParamField>
|
||||
<ParamField query="ProjectId" 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
|
||||
### `Pki().Subscribers()`
|
||||
|
||||
### Create a symmetric key
|
||||
The `Pki().Subscribers()` sub-class is used to programmatically interact with the Infisical PKI product line. Currently only issuing new certificates and retrieving the latest certificate bundle from a subscriber is supported. More widespread support for the PKI product is coming to the .NET SDK in the near future.
|
||||
|
||||
Create a base64-encoded, 256-bit symmetric key to be used for encryption/decryption.
|
||||
|
||||
#### Issue a new certificate
|
||||
|
||||
```cs
|
||||
var key = infisical.CreateSymmetricKey();
|
||||
public async Task<SubscriberIssuedCertificate> IssueCertificateAsync(IssueCertificateOptions options);
|
||||
|
||||
throws InfisicalException
|
||||
```
|
||||
|
||||
#### Returns (string)
|
||||
`key` (string): A base64-encoded, 256-bit symmetric key, that can be used for encryption/decryption purposes.
|
||||
|
||||
### Encrypt symmetric
|
||||
```cs
|
||||
var options = new EncryptSymmetricOptions
|
||||
|
||||
var options = new IssueCertificateOptions
|
||||
{
|
||||
Plaintext = "Infisical is awesome!",
|
||||
Key = key,
|
||||
SubscriberName = "<subscriber-name>",
|
||||
ProjectId = "<your-project-id>",
|
||||
};
|
||||
|
||||
var encryptedData = infisical.EncryptSymmetric(options);
|
||||
SubscriberIssuedCertificate newCertificate = await sdk.Pki().Subscribers().IssueCertificateAsync(options);
|
||||
```
|
||||
|
||||
#### Parameters
|
||||
**Parameters:**
|
||||
- `SubscriberName` (string): The name of the subscriber to create a certificate for.
|
||||
- `ProjectId` (string): The ID of PKI project.
|
||||
|
||||
<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>
|
||||
</ParamField>
|
||||
**Returns:**
|
||||
- `Task<SubscriberIssuedCertificate>`: The newly issued certificate along with it's credentials for the specified subscriber.
|
||||
|
||||
#### Returns (object)
|
||||
`Tag` (string): A base64-encoded, 128-bit authentication tag.
|
||||
`Iv` (string): A base64-encoded, 96-bit initialization vector.
|
||||
`CipherText` (string): A base64-encoded, encrypted ciphertext.
|
||||
#### Retrieve latest certificate bundle
|
||||
|
||||
### Decrypt symmetric
|
||||
```cs
|
||||
var decryptOptions = new DecryptSymmetricOptions
|
||||
{
|
||||
Key = key,
|
||||
Ciphertext = encryptedData.Ciphertext,
|
||||
Iv = encryptedData.Iv,
|
||||
Tag = encryptedData.Tag,
|
||||
};
|
||||
public async Task<CertificateBundle> RetrieveLatestCertificateBundleAsync(RetrieveLatestCertificateBundleOptions options)
|
||||
|
||||
var decryptedPlaintext = infisical.DecryptSymmetric(decryptOptions);
|
||||
throws InfisicalException
|
||||
```
|
||||
|
||||
#### 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>
|
||||
</ParamField>
|
||||
```cs
|
||||
var options = new RetrieveLatestCertificateBundleOptions
|
||||
{
|
||||
SubscriberName = "<subscriber-name>",
|
||||
ProjectId = "<your-project-id>",
|
||||
};
|
||||
|
||||
#### Returns (string)
|
||||
`Plaintext` (string): The decrypted plaintext.
|
||||
*/}
|
||||
CertificateBundle latestCertificate = await sdk.Pki().Subscribers().RetrieveLatestCertificateBundleAsync(options);
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `SubscriberName` (string): The name of the subscriber to retrieve the latest certificate bundle for
|
||||
- `ProjectId` (string): The ID of PKI project.
|
||||
|
||||
**Returns:**
|
||||
- `Task<CertificateBundle>`: The latest certificate bundle for the specified subscriber.
|
@@ -1,575 +1,487 @@
|
||||
---
|
||||
title: "Infisical Java SDK"
|
||||
sidebarTitle: "Java"
|
||||
url: "https://github.com/Infisical/java-sdk?tab=readme-ov-file#infisical-java-sdk"
|
||||
icon: "/images/sdks/languages/java.svg"
|
||||
---
|
||||
|
||||
{
|
||||
/*
|
||||
If you're working with Java, the official [Infisical Java SDK](https://github.com/Infisical/sdk/tree/main/languages/java) package is the easiest way to fetch and work with secrets for your application.
|
||||
The Infisical SDK provides a convenient way to interact with the Infisical API.
|
||||
|
||||
- [Maven Package](https://github.com/Infisical/sdk/packages/2019741)
|
||||
- [Github Repository](https://github.com/Infisical/sdk/tree/main/languages/java)
|
||||
## Installation
|
||||
|
||||
## Basic Usage
|
||||
Replace `{version}` with the version of the SDK you wish to use. This documentation covers version >=3.0.0.
|
||||
|
||||
### Maven
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>com.infisical</groupId>
|
||||
<artifactId>sdk</artifactId>
|
||||
<version>{version}</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
### Gradle
|
||||
```gradle
|
||||
implementation group: 'com.infisical', name: 'sdk', version: '{version}'
|
||||
```
|
||||
|
||||
|
||||
### Others
|
||||
For other build tools, please check our [package snippets](https://central.sonatype.com/artifact/com.infisical/sdk), and select the build tool you're using for your project.
|
||||
|
||||
|
||||
|
||||
## Getting Started
|
||||
|
||||
```java
|
||||
package com.example.app;
|
||||
package com.example.example;
|
||||
|
||||
import com.infisical.sdk.InfisicalClient;
|
||||
import com.infisical.sdk.schema.*;
|
||||
import com.infisical.sdk.InfisicalSdk;
|
||||
import com.infisical.sdk.SdkConfig;
|
||||
|
||||
public class Example {
|
||||
public static void main(String[] args) {
|
||||
|
||||
// Create the authentication settings for the client
|
||||
ClientSettings settings = new ClientSettings();
|
||||
AuthenticationOptions authOptions = new AuthenticationOptions();
|
||||
UniversalAuthMethod authMethod = new UniversalAuthMethod();
|
||||
|
||||
authMethod.setClientID("YOUR_IDENTITY_ID");
|
||||
authMethod.setClientSecret("YOUR_CLIENT_SECRET");
|
||||
public static void main(String[] args) {
|
||||
var sdk = new InfisicalSdk(
|
||||
new SdkConfig.Builder()
|
||||
// Optional, will default to https://app.infisical.com
|
||||
.withSiteUrl("https://your-infisical-instance.com")
|
||||
.build()
|
||||
);
|
||||
|
||||
authOptions.setUniversalAuth(authMethod);
|
||||
settings.setAuth(authOptions);
|
||||
sdk.Auth().UniversalAuthLogin(
|
||||
"CLIENT_ID",
|
||||
"CLIENT_SECRET"
|
||||
);
|
||||
|
||||
// Create a new Infisical Client
|
||||
InfisicalClient client = new InfisicalClient(settings);
|
||||
var secret = sdk.Secrets().GetSecret(
|
||||
"<secret-name>",
|
||||
"<project-id>",
|
||||
"<env-slug>",
|
||||
"<secret-path>",
|
||||
null, // Expand Secret References (boolean, optional)
|
||||
null, // Include Imports (boolean, optional)
|
||||
null // Secret Type (shared/personal, defaults to shared, optional)
|
||||
);
|
||||
|
||||
// Create the options for fetching the secret
|
||||
GetSecretOptions options = new GetSecretOptions();
|
||||
options.setSecretName("TEST");
|
||||
options.setEnvironment("dev");
|
||||
options.setProjectID("PROJECT_ID");
|
||||
|
||||
// Fetch the sercret with the provided options
|
||||
GetSecretResponseSecret secret = client.getSecret(options);
|
||||
|
||||
// Print the value
|
||||
System.out.println(secret.getSecretValue());
|
||||
|
||||
// Important to avoid memory leaks!
|
||||
// If you intend to use the client throughout your entire application, you can omit this line.
|
||||
client.close();
|
||||
}
|
||||
System.out.println(secret);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This example demonstrates how to use the Infisical Java SDK in a Java application. The application retrieves a secret named `TEST` from the `dev` environment of the `PROJECT_ID` project.
|
||||
## Core Methods
|
||||
|
||||
<Warning>
|
||||
We do not recommend hardcoding your [Machine Identity Tokens](/platform/identities/overview). Setting it as an environment variable would be best.
|
||||
</Warning>
|
||||
The SDK methods are organized into the following high-level categories:
|
||||
|
||||
# Installation
|
||||
1. `Auth()`: Handles authentication methods.
|
||||
2. `Secrets()`: Manages CRUD operations for secrets.
|
||||
|
||||
The Infisical Java SDK is hosted on the GitHub Packages Apache Maven registry. Because of this you need to configure your environment properly so it's able to pull dependencies from the GitHub registry. Please check [this guide from GitHub](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-apache-maven-registry) on how to achieve this.
|
||||
### `Auth`
|
||||
|
||||
Our package is [located here](https://github.com/Infisical/sdk/packages/2019741). Please follow the installation guide on the page.
|
||||
The `Auth` component provides methods for authentication:
|
||||
|
||||
# Configuration
|
||||
### Universal Auth
|
||||
|
||||
Import the SDK and create a client instance with your [Machine Identity](/platform/identities/universal-auth).
|
||||
#### Authenticating
|
||||
|
||||
```java
|
||||
import com.infisical.sdk.InfisicalClient;
|
||||
import com.infisical.sdk.schema.*;
|
||||
|
||||
public class App {
|
||||
public static void main(String[] args) {
|
||||
// Create the authentication settings for the client
|
||||
ClientSettings settings = new ClientSettings();
|
||||
AuthenticationOptions authOptions = new AuthenticationOptions();
|
||||
UniversalAuthMethod authMethod = new UniversalAuthMethod();
|
||||
|
||||
authMethod.setClientID("YOUR_IDENTITY_ID");
|
||||
authMethod.setClientSecret("YOUR_CLIENT_SECRET");
|
||||
|
||||
authOptions.setUniversalAuth(authMethod);
|
||||
settings.setAuth(authOptions);
|
||||
|
||||
// Create a new Infisical Client
|
||||
InfisicalClient client = new InfisicalClient(settings); // Your client!
|
||||
}
|
||||
}
|
||||
public void UniversalAuthLogin(
|
||||
String clientId,
|
||||
String clientSecret
|
||||
)
|
||||
throws InfisicalException
|
||||
```
|
||||
|
||||
### ClientSettings methods
|
||||
|
||||
<ParamField query="options" type="object">
|
||||
<Expandable title="properties">
|
||||
<ParamField query="setClientID()" type="string" deprecated optional>
|
||||
Your machine identity client ID.
|
||||
|
||||
**This field is deprecated and will be removed in future versions.** Please use the `setAuth()` method on the client settings instead.
|
||||
</ParamField>
|
||||
<ParamField query="setClientSecret()" deprecated type="string" optional>
|
||||
Your machine identity client secret.
|
||||
|
||||
**This field is deprecated and will be removed in future versions.** Please use the `setAuth()` method on the client settings instead.
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="setAccessToken()" deprecatedtype="string" optional>
|
||||
An access token obtained from the machine identity login endpoint.
|
||||
|
||||
**This field is deprecated and will be removed in future versions.** Please use the `setAuth()` method on the client settings instead.
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="setCacheTTL()" type="number" default="300" optional>
|
||||
Time-to-live (in seconds) for refreshing cached secrets.
|
||||
If manually set to 0, caching will be disabled, this is not recommended.
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="setSiteURL()" type="string" default="https://app.infisical.com" optional>
|
||||
Your self-hosted absolute site URL including the protocol (e.g. `https://app.infisical.com`)
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="setSSLCertificatePath()">
|
||||
Optionally provide a path to a custom SSL certificate file. This can be substituted by setting the `INFISICAL_SSL_CERTIFICATE` environment variable to the contents of the certificate.
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="setAuth()" type="AuthenticationOptions">
|
||||
The authentication object to use for the client. This is required unless you're using environment variables.
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
|
||||
</ParamField>
|
||||
|
||||
### Authentication
|
||||
|
||||
The SDK supports a variety of authentication methods. The most common authentication method is Universal Auth, which uses a client ID and client secret to authenticate.
|
||||
|
||||
#### Universal Auth
|
||||
|
||||
**Using environment variables**
|
||||
- `INFISICAL_UNIVERSAL_AUTH_CLIENT_ID` - Your machine identity client ID.
|
||||
- `INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET` - Your machine identity client secret.
|
||||
|
||||
**Using the SDK directly**
|
||||
```java
|
||||
ClientSettings settings = new ClientSettings();
|
||||
AuthenticationOptions authOptions = new AuthenticationOptions();
|
||||
UniversalAuthMethod authMethod = new UniversalAuthMethod();
|
||||
|
||||
authMethod.setClientID("YOUR_IDENTITY_ID");
|
||||
authMethod.setClientSecret("YOUR_CLIENT_SECRET");
|
||||
|
||||
authOptions.setUniversalAuth(authMethod);
|
||||
settings.setAuth(authOptions);
|
||||
|
||||
InfisicalClient client = new InfisicalClient(settings);
|
||||
```
|
||||
|
||||
#### GCP ID Token Auth
|
||||
<Info>
|
||||
Please note that this authentication method will only work if you're running your application on Google Cloud Platform.
|
||||
Please [read more](/documentation/platform/identities/gcp-auth) about this authentication method.
|
||||
</Info>
|
||||
|
||||
**Using environment variables**
|
||||
- `INFISICAL_GCP_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID.
|
||||
|
||||
**Using the SDK directly**
|
||||
```java
|
||||
ClientSettings settings = new ClientSettings();
|
||||
AuthenticationOptions authOptions = new AuthenticationOptions();
|
||||
GCPIDTokenAuthMethod authMethod = new GCPIDTokenAuthMethod();
|
||||
|
||||
authMethod.setIdentityID("YOUR_MACHINE_IDENTITY_ID");
|
||||
|
||||
authOptions.setGcpIDToken(authMethod);
|
||||
settings.setAuth(authOptions);
|
||||
|
||||
InfisicalClient client = new InfisicalClient(settings);
|
||||
```
|
||||
|
||||
#### GCP IAM Auth
|
||||
|
||||
**Using environment variables**
|
||||
- `INFISICAL_GCP_IAM_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID.
|
||||
- `INFISICAL_GCP_IAM_SERVICE_ACCOUNT_KEY_FILE_PATH` - The path to your GCP service account key file.
|
||||
|
||||
**Using the SDK directly**
|
||||
```java
|
||||
ClientSettings settings = new ClientSettings();
|
||||
AuthenticationOptions authOptions = new AuthenticationOptions();
|
||||
GCPIamAuthMethod authMethod = new GCPIamAuthMethod();
|
||||
|
||||
authMethod.setIdentityID("YOUR_MACHINE_IDENTITY_ID");
|
||||
authMethod.setServiceAccountKeyFilePath("./path/to/your/service-account-key.json");
|
||||
|
||||
authOptions.setGcpIam(authMethod);
|
||||
settings.setAuth(authOptions);
|
||||
|
||||
InfisicalClient client = new InfisicalClient(settings);
|
||||
```
|
||||
|
||||
#### AWS IAM Auth
|
||||
<Info>
|
||||
Please note that this authentication method will only work if you're running your application on AWS.
|
||||
Please [read more](/documentation/platform/identities/aws-auth) about this authentication method.
|
||||
</Info>
|
||||
|
||||
**Using environment variables**
|
||||
- `INFISICAL_AWS_IAM_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID.
|
||||
|
||||
**Using the SDK directly**
|
||||
```java
|
||||
ClientSettings settings = new ClientSettings();
|
||||
AuthenticationOptions authOptions = new AuthenticationOptions();
|
||||
AWSIamAuthMethod authMethod = new AWSIamAuthMethod();
|
||||
|
||||
authMethod.setIdentityID("YOUR_MACHINE_IDENTITY_ID");
|
||||
|
||||
authOptions.setAwsIam(authMethod);
|
||||
settings.setAuth(authOptions);
|
||||
|
||||
InfisicalClient client = new InfisicalClient(settings);
|
||||
```
|
||||
|
||||
#### Azure Auth
|
||||
<Info>
|
||||
Please note that this authentication method will only work if you're running your application on Azure.
|
||||
Please [read more](/documentation/platform/identities/azure-auth) about this authentication method.
|
||||
</Info>
|
||||
|
||||
**Using environment variables**
|
||||
- `INFISICAL_AZURE_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID.
|
||||
|
||||
**Using the SDK directly**
|
||||
```java
|
||||
ClientSettings settings = new ClientSettings();
|
||||
AuthenticationOptions authOptions = new AuthenticationOptions();
|
||||
AzureAuthMethod authMethod = new AzureAuthMethod();
|
||||
|
||||
authMethod.setIdentityID("YOUR_IDENTITY_ID");
|
||||
|
||||
authOptions.setAzure(authMethod);
|
||||
settings.setAuth(authOptions);
|
||||
|
||||
InfisicalClient client = new InfisicalClient(settings);
|
||||
```
|
||||
|
||||
#### Kubernetes Auth
|
||||
<Info>
|
||||
Please note that this authentication method will only work if you're running your application on Kubernetes.
|
||||
Please [read more](/documentation/platform/identities/kubernetes-auth) about this authentication method.
|
||||
</Info>
|
||||
|
||||
**Using environment variables**
|
||||
- `INFISICAL_KUBERNETES_IDENTITY_ID` - Your Infisical Machine Identity ID.
|
||||
- `INFISICAL_KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH_ENV_NAME` - The environment variable name that contains the path to the service account token. This is optional and will default to `/var/run/secrets/kubernetes.io/serviceaccount/token`.
|
||||
|
||||
**Using the SDK directly**
|
||||
```java
|
||||
ClientSettings settings = new ClientSettings();
|
||||
AuthenticationOptions authOptions = new AuthenticationOptions();
|
||||
KubernetesAuthMethod authMethod = new KubernetesAuthMethod();
|
||||
|
||||
authMethod.setIdentityID("YOUR_IDENTITY_ID");
|
||||
authMethod.setServiceAccountTokenPath("/var/run/secrets/kubernetes.io/serviceaccount/token"); // Optional
|
||||
|
||||
authOptions.setKubernetes(authMethod);
|
||||
settings.setAuth(authOptions);
|
||||
|
||||
InfisicalClient client = new InfisicalClient(settings);
|
||||
sdk.Auth().UniversalAuthLogin(
|
||||
"CLIENT_ID",
|
||||
"CLIENT_SECRET"
|
||||
);
|
||||
```
|
||||
|
||||
|
||||
### Caching
|
||||
**Parameters:**
|
||||
- `clientId` (string): The client ID of your Machine Identity.
|
||||
- `clientSecret` (string): The client secret of your Machine Identity.
|
||||
|
||||
To reduce the number of API requests, the SDK temporarily stores secrets it retrieves. By default, a secret remains cached for 5 minutes after it's first fetched. Each time it's fetched again, this 5-minute timer resets. You can adjust this caching duration by setting the "cacheTTL" option when creating the client.
|
||||
|
||||
## Working with Secrets
|
||||
|
||||
### client.listSecrets(options)
|
||||
### LDAP Auth
|
||||
|
||||
```java
|
||||
ListSecretsOptions options = new ListSecretsOptions();
|
||||
options.setEnvironment("dev");
|
||||
options.setProjectID("PROJECT_ID");
|
||||
options.setPath("/foo/bar");
|
||||
options.setIncludeImports(false);
|
||||
options.setRecursive(false);
|
||||
options.setExpandSecretReferences(true);
|
||||
|
||||
SecretElement[] secrets = client.listSecrets(options);
|
||||
public void LdapAuthLogin(
|
||||
LdapAuthLoginInput input
|
||||
)
|
||||
throws InfisicalException
|
||||
```
|
||||
|
||||
Retrieve all secrets within the Infisical project and environment that client is connected to
|
||||
|
||||
#### Methods
|
||||
|
||||
<ParamField query="Parameters" type="object">
|
||||
<Expandable title="properties">
|
||||
<ParamField query="setEnvironment()" type="string" required>
|
||||
The slug name (dev, prod, etc) of the environment from where secrets should be fetched from.
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="setProjectID()" type="string">
|
||||
The project ID where the secret lives in.
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="setPath()" type="string" optional>
|
||||
The path from where secrets should be fetched from.
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="setAttachToProcessEnv()" type="boolean" default="false" optional>
|
||||
Whether or not to set the fetched secrets to the process environment. If true, you can access the secrets like so `System.getenv("SECRET_NAME")`.
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="setIncludeImports()" 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="setRecursive()" type="boolean" default="false" optional>
|
||||
Whether or not to fetch secrets recursively from the specified path. Please note that there's a 20-depth limit for recursive fetching.
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="setExpandSecretReferences()" 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>
|
||||
|
||||
</ParamField>
|
||||
|
||||
### client.getSecret(options)
|
||||
|
||||
```java
|
||||
GetSecretOptions options = new GetSecretOptions();
|
||||
options.setSecretName("TEST");
|
||||
options.setEnvironment("dev");
|
||||
options.setProjectID("PROJECT_ID");
|
||||
var input = LdapAuthLoginInput
|
||||
.builder()
|
||||
.identityId("<machine-identity-id>")
|
||||
.username("<ldap-username>")
|
||||
.password("<ldap-password>")
|
||||
.build();
|
||||
|
||||
GetSecretResponseSecret secret = client.getSecret(options);
|
||||
|
||||
String secretValue = secret.getSecretValue();
|
||||
sdk.Auth().LdapAuthLogin(input);
|
||||
```
|
||||
|
||||
Retrieve a secret from Infisical.
|
||||
**Parameters:**
|
||||
- `input` (LdapAuthLoginInput): The input for authenticating with LDAP.
|
||||
- `identityId` (String): The ID of the machine identity to authenticate with.
|
||||
- `username` (String): The LDAP username.
|
||||
- `password` (String): The LDAP password.
|
||||
|
||||
By default, `getSecret()` fetches and returns a shared secret.
|
||||
### Access Token Auth
|
||||
|
||||
#### Methods
|
||||
|
||||
<ParamField query="Parameters" type="object" optional>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="setSecretName()" type="string" required>
|
||||
The key of the secret to retrieve.
|
||||
</ParamField>
|
||||
<ParamField query="setProjectID()" type="string" required>
|
||||
The project ID where the secret lives in.
|
||||
</ParamField>
|
||||
<ParamField query="setEnvironment()" type="string" required>
|
||||
The slug name (dev, prod, etc) of the environment from where secrets should be fetched from.
|
||||
</ParamField>
|
||||
<ParamField query="setPath()" type="string" optional>
|
||||
The path from where secret should be fetched from.
|
||||
</ParamField>
|
||||
<ParamField query="setType()" type="string" optional>
|
||||
The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared".
|
||||
</ParamField>
|
||||
<ParamField query="setIncludeImports()" type="boolean" optional>
|
||||
Whether or not to include imported secrets from the current path. Read about [secret import](/documentation/platform/secret-reference)
|
||||
</ParamField>
|
||||
<ParamField query="setExpandSecretReferences()" 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>
|
||||
</ParamField>
|
||||
|
||||
### client.createSecret(options)
|
||||
#### Authenticating
|
||||
|
||||
```java
|
||||
CreateSecretOptions createOptions = new CreateSecretOptions();
|
||||
createOptions.setSecretName("NEW_SECRET");
|
||||
createOptions.setEnvironment("dev");
|
||||
createOptions.setProjectID("PROJECT_ID");
|
||||
createOptions.setSecretValue("SOME SECRET VALUE");
|
||||
createOptions.setPath("/"); // Default
|
||||
createOptions.setType("shared"); // Default
|
||||
|
||||
CreateSecretResponseSecret newSecret = client.createSecret(createOptions);
|
||||
public void SetAccessToken(
|
||||
String accessToken
|
||||
)
|
||||
throws InfisicalException
|
||||
```
|
||||
|
||||
Create a new secret in Infisical.
|
||||
|
||||
#### Methods
|
||||
|
||||
<ParamField query="Parameters" type="object" optional>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="setSecretName()" type="string" required>
|
||||
The key of the secret to create.
|
||||
</ParamField>
|
||||
<ParamField query="setSecretValue()" type="string" required>
|
||||
The value of the secret.
|
||||
</ParamField>
|
||||
<ParamField query="setProjectID()" type="string" required>
|
||||
The project ID where the secret lives in.
|
||||
</ParamField>
|
||||
<ParamField query="setEnvironment()" type="string" required>
|
||||
The slug name (dev, prod, etc) of the environment from where secrets should be fetched from.
|
||||
</ParamField>
|
||||
<ParamField query="setPath()" type="string" optional>
|
||||
The path from where secret should be created.
|
||||
</ParamField>
|
||||
<ParamField query="setType()" 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)
|
||||
|
||||
```java
|
||||
UpdateSecretOptions options = new UpdateSecretOptions();
|
||||
|
||||
options.setSecretName("SECRET_TO_UPDATE");
|
||||
options.setSecretValue("NEW SECRET VALUE");
|
||||
options.setEnvironment("dev");
|
||||
options.setProjectID("PROJECT_ID");
|
||||
options.setPath("/"); // Default
|
||||
options.setType("shared"); // Default
|
||||
|
||||
UpdateSecretResponseSecret updatedSecret = client.updateSecret(options);
|
||||
sdk.Auth().SetAccessToken("ACCESS_TOKEN");
|
||||
```
|
||||
|
||||
Update an existing secret in Infisical.
|
||||
**Parameters:**
|
||||
- `accessToken` (string): The access token you want to use for authentication.
|
||||
|
||||
#### Methods
|
||||
### `Secrets`
|
||||
|
||||
<ParamField query="Parameters" type="object" optional>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="setSecretName()" type="string" required>
|
||||
The key of the secret to update.
|
||||
</ParamField>
|
||||
<ParamField query="setSecretValue()" type="string" required>
|
||||
The new value of the secret.
|
||||
</ParamField>
|
||||
<ParamField query="setProjectID()" type="string" required>
|
||||
The project ID where the secret lives in.
|
||||
</ParamField>
|
||||
<ParamField query="setEnvironment()" type="string" required>
|
||||
The slug name (dev, prod, etc) of the environment from where secrets should be fetched from.
|
||||
</ParamField>
|
||||
<ParamField query="setPath()" type="string" optional>
|
||||
The path from where secret should be updated.
|
||||
</ParamField>
|
||||
<ParamField query="setType()" 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>
|
||||
This sub-class handles operations related to secrets:
|
||||
|
||||
### client.deleteSecret(options)
|
||||
#### List Secrets
|
||||
|
||||
```java
|
||||
DeleteSecretOptions options = new DeleteSecretOptions();
|
||||
public List<Secret> ListSecrets(
|
||||
String projectId,
|
||||
String environmentSlug,
|
||||
String secretPath,
|
||||
Boolean expandSecretReferences,
|
||||
Boolean recursive,
|
||||
Boolean includeImports,
|
||||
Boolean setSecretsOnSystemProperties
|
||||
)
|
||||
|
||||
options.setSecretName("SECRET_TO_DELETE");
|
||||
options.setEnvironment("dev");
|
||||
options.setProjectID("PROJECT_ID");
|
||||
options.setPath("/"); // Default
|
||||
options.setType("shared"); // Default
|
||||
|
||||
DeleteSecretResponseSecret deletedSecret = client.deleteSecret(options);
|
||||
throws InfisicalException
|
||||
```
|
||||
|
||||
Delete a secret in Infisical.
|
||||
|
||||
#### Methods
|
||||
|
||||
<ParamField query="Parameters" type="object" optional>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="setSecretName()" type="string">
|
||||
The key of the secret to update.
|
||||
</ParamField>
|
||||
<ParamField query="setProjectID()" type="string" required>
|
||||
The project ID where the secret lives in.
|
||||
</ParamField>
|
||||
<ParamField query="setEnvironment()" type="string" required>
|
||||
The slug name (dev, prod, etc) of the environment from where secrets should be fetched from.
|
||||
</ParamField>
|
||||
<ParamField query="setPath()" type="string" optional>
|
||||
The path from where secret should be deleted.
|
||||
</ParamField>
|
||||
<ParamField query="setType()" 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
|
||||
|
||||
### Create a symmetric key
|
||||
|
||||
Create a base64-encoded, 256-bit symmetric key to be used for encryption/decryption.
|
||||
|
||||
```java
|
||||
String key = client.createSymmetricKey();
|
||||
List<Secret> secrets = sdk.Secrets().ListSecrets(
|
||||
"<project-id>",
|
||||
"<env-slug>", // dev, prod, staging, etc.
|
||||
"/secret/path", // `/` is the root folder
|
||||
false, // Should expand secret references
|
||||
false, // Should get secrets recursively from sub folders
|
||||
false, // Should include imports
|
||||
false // Should set the fetched secrets as key/value pairs on the system properties. Makes the secrets accessible as System.getProperty("<secret-key>")
|
||||
);
|
||||
```
|
||||
|
||||
#### Returns (string)
|
||||
`key` (string): A base64-encoded, 256-bit symmetric key, that can be used for encryption/decryption purposes.
|
||||
**Parameters:**
|
||||
- `projectId` (string): The ID of your project.
|
||||
- `environmentSlug` (string): The environment in which to list secrets (e.g., "dev").
|
||||
- `secretPath` (string): The path to the secrets.
|
||||
- `expandSecretReferences` (boolean): Whether to expand secret references.
|
||||
- `recursive` (boolean): Whether to list secrets recursively.
|
||||
- `includeImports` (boolean): Whether to include imported secrets.
|
||||
- `setSecretsOnSystemProperties` (boolean): Set the retrieved secrets as key/value pairs on the system properties, making them accessible through `System.getProperty("<secret-key>")`
|
||||
|
||||
**Returns:**
|
||||
- `List<Secret>`: The response containing the list of secrets.
|
||||
|
||||
#### Create Secret
|
||||
|
||||
|
||||
### Encrypt symmetric
|
||||
```java
|
||||
EncryptSymmetricOptions options = new EncryptSymmetricOptions();
|
||||
options.setKey(key);
|
||||
options.setPlaintext("Infisical is awesome!");
|
||||
|
||||
EncryptSymmetricResponse encryptedData = client.encryptSymmetric(options);
|
||||
public Secret CreateSecret(
|
||||
String secretName,
|
||||
String secretValue,
|
||||
String projectId,
|
||||
String environmentSlug,
|
||||
String secretPath
|
||||
)
|
||||
throws InfisicalException
|
||||
```
|
||||
|
||||
#### Methods
|
||||
|
||||
<ParamField query="Parameters" type="object" required>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="setPlaintext()" type="string">
|
||||
The plaintext you want to encrypt.
|
||||
</ParamField>
|
||||
<ParamField query="setKey()" type="string" required>
|
||||
The symmetric key to use for encryption.
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
</ParamField>
|
||||
|
||||
#### Returns (object)
|
||||
`tag (getTag())` (string): A base64-encoded, 128-bit authentication tag.
|
||||
`iv (getIv())` (string): A base64-encoded, 96-bit initialization vector.
|
||||
`ciphertext (getCipherText())` (string): A base64-encoded, encrypted ciphertext.
|
||||
|
||||
|
||||
### Decrypt symmetric
|
||||
```java
|
||||
DecryptSymmetricOptions decryptOptions = new DecryptSymmetricOptions();
|
||||
decryptOptions.setKey(key);
|
||||
decryptOptions.setCiphertext(encryptedData.getCiphertext());
|
||||
decryptOptions.setIv(encryptedData.getIv());
|
||||
decryptOptions.setTag(encryptedData.getTag());
|
||||
|
||||
String decryptedString = client.decryptSymmetric(decryptOptions);
|
||||
Secret newSecret = sdk.Secrets().CreateSecret(
|
||||
"NEW_SECRET_NAME",
|
||||
"secret-value",
|
||||
"<project-id>",
|
||||
"<env-slug>", // dev, prod, staging, etc.
|
||||
"/secret/path", // `/` is the root folder
|
||||
);
|
||||
```
|
||||
|
||||
#### Methods
|
||||
<ParamField query="Parameters" type="object" required>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="setCiphertext()" type="string">
|
||||
The ciphertext you want to decrypt.
|
||||
</ParamField>
|
||||
<ParamField query="setKey()" type="string" required>
|
||||
The symmetric key to use for encryption.
|
||||
</ParamField>
|
||||
<ParamField query="setIv()" type="string" required>
|
||||
The initialization vector to use for decryption.
|
||||
</ParamField>
|
||||
<ParamField query="setTag()" type="string" required>
|
||||
The authentication tag to use for decryption.
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
</ParamField>
|
||||
**Parameters:**
|
||||
- `secretName` (string): The name of the secret to create
|
||||
- `secretValue` (string): The value of the secret.
|
||||
- `projectId` (string): The ID of your project.
|
||||
- `environmentSlug` (string): The environment in which to create the secret.
|
||||
- `secretPath` (string, optional): The path to the secret.
|
||||
|
||||
#### Returns (string)
|
||||
`Plaintext` (string): The decrypted plaintext.
|
||||
*/}
|
||||
**Returns:**
|
||||
- `Secret`: The created secret.
|
||||
|
||||
#### Update Secret
|
||||
|
||||
```java
|
||||
public Secret UpdateSecret(
|
||||
String secretName,
|
||||
String projectId,
|
||||
String environmentSlug,
|
||||
String secretPath,
|
||||
String newSecretValue,
|
||||
String newSecretName
|
||||
)
|
||||
|
||||
throws InfisicalException
|
||||
```
|
||||
|
||||
|
||||
```java
|
||||
Secret updatedSecret = sdk.Secrets().UpdateSecret(
|
||||
"SECRET_NAME",
|
||||
"<project-id>",
|
||||
"<env-slug>", // dev, prod, staging, etc.
|
||||
"/secret/path", // `/` is the root folder
|
||||
"NEW_SECRET_VALUE", // nullable
|
||||
"NEW_SECRET_NAME" // nullable
|
||||
);
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `secretName` (string): The name of the secret to update.
|
||||
- `projectId` (string): The ID of your project.
|
||||
- `environmentSlug` (string): The environment in which to update the secret.
|
||||
- `secretPath` (string): The path to the secret.
|
||||
- `newSecretValue` (string, nullable): The new value of the secret.
|
||||
- `newSecretName` (string, nullable): A new name for the secret.
|
||||
|
||||
**Returns:**
|
||||
- `Secret`: The updated secret.
|
||||
|
||||
#### Get Secret by Name
|
||||
|
||||
```java
|
||||
public Secret GetSecret(
|
||||
String secretName,
|
||||
String projectId,
|
||||
String environmentSlug,
|
||||
String secretPath,
|
||||
Boolean expandSecretReferences,
|
||||
Boolean includeImports,
|
||||
String secretType
|
||||
)
|
||||
throws InfisicalException
|
||||
```
|
||||
|
||||
```java
|
||||
Secret secret = sdk.Secrets().GetSecret(
|
||||
"SECRET_NAME",
|
||||
"<project-id>",
|
||||
"<env-slug>", // dev, prod, staging, etc.
|
||||
"/secret/path", // `/` is the root folder
|
||||
false, // Should expand secret references
|
||||
false, // Should get secrets recursively from sub folders
|
||||
false, // Should include imports
|
||||
"shared" // Optional Secret Type (defaults to "shared")
|
||||
);
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `secretName` (string): The name of the secret to get`
|
||||
- `projectId` (string): The ID of your project.
|
||||
- `environmentSlug` (string): The environment in which to retrieve the secret.
|
||||
- `secretPath` (string): The path to the secret.
|
||||
- `expandSecretReferences` (boolean, optional): Whether to expand secret references.
|
||||
- `includeImports` (boolean, optional): Whether to include imported secrets.
|
||||
- `secretType` (personal | shared, optional): The type of secret to fetch.
|
||||
|
||||
|
||||
**Returns:**
|
||||
- `Secret`: The fetched secret.
|
||||
|
||||
#### Delete Secret by Name
|
||||
|
||||
```java
|
||||
public Secret DeleteSecret(
|
||||
String secretName,
|
||||
String projectId,
|
||||
String environmentSlug,
|
||||
String secretPath
|
||||
)
|
||||
throws InfisicalException
|
||||
```
|
||||
|
||||
```java
|
||||
Secret deletedSecret = sdk.Secrets().DeleteSecret(
|
||||
"SECRET_NAME",
|
||||
"<project-id>",
|
||||
"<env-slug>", // dev, prod, staging, etc.
|
||||
"/secret/path", // `/` is the root folder
|
||||
);
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `secretName` (string): The name of the secret to delete.
|
||||
- `projectId` (string): The ID of your project.
|
||||
- `environmentSlug` (string): The environment in which to delete the secret.
|
||||
- `secretPath` (string, optional): The path to the secret.
|
||||
|
||||
**Returns:**
|
||||
- `Secret`: The deleted secret.
|
||||
|
||||
|
||||
### `Folders`
|
||||
|
||||
#### Get Folder By Name
|
||||
|
||||
```java
|
||||
public Folder Get(
|
||||
String folderId
|
||||
);
|
||||
throws InfisicalException
|
||||
```
|
||||
|
||||
```java
|
||||
Folder folder = sdk.Folders().Get("<folder-id>");
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `folderId` (String): The ID of the folder to retrieve.
|
||||
|
||||
**Returns:**
|
||||
- `Folder`: The retrieved folder.
|
||||
|
||||
#### List Folders
|
||||
|
||||
```java
|
||||
public List<Folder> List(
|
||||
ListFoldersInput input
|
||||
)
|
||||
throws InfisicalException
|
||||
```
|
||||
|
||||
```java
|
||||
ListFoldersInput input = ListFoldersInput
|
||||
.builder()
|
||||
.projectId("<your-project-id>")
|
||||
.environmentSlug("<env-slug>")
|
||||
.folderPath("/")
|
||||
.recursive(false)
|
||||
.build();
|
||||
|
||||
List<Folder> folders = sdk.Folders().List(input);
|
||||
```
|
||||
|
||||
|
||||
**Parameters:**
|
||||
- `input` (ListFoldersInput): The input for listing folders.
|
||||
- `projectId` (String): The ID of the project to list folders from.
|
||||
- `environmentSlug` (String): The slug of the environment to list folders from.
|
||||
- `folderPath` (String): The path to list folders from. Defaults to `/`.
|
||||
- `recursive` (Boolean): Whether or not to list sub-folders recursively from the specified folder path and downwards. Defaults to `false`.
|
||||
|
||||
**Returns:**
|
||||
- `List<Folder>`: The retrieved folders.
|
||||
|
||||
#### Create Folder
|
||||
|
||||
```java
|
||||
public Folder Create(
|
||||
CreateFolderInput input
|
||||
)
|
||||
throws InfisicalException
|
||||
```
|
||||
|
||||
```java
|
||||
var input = CreateFolderInput
|
||||
.builder()
|
||||
.projectId("<your-project-id>")
|
||||
.environmentSlug("<env-slug>")
|
||||
.folderName("<folder-name>")
|
||||
.folderPath("/")
|
||||
.description("Optional folder description")
|
||||
.build();
|
||||
|
||||
Folder createdFolder = sdk.Folders().Create(input);
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `input` (CreateFolderInput): The input for creating a folder.
|
||||
- `projectId` (String): The ID of the project to create the folder in.
|
||||
- `environmentSlug` (String): The slug of the environment to create the folder in.
|
||||
- `folderPath` (String): The path to create the folder in. Defaults to `/`.
|
||||
- `folderName` (String): The name of the folder to create.
|
||||
- `description` (String): The description of the folder to create. This is optional.
|
||||
|
||||
**Returns:**
|
||||
- `Folder`: The created folder.
|
||||
|
||||
#### Update Folder
|
||||
|
||||
```java
|
||||
public Folder Update(
|
||||
UpdateFolderInput input
|
||||
)
|
||||
throws InfisicalException
|
||||
```
|
||||
|
||||
```java
|
||||
var input = UpdateFolderInput
|
||||
.builder()
|
||||
.projectId("<your-project-id>")
|
||||
.environmentSlug("<env-slug>")
|
||||
.folderId("<id-of-folder-to-update>")
|
||||
.newFolderName("<the-new-folder-name>")
|
||||
.folderPath("/")
|
||||
.build();
|
||||
|
||||
Folder updatedFolder = sdk.Folders().Update(input);
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `input` (UpdateFolderInput): The input for updating a folder.
|
||||
- `projectId` (String): The ID of the project where the folder exists.
|
||||
- `environmentSlug` (String): The slug of the environment where the folder exists.
|
||||
- `folderPath` (String): The path of the folder to update.
|
||||
- `folderId` (String): The ID of the folder to update.
|
||||
- `newFolderName` (String): The new folder name.
|
||||
|
||||
**Returns:**
|
||||
- `Folder`: The updated folder.
|
||||
|
||||
#### Delete Folder
|
||||
|
||||
```java
|
||||
public Folder Delete(
|
||||
DeleteFolderInput input
|
||||
)
|
||||
throws InfisicalException
|
||||
```
|
||||
|
||||
```java
|
||||
var input = DeleteFolderInput
|
||||
.builder()
|
||||
.folderId("<the-folder-id>")
|
||||
.environmentSlug("<env-slug>")
|
||||
.projectId("<your-project-id>")
|
||||
.build();
|
||||
|
||||
Folder deletedFolder = sdk.Folders().Delete(input);
|
||||
```
|
||||
|
||||
|
||||
**Parameters:**
|
||||
- `input` (DeleteFolderInput): The input for deleting a folder.
|
||||
- `projectId` (String): The ID of the project where the folder exists.
|
||||
- `environmentSlug` (String): The slug of the environment where the folder exists.
|
||||
- `folderId` (String): The ID of the folder to delete.
|
||||
|
||||
**Returns:**
|
||||
- `Folder`: The deleted folder.
|
File diff suppressed because it is too large
Load Diff
@@ -1,533 +1,403 @@
|
||||
---
|
||||
title: "Infisical Python SDK"
|
||||
sidebarTitle: "Python"
|
||||
url: "https://github.com/Infisical/python-sdk-official?tab=readme-ov-file#infisical-python-sdk"
|
||||
icon: "/images/sdks/languages/python.svg"
|
||||
---
|
||||
|
||||
{/* If you're working with Python, the official [infisical-python](https://github.com/Infisical/sdk/edit/main/crates/infisical-py) package is the easiest way to fetch and work with secrets for your application.
|
||||
The Infisical SDK provides a convenient way to interact with the Infisical API.
|
||||
|
||||
- [PyPi Package](https://pypi.org/project/infisical-python/)
|
||||
- [Github Repository](https://github.com/Infisical/sdk/edit/main/crates/infisical-py)
|
||||
### Migrating to version 1.0.3 or above
|
||||
|
||||
## Basic Usage
|
||||
We have recently rolled out our first stable version of the SDK, version `1.0.3` and above.
|
||||
|
||||
```py
|
||||
from flask import Flask
|
||||
from infisical_client import ClientSettings, InfisicalClient, GetSecretOptions, AuthenticationOptions, UniversalAuthMethod
|
||||
The 1.0.3 version comes with a few key changes that may change how you're using the SDK.
|
||||
1. **Removal of `rest`**: The SDK no longer exposes the entire Infisical API. This was nessecary as we have moved away from using an OpenAPI generator approach. We aim to add support for more API resources in the near future. If you have any specific requests, please [open an issue](https://github.com/Infisical/python-sdk-official/issues).
|
||||
|
||||
app = Flask(__name__)
|
||||
2. **New response types**: The 1.0.3 release uses return types that differ from the older versions. The new return types such as `BaseSecret`, are all exported from the Infisical SDK.
|
||||
|
||||
client = InfisicalClient(ClientSettings(
|
||||
auth=AuthenticationOptions(
|
||||
universal_auth=UniversalAuthMethod(
|
||||
client_id="CLIENT_ID",
|
||||
client_secret="CLIENT_SECRET",
|
||||
)
|
||||
)
|
||||
))
|
||||
3. **Property renaming**: Some properties on the responses have been slightly renamed. An example of this would be that the `secret_key` property on the `get_secret_by_name()` method, that has been renamed to `secretKey`.
|
||||
|
||||
@app.route("/")
|
||||
def hello_world():
|
||||
# access value
|
||||
With this in mind, you're ready to upgrade your SDK version to `1.0.3` or above.
|
||||
|
||||
name = client.getSecret(options=GetSecretOptions(
|
||||
environment="dev",
|
||||
project_id="PROJECT_ID",
|
||||
secret_name="NAME"
|
||||
))
|
||||
You can refer to our [legacy documentation](https://github.com/Infisical/python-sdk-official/tree/9b0403938ee5ae599d42c5f1fdf9158671a15606?tab=readme-ov-file#infisical-python-sdk) if need be.
|
||||
|
||||
return f"Hello! My name is: {name.secret_value}"
|
||||
```
|
||||
## Requirements
|
||||
|
||||
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.
|
||||
</Warning>
|
||||
Python 3.7+
|
||||
|
||||
## Installation
|
||||
|
||||
Run `pip` to add `infisical-python` to your project
|
||||
|
||||
```console
|
||||
$ pip install infisical-python
|
||||
```bash
|
||||
pip install infisicalsdk
|
||||
```
|
||||
|
||||
Note: You need Python 3.7+.
|
||||
## Getting Started
|
||||
|
||||
## Configuration
|
||||
```python
|
||||
from infisical_sdk import InfisicalSDKClient
|
||||
|
||||
Import the SDK and create a client instance with your [Machine Identity](/api-reference/overview/authentication).
|
||||
# Initialize the client
|
||||
client = InfisicalSDKClient(host="https://app.infisical.com")
|
||||
|
||||
```py
|
||||
from infisical_client import ClientSettings, InfisicalClient, AuthenticationOptions, UniversalAuthMethod
|
||||
# Authenticate (example using Universal Auth)
|
||||
client.auth.universal_auth.login(
|
||||
client_id="<machine-identity-client-id>",
|
||||
client_secret="<machine-identity-client-secret>"
|
||||
)
|
||||
|
||||
client = InfisicalClient(ClientSettings(
|
||||
auth=AuthenticationOptions(
|
||||
universal_auth=UniversalAuthMethod(
|
||||
client_id="CLIENT_ID",
|
||||
client_secret="CLIENT_SECRET",
|
||||
)
|
||||
)
|
||||
))
|
||||
# Use the SDK to interact with Infisical
|
||||
secrets = client.secrets.list_secrets(project_id="<project-id>", environment_slug="dev", secret_path="/")
|
||||
```
|
||||
|
||||
#### Parameters
|
||||
## InfisicalSDKClient Parameters
|
||||
|
||||
<ParamField query="options" type="object">
|
||||
<Expandable title="properties">
|
||||
<ParamField query="client_id" type="string" deprecated optional>
|
||||
Your Infisical Client ID.
|
||||
The `InfisicalSDKClient` takes the following parameters, which are used as a global configuration for the lifetime of the SDK instance.
|
||||
|
||||
**This field is deprecated and will be removed in future versions.** Please use the `auth` field instead.
|
||||
</ParamField>
|
||||
<ParamField query="client_secret" type="string" deprecated optional>
|
||||
Your Infisical Client Secret.
|
||||
- **host** (`str`, _Optional_): The host URL for your Infisical instance. Defaults to `https://app.infisical.com`.
|
||||
- **token** (`str`, _Optional_): Specify an authentication token to use for all requests. If provided, you will not need to call any of the `auth` methods. Defaults to `None`
|
||||
- **cache_ttl** (`int`, _Optional_): The SDK has built-in client-side caching for secrets, greatly improving response times. By default, secrets are cached for 1 minute (60 seconds). You can disable caching by setting `cache_ttl` to `None`, or adjust the duration in seconds as needed.
|
||||
|
||||
**This field is deprecated and will be removed in future versions.** Please use the `auth` field instead.
|
||||
</ParamField>
|
||||
<ParamField query="access_token" type="string" deprecated optional>
|
||||
If you want to directly pass an access token obtained from the authentication endpoints, you can do so.
|
||||
```python
|
||||
client = InfisicalSDKClient(
|
||||
host="https://app.infisical.com", # Defaults to https://app.infisical.com
|
||||
token="<optional-auth-token>", # If not set, use the client.auth() methods.
|
||||
cache_ttl = 300 # `None` to disable caching
|
||||
)
|
||||
```
|
||||
|
||||
**This field is deprecated and will be removed in future versions.** Please use the `auth` field instead.
|
||||
</ParamField>
|
||||
## Core Methods
|
||||
|
||||
<ParamField query="cache_ttl" type="number" default="300" optional>
|
||||
Time-to-live (in seconds) for refreshing cached secrets.
|
||||
If manually set to 0, caching will be disabled, this is not recommended.
|
||||
</ParamField>
|
||||
The SDK methods are organized into the following high-level categories:
|
||||
|
||||
<ParamField query="site_url" type="string" default="https://app.infisical.com" optional>
|
||||
Your self-hosted absolute site URL including the protocol (e.g. `https://app.infisical.com`)
|
||||
</ParamField>
|
||||
1. `auth`: Handles authentication methods.
|
||||
2. `secrets`: Manages CRUD operations for secrets.
|
||||
3. `kms`: Perform cryptographic operations with Infisical KMS.
|
||||
|
||||
<ParamField query="ssl_certificate_path" optional>
|
||||
Optionally provide a path to a custom SSL certificate file. This can be substituted by setting the `INFISICAL_SSL_CERTIFICATE` environment variable to the contents of the certificate.
|
||||
</ParamField>
|
||||
### `auth`
|
||||
|
||||
<ParamField query="auth" type="AuthenticationOptions">
|
||||
The authentication object to use for the client. This is required unless you're using environment variables.
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
|
||||
</ParamField>
|
||||
|
||||
### Authentication
|
||||
|
||||
The SDK supports a variety of authentication methods. The most common authentication method is Universal Auth, which uses a client ID and client secret to authenticate.
|
||||
The `Auth` component provides methods for authentication:
|
||||
|
||||
#### Universal Auth
|
||||
|
||||
**Using environment variables**
|
||||
- `INFISICAL_UNIVERSAL_AUTH_CLIENT_ID` - Your machine identity client ID.
|
||||
- `INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET` - Your machine identity client secret.
|
||||
|
||||
**Using the SDK directly**
|
||||
```python3
|
||||
from infisical_client import ClientSettings, InfisicalClient, AuthenticationOptions, UniversalAuthMethod
|
||||
|
||||
client = InfisicalClient(ClientSettings(
|
||||
auth=AuthenticationOptions(
|
||||
universal_auth=UniversalAuthMethod(
|
||||
client_id="CLIENT_ID",
|
||||
client_secret="CLIENT_SECRET",
|
||||
)
|
||||
)
|
||||
))
|
||||
```
|
||||
|
||||
#### GCP ID Token Auth
|
||||
<Info>
|
||||
Please note that this authentication method will only work if you're running your application on Google Cloud Platform.
|
||||
Please [read more](/documentation/platform/identities/gcp-auth) about this authentication method.
|
||||
</Info>
|
||||
|
||||
**Using environment variables**
|
||||
- `INFISICAL_GCP_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID.
|
||||
|
||||
**Using the SDK directly**
|
||||
```py
|
||||
from infisical_client import ClientSettings, InfisicalClient, AuthenticationOptions, GCPIDTokenAuthMethod
|
||||
|
||||
client = InfisicalClient(ClientSettings(
|
||||
auth=AuthenticationOptions(
|
||||
gcp_id_token=GCPIDTokenAuthMethod(
|
||||
identity_id="MACHINE_IDENTITY_ID",
|
||||
)
|
||||
)
|
||||
))
|
||||
```
|
||||
|
||||
#### GCP IAM Auth
|
||||
|
||||
**Using environment variables**
|
||||
- `INFISICAL_GCP_IAM_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID.
|
||||
- `INFISICAL_GCP_IAM_SERVICE_ACCOUNT_KEY_FILE_PATH` - The path to your GCP service account key file.
|
||||
|
||||
**Using the SDK directly**
|
||||
```py
|
||||
from infisical_client import ClientSettings, InfisicalClient, AuthenticationOptions, GCPIamAuthMethod
|
||||
|
||||
|
||||
client = InfisicalClient(ClientSettings(
|
||||
auth=AuthenticationOptions(
|
||||
gcp_iam=GCPIamAuthMethod(
|
||||
identity_id="MACHINE_IDENTITY_ID",
|
||||
service_account_key_file_path="./path/to/service_account_key.json"
|
||||
)
|
||||
)
|
||||
))
|
||||
```
|
||||
|
||||
#### AWS IAM Auth
|
||||
<Info>
|
||||
Please note that this authentication method will only work if you're running your application on AWS.
|
||||
Please [read more](/documentation/platform/identities/aws-auth) about this authentication method.
|
||||
</Info>
|
||||
|
||||
**Using environment variables**
|
||||
- `INFISICAL_AWS_IAM_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID.
|
||||
|
||||
**Using the SDK directly**
|
||||
```py
|
||||
from infisical_client import ClientSettings, InfisicalClient, AuthenticationOptions, AWSIamAuthMethod
|
||||
|
||||
client = InfisicalClient(ClientSettings(
|
||||
auth=AuthenticationOptions(
|
||||
aws_iam=AWSIamAuthMethod(identity_id="MACHINE_IDENTITY_ID")
|
||||
)
|
||||
))
|
||||
```
|
||||
|
||||
#### Azure Auth
|
||||
<Info>
|
||||
Please note that this authentication method will only work if you're running your application on Azure.
|
||||
Please [read more](/documentation/platform/identities/azure-auth) about this authentication method.
|
||||
</Info>
|
||||
|
||||
**Using environment variables**
|
||||
- `INFISICAL_AZURE_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID.
|
||||
|
||||
**Using the SDK directly**
|
||||
```python
|
||||
from infisical_client import InfisicalClient, ClientSettings, AuthenticationOptions, AzureAuthMethod
|
||||
|
||||
kubernetes_client = InfisicalClient(ClientSettings(
|
||||
auth=AuthenticationOptions(
|
||||
azure=AzureAuthMethod(
|
||||
identity_id="YOUR_IDENTITY_ID",
|
||||
)
|
||||
)
|
||||
))
|
||||
response = client.auth.universal_auth.login(
|
||||
client_id="<machine-identity-client-id>",
|
||||
client_secret="<machine-identity-client-secret>"
|
||||
)
|
||||
```
|
||||
|
||||
#### AWS Auth
|
||||
|
||||
#### Kubernetes Auth
|
||||
<Info>
|
||||
Please note that this authentication method will only work if you're running your application on Kubernetes.
|
||||
Please [read more](/documentation/platform/identities/kubernetes-auth) about this authentication method.
|
||||
</Info>
|
||||
|
||||
**Using environment variables**
|
||||
- `INFISICAL_KUBERNETES_IDENTITY_ID` - Your Infisical Machine Identity ID.
|
||||
- `INFISICAL_KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH_ENV_NAME` - The environment variable name that contains the path to the service account token. This is optional and will default to `/var/run/secrets/kubernetes.io/serviceaccount/token`.
|
||||
|
||||
**Using the SDK directly**
|
||||
```python
|
||||
from infisical_client import InfisicalClient, ClientSettings, AuthenticationOptions, KubernetesAuthMethod
|
||||
|
||||
kubernetes_client = InfisicalClient(ClientSettings(
|
||||
auth=AuthenticationOptions(
|
||||
kubernetes=KubernetesAuthMethod(
|
||||
identity_id="YOUR_IDENTITY_ID",
|
||||
service_account_token_path="/var/run/secrets/kubernetes.io/serviceaccount/token" # Optional
|
||||
)
|
||||
)
|
||||
))
|
||||
response = client.auth.aws_auth.login(identity_id="<machine-identity-id>")
|
||||
```
|
||||
|
||||
### Caching
|
||||
### `secrets`
|
||||
|
||||
To reduce the number of API requests, the SDK temporarily stores secrets it retrieves. By default, a secret remains cached for 5 minutes after it's first fetched. Each time it's fetched again, this 5-minute timer resets. You can adjust this caching duration by setting the "cache_ttl" option when creating the client.
|
||||
This sub-class handles operations related to secrets:
|
||||
|
||||
## Working with Secrets
|
||||
#### List Secrets
|
||||
|
||||
### client.listSecrets(options)
|
||||
|
||||
```py
|
||||
client.listSecrets(options=ListSecretsOptions(
|
||||
environment="dev",
|
||||
project_id="PROJECT_ID"
|
||||
))
|
||||
```
|
||||
|
||||
Retrieve all secrets within the Infisical project and environment that client is connected to
|
||||
|
||||
#### Parameters
|
||||
|
||||
<ParamField query="Parameters" type="object">
|
||||
<Expandable title="properties">
|
||||
<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 secrets should be fetched from.
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="attach_to_process_env" type="boolean" default="false" optional>
|
||||
Whether or not to set the fetched secrets to the process environment. If true, you can access the secrets like so `process.env["SECRET_NAME"]`.
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="recursive" type="boolean" default="false" optional>
|
||||
Whether or not to fetch secrets recursively from the specified path. Please note that there's a 20-depth limit for recursive fetching.
|
||||
</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>
|
||||
|
||||
<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>
|
||||
</Expandable>
|
||||
|
||||
</ParamField>
|
||||
|
||||
### client.getSecret(options)
|
||||
|
||||
```py
|
||||
secret = client.getSecret(options=GetSecretOptions(
|
||||
environment="dev",
|
||||
project_id="PROJECT_ID",
|
||||
secret_name="API_KEY"
|
||||
))
|
||||
value = secret.secret_value # get its value
|
||||
```
|
||||
|
||||
By default, `getSecret()` fetches and returns a shared secret. If not found, it returns a personal secret.
|
||||
|
||||
#### 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>
|
||||
<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="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>
|
||||
</ParamField>
|
||||
|
||||
### client.createSecret(options)
|
||||
|
||||
```py
|
||||
api_key = client.createSecret(options=CreateSecretOptions(
|
||||
secret_name="API_KEY",
|
||||
secret_value="Some API Key",
|
||||
environment="dev",
|
||||
project_id="PROJECT_ID"
|
||||
))
|
||||
```
|
||||
|
||||
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>
|
||||
</ParamField>
|
||||
|
||||
### client.updateSecret(options)
|
||||
|
||||
```py
|
||||
client.updateSecret(options=UpdateSecretOptions(
|
||||
secret_name="API_KEY",
|
||||
secret_value="NEW_VALUE",
|
||||
environment="dev",
|
||||
project_id="PROJECT_ID"
|
||||
))
|
||||
```
|
||||
|
||||
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>
|
||||
</ParamField>
|
||||
|
||||
### client.deleteSecret(options)
|
||||
|
||||
```py
|
||||
client.deleteSecret(options=DeleteSecretOptions(
|
||||
environment="dev",
|
||||
project_id="PROJECT_ID",
|
||||
secret_name="API_KEY"
|
||||
))
|
||||
```
|
||||
|
||||
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>
|
||||
</ParamField>
|
||||
|
||||
## Cryptography
|
||||
|
||||
### Create a symmetric key
|
||||
|
||||
Create a base64-encoded, 256-bit symmetric key to be used for encryption/decryption.
|
||||
|
||||
```py
|
||||
key = client.createSymmetricKey()
|
||||
```
|
||||
|
||||
#### Returns (string)
|
||||
|
||||
`key` (string): A base64-encoded, 256-bit symmetric key, that can be used for encryption/decryption purposes.
|
||||
|
||||
### Encrypt symmetric
|
||||
|
||||
```py
|
||||
encryptOptions = EncryptSymmetricOptions(
|
||||
key=key,
|
||||
plaintext="Infisical is awesome!"
|
||||
```python
|
||||
secrets = client.secrets.list_secrets(
|
||||
project_id="<project-id>",
|
||||
environment_slug="dev",
|
||||
secret_path="/",
|
||||
expand_secret_references=True, # Optional
|
||||
view_secret_value=True, # Optional
|
||||
recursive=False, # Optional
|
||||
include_imports=True, # Optional
|
||||
tag_filters=[] # Optional
|
||||
)
|
||||
|
||||
encryptedData = client.encryptSymmetric(encryptOptions)
|
||||
```
|
||||
|
||||
#### Parameters
|
||||
**Parameters:**
|
||||
- `project_id` (str): The ID of your project.
|
||||
- `project_slug` (str): The slug of your project.
|
||||
- `environment_slug` (str): The environment in which to list secrets (e.g., "dev").
|
||||
- `secret_path` (str): The path to the secrets.
|
||||
- `expand_secret_references` (bool): Whether to expand secret references.
|
||||
- `view_secret_value` (bool): Whether or not to include the secret value in the response. If set to false, the `secretValue` will be masked with `<hidden-by-infisical>`. Defaults to true.
|
||||
- `recursive` (bool): Whether to list secrets recursively.
|
||||
- `include_imports` (bool): Whether to include imported secrets.
|
||||
- `tag_filters` (List[str]): Tags to filter secrets.
|
||||
|
||||
<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>
|
||||
</ParamField>
|
||||
**Note:** Exactly one of `project_id` or `project_slug` is required. If both are provided, `project_id` takes precedence.
|
||||
|
||||
#### Returns (object)
|
||||
**Returns:**
|
||||
- `ListSecretsResponse`: The response containing the list of secrets.
|
||||
|
||||
`tag` (string): A base64-encoded, 128-bit authentication tag. `iv` (string): A base64-encoded, 96-bit initialization vector. `ciphertext` (string): A base64-encoded, encrypted ciphertext.
|
||||
#### Create Secret
|
||||
|
||||
### Decrypt symmetric
|
||||
|
||||
```py
|
||||
decryptOptions = DecryptSymmetricOptions(
|
||||
ciphertext=encryptedData.ciphertext,
|
||||
iv=encryptedData.iv,
|
||||
tag=encryptedData.tag,
|
||||
key=key
|
||||
```python
|
||||
new_secret = client.secrets.create_secret_by_name(
|
||||
secret_name="NEW_SECRET",
|
||||
project_id="<project-id>",
|
||||
secret_path="/",
|
||||
environment_slug="dev",
|
||||
secret_value="secret_value",
|
||||
secret_comment="Optional comment",
|
||||
skip_multiline_encoding=False,
|
||||
secret_reminder_repeat_days=30, # Optional
|
||||
secret_reminder_note="Remember to update this secret", # Optional
|
||||
secret_metadata=[{"key": "metadata_key", "value": "metadata_value"}], # Optional
|
||||
tags_ids=["tag_id_1", "tag_id_2"] # Optional
|
||||
)
|
||||
|
||||
decryptedString = client.decryptSymmetric(decryptOptions)
|
||||
|
||||
|
||||
```
|
||||
|
||||
#### Parameters
|
||||
**Parameters:**
|
||||
- `secret_name` (str): The name of the secret.
|
||||
- `project_id` (str): The ID of your project.
|
||||
- `project_slug` (str): The slug of your project.
|
||||
- `secret_path` (str): The path to the secret.
|
||||
- `environment_slug` (str): The environment in which to create the secret.
|
||||
- `secret_value` (str): The value of the secret.
|
||||
- `secret_comment` (str, optional): A comment associated with the secret.
|
||||
- `skip_multiline_encoding` (bool, optional): Whether to skip encoding for multiline secrets.
|
||||
- `secret_reminder_repeat_days` (Union[float, int], optional): Number of days after which to repeat secret reminders.
|
||||
- `secret_reminder_note` (str, optional): A note for the secret reminder.
|
||||
- `secret_metadata` (List[Dict[str, Any]], optional): Metadata associated with the secret.
|
||||
- `tags_ids` (List[str], optional): IDs of tags to associate with the secret.
|
||||
|
||||
<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>
|
||||
</ParamField>
|
||||
**Note:** Exactly one of `project_id` or `project_slug` is required. If both are provided, `project_id` takes precedence.
|
||||
|
||||
#### Returns (string)
|
||||
**Returns:**
|
||||
- `BaseSecret`: The response after creating the secret.
|
||||
|
||||
`plaintext` (string): The decrypted plaintext. */}
|
||||
#### Update Secret
|
||||
|
||||
```python
|
||||
updated_secret = client.secrets.update_secret_by_name(
|
||||
current_secret_name="EXISTING_SECRET",
|
||||
project_id="<project-id>",
|
||||
project_slug="<project-slug>",
|
||||
secret_path="/",
|
||||
environment_slug="dev",
|
||||
secret_value="new_secret_value",
|
||||
secret_comment="Updated comment", # Optional
|
||||
skip_multiline_encoding=False,
|
||||
secret_reminder_repeat_days=30, # Optional
|
||||
secret_reminder_note="Updated reminder note", # Optional
|
||||
new_secret_name="NEW_NAME", # Optional
|
||||
secret_metadata=[{"key": "metadata_key", "value": "metadata_value"}], # Optional
|
||||
tags_ids=["tag_id_1", "tag_id_2"] # Optional
|
||||
)
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `current_secret_name` (str): The current name of the secret.
|
||||
- `project_id` (str): The ID of your project.
|
||||
- `project_slug` (str): The slug of your project.
|
||||
- `secret_path` (str): The path to the secret.
|
||||
- `environment_slug` (str): The environment in which to update the secret.
|
||||
- `secret_value` (str, optional): The new value of the secret.
|
||||
- `secret_comment` (str, optional): An updated comment associated with the secret.
|
||||
- `skip_multiline_encoding` (bool, optional): Whether to skip encoding for multiline secrets.
|
||||
- `secret_reminder_repeat_days` (Union[float, int], optional): Updated number of days after which to repeat secret reminders.
|
||||
- `secret_reminder_note` (str, optional): An updated note for the secret reminder.
|
||||
- `new_secret_name` (str, optional): A new name for the secret.
|
||||
- `secret_metadata` (List[Dict[str, Any]], optional): Metadata associated with the secret.
|
||||
- `tags_ids` (List[str], optional): IDs of tags to associate with the secret.
|
||||
|
||||
**Note:** Exactly one of `project_id` or `project_slug` is required. If both are provided, `project_id` takes precedence.
|
||||
|
||||
**Returns:**
|
||||
- `BaseSecret`: The response after updating the secret.
|
||||
|
||||
#### Get Secret by Name
|
||||
|
||||
```python
|
||||
secret = client.secrets.get_secret_by_name(
|
||||
secret_name="EXISTING_SECRET",
|
||||
project_id="<project-id>",
|
||||
environment_slug="dev",
|
||||
secret_path="/",
|
||||
expand_secret_references=True, # Optional
|
||||
view_secret_value=True, # Optional
|
||||
include_imports=True, # Optional
|
||||
version=None # Optional
|
||||
)
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `secret_name` (str): The name of the secret.
|
||||
- `project_id` (str): The ID of your project.
|
||||
- `project_slug` (str): The slug of your project.
|
||||
- `environment_slug` (str): The environment in which to retrieve the secret.
|
||||
- `secret_path` (str): The path to the secret.
|
||||
- `expand_secret_references` (bool): Whether to expand secret references.
|
||||
- `view_secret_value` (bool): Whether or not to include the secret value in the response. If set to false, the `secretValue` will be masked with `<hidden-by-infisical>`. Defaults to true.
|
||||
- `include_imports` (bool): Whether to include imported secrets.
|
||||
- `version` (str, optional): The version of the secret to retrieve. Fetches the latest by default.
|
||||
|
||||
**Note:** Exactly one of `project_id` or `project_slug` is required. If both are provided, `project_id` takes precedence.
|
||||
|
||||
**Returns:**
|
||||
- `BaseSecret`: The response containing the secret.
|
||||
|
||||
#### Delete Secret by Name
|
||||
|
||||
```python
|
||||
deleted_secret = client.secrets.delete_secret_by_name(
|
||||
secret_name="EXISTING_SECRET",
|
||||
project_id="<project-id>",
|
||||
environment_slug="dev",
|
||||
secret_path="/"
|
||||
)
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `secret_name` (str): The name of the secret to delete.
|
||||
- `project_id` (str): The ID of your project.
|
||||
- `project_slug` (str): The slug of your project.
|
||||
- `environment_slug` (str): The environment in which to delete the secret.
|
||||
- `secret_path` (str): The path to the secret.
|
||||
|
||||
**Note:** Exactly one of `project_id` or `project_slug` is required. If both are provided, `project_id` takes precedence.
|
||||
|
||||
**Returns:**
|
||||
- `BaseSecret`: The response after deleting the secret.
|
||||
|
||||
### `kms`
|
||||
|
||||
This sub-class handles KMS related operations:
|
||||
|
||||
#### List KMS Keys
|
||||
|
||||
```python
|
||||
kms_keys = client.kms.list_keys(
|
||||
project_id="<project-id>",
|
||||
offset=0, # Optional
|
||||
limit=100, # Optional
|
||||
order_by=KmsKeysOrderBy.NAME, # Optional
|
||||
order_direction=OrderDirection.ASC, # Optional
|
||||
search=None # Optional
|
||||
)
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `project_id` (str): The ID of your project.
|
||||
- `offset` (int, optional): The offset to paginate from.
|
||||
- `limit` (int, optional): The page size for paginating.
|
||||
- `order_by` (KmsKeysOrderBy, optional): The key property to order the list response by.
|
||||
- `order_direction` (OrderDirection, optional): The direction to order the list response in.
|
||||
- `search` (str, optional): The text value to filter key names by.
|
||||
|
||||
**Returns:**
|
||||
- `ListKmsKeysResponse`: The response containing the list of KMS keys.
|
||||
|
||||
#### Get KMS Key by ID
|
||||
|
||||
```python
|
||||
kms_key = client.kms.get_key_by_id(
|
||||
key_id="<key-id>"
|
||||
)
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `key_id` (str): The ID of the key to retrieve.
|
||||
|
||||
**Returns:**
|
||||
- `KmsKey`: The specified key.
|
||||
|
||||
#### Get KMS Key by Name
|
||||
|
||||
```python
|
||||
kms_key = client.kms.get_key_by_name(
|
||||
key_name="my-key",
|
||||
project_id="<project-id>"
|
||||
)
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `key_name` (str): The name of the key to retrieve.
|
||||
- `project_id` (str): The ID of your project.
|
||||
|
||||
**Returns:**
|
||||
- `KmsKey`: The specified key.
|
||||
|
||||
#### Create KMS Key
|
||||
|
||||
```python
|
||||
kms_key = client.kms.create_key(
|
||||
name="my-key",
|
||||
project_id="<project-id>",
|
||||
encryption_algorithm=SymmetricEncryption.AES_GCM_256,
|
||||
description=None # Optional
|
||||
)
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `name` (str): The name of the key (must be slug-friendly).
|
||||
- `project_id` (str): The ID of your project.
|
||||
- `encryption_algorithm` (SymmetricEncryption): The encryption algorithm this key should use.
|
||||
- `description` (str, optional): A description of your key.
|
||||
|
||||
**Returns:**
|
||||
- `KmsKey`: The newly created key.
|
||||
|
||||
#### Update KMS Key
|
||||
|
||||
```python
|
||||
updated_key = client.kms.update_key(
|
||||
key_id="<key-id>",
|
||||
name="my-updated-key", # Optional
|
||||
description="Updated description", # Optional
|
||||
is_disabled=True # Optional
|
||||
)
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `key_id` (str): The ID of the key to be updated.
|
||||
- `name` (str, optional): The updated name of the key (must be slug-friendly).
|
||||
- `description` (str): The updated description of the key.
|
||||
- `is_disabled` (str): The flag to disable operations with this key.
|
||||
|
||||
**Returns:**
|
||||
- `KmsKey`: The updated key.
|
||||
|
||||
#### Delete KMS Key
|
||||
|
||||
```python
|
||||
deleted_key = client.kms.delete_key(
|
||||
key_id="<key-id>"
|
||||
)
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `key_id` (str): The ID of the key to be deleted.
|
||||
|
||||
**Returns:**
|
||||
- `KmsKey`: The deleted key.
|
||||
|
||||
#### Encrypt Data with KMS Key
|
||||
|
||||
```python
|
||||
encrypted_data = client.kms.encrypt_data(
|
||||
key_id="<key-id>",
|
||||
base64EncodedPlaintext="TXkgc2VjcmV0IG1lc3NhZ2U=" # must be base64 encoded
|
||||
)
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `key_id` (str): The ID of the key to encrypt the data with.
|
||||
- `base64EncodedPlaintext` (str): The plaintext data to encrypt (must be base64 encoded).
|
||||
|
||||
**Returns:**
|
||||
- `str`: The encrypted ciphertext.
|
||||
|
||||
#### Decrypt Data with KMS Key
|
||||
|
||||
```python
|
||||
decrypted_data = client.kms.decrypt_data(
|
||||
key_id="<key-id>",
|
||||
ciphertext="Aq96Ry7sMH3k/ogaIB5MiSfH+LblQRBu69lcJe0GfIvI48ZvbWY+9JulyoQYdjAx"
|
||||
)
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `key_id` (str): The ID of the key to decrypt the data with.
|
||||
- `ciphertext` (str): The ciphertext returned from the encrypt operation.
|
||||
|
||||
**Returns:**
|
||||
- `str`: The base64 encoded plaintext.
|
@@ -4,8 +4,6 @@ sidebarTitle: "Ruby"
|
||||
icon: "/images/sdks/languages/ruby.svg"
|
||||
---
|
||||
|
||||
|
||||
|
||||
If you're working with Ruby, the official [Infisical Ruby SDK](https://github.com/infisical/sdk) package is the easiest way to fetch and work with secrets for your application.
|
||||
|
||||
- [Ruby Package](https://rubygems.org/gems/infisical-sdk)
|
||||
|
@@ -2,5 +2,494 @@
|
||||
title: "Infisical Rust SDK"
|
||||
sidebarTitle: "Rust"
|
||||
icon: "/images/sdks/languages/rust.svg"
|
||||
url: "https://github.com/Infisical/rust-sdk?tab=readme-ov-file#infisical--the-official-infisical-rust-sdk"
|
||||
---
|
||||
---
|
||||
|
||||
The Infisical Rust SDK ([docs.rs](https://docs.rs/infisical)) provides a convenient and ergonomic way to interact with Infisical programmatically using modern and idiomatic Rust.
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
cargo add infisical
|
||||
```
|
||||
|
||||
### Getting Started
|
||||
|
||||
The easiest way to get started is to use the builder pattern for both the client and your requests.
|
||||
|
||||
```rust
|
||||
use infisical::{AuthMethod, Client, InfisicalError, encode_base64, decode_base64};
|
||||
use infisical::secrets::GetSecretRequest;
|
||||
|
||||
async fn fetch_secret() -> Result<(), InfisicalError> {
|
||||
// 1. Build the client. You can chain methods to configure it.
|
||||
let mut client = Client::builder()
|
||||
.base_url("https://app.infisical.com") // Optional: defaults to https://app.infisical.com
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
// 2. Set up your authentication method and log in.
|
||||
let auth_method = AuthMethod::new_universal_auth("<your-client-id>", "<your-client-secret>");
|
||||
client.login(auth_method).await?;
|
||||
|
||||
// 3. Build a request to get a secret.
|
||||
// Required parameters (name, project_id, environment) are passed to `builder()`.
|
||||
let request = GetSecretRequest::builder("API_KEY", "<your-project-id>", "dev")
|
||||
.path("/") // Optional parameters are set with builder methods.
|
||||
.build();
|
||||
|
||||
// 4. Make the API call.
|
||||
let secret = client.secrets().get(request).await?;
|
||||
|
||||
println!("Fetched secret key: {}", secret.secret_key);
|
||||
// For security, avoid printing the secret value in production code!
|
||||
// println!("Secret value: {}", secret.secret_value);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Client Configuration
|
||||
|
||||
The `Client::builder()` provides several configuration options:
|
||||
|
||||
```rust
|
||||
let mut client = Client::builder()
|
||||
.base_url("https://app.infisical.com") // Optional: set custom Infisical instance URL
|
||||
.build()
|
||||
.await?;
|
||||
```
|
||||
|
||||
**Parameters**
|
||||
- `.base_url(url)`: Optional method to set the Infisical instance URL. Defaults to `https://app.infisical.com` for Infisical Cloud. Use `https://eu.infisical.com` for EU and `http://localhost:8080` for local development.
|
||||
|
||||
### Core Methods
|
||||
|
||||
The SDK methods are organized into the following high-level categories:
|
||||
|
||||
- `Client::builder()`: The main entry point for creating a client.
|
||||
- `client.login()`: Allows client to make authenticated requests to the API.
|
||||
- `client.secrets()`: Provides access to all CRUD operations for secrets.
|
||||
- `client.kms()`: Provides access to all KMS (Key Management Service) operations.
|
||||
|
||||
### Helper Functions
|
||||
|
||||
The SDK provides utility functions for common operations:
|
||||
|
||||
```rust
|
||||
use infisical::{encode_base64, decode_base64};
|
||||
|
||||
// Base64 encode a string
|
||||
let encoded = encode_base64("sensitive data");
|
||||
println!("Encoded: {}", encoded);
|
||||
|
||||
// Base64 decode a string
|
||||
let decoded = decode_base64(&encoded)?;
|
||||
println!("Decoded: {}", decoded);
|
||||
```
|
||||
|
||||
**Available Functions**
|
||||
- `encode_base64(data: &str) -> String`: Encodes a string as base64
|
||||
- `decode_base64(data: &str) -> Result<String, InfisicalError>`: Decodes a base64 string
|
||||
|
||||
### `secrets`
|
||||
|
||||
All secret operations are accessed via `client.secrets()`. Each operation has a dedicated request builder.
|
||||
|
||||
#### Create Secret
|
||||
|
||||
Create a new secret in your project.
|
||||
|
||||
**Example**
|
||||
|
||||
```rust
|
||||
use infisical::secrets::CreateSecretRequest;
|
||||
|
||||
let request = CreateSecretRequest::builder(
|
||||
"API_KEY",
|
||||
"your-secret-value",
|
||||
"<your-project-id>",
|
||||
"dev"
|
||||
)
|
||||
.path("/")
|
||||
.secret_comment("A comment for the new secret")
|
||||
.build();
|
||||
|
||||
let created_secret = client.secrets().create(request).await?;
|
||||
```
|
||||
|
||||
**Parameters**
|
||||
|
||||
- `secret_name`, `secret_value`, `project_id`, `environment`: Required parameters passed to the `builder()` function.
|
||||
- `.path(path)`: Optional method to set the secret's path (defaults to `/`).
|
||||
- `.secret_comment(comment)`: Optional method to add a comment.
|
||||
- `.skip_multiline_encoding(bool)`: Optional method to control multiline encoding (defaults to `false`).
|
||||
- `.r#type(type)`: Optional method to set the secret type (`shared` or `personal`), defaults to `shared`.
|
||||
|
||||
#### Get Secret
|
||||
|
||||
Retrieve a specific secret by name.
|
||||
|
||||
**Example**
|
||||
|
||||
```rust
|
||||
use infisical::secrets::GetSecretRequest;
|
||||
|
||||
let request = GetSecretRequest::builder("API_KEY", "<your-project-id>", "dev")
|
||||
.path("/")
|
||||
.build();
|
||||
|
||||
let secret = client.secrets().get(request).await?;
|
||||
```
|
||||
|
||||
**Parameters**
|
||||
|
||||
- `secret_name`, `project_id`, `environment`: Required parameters passed to the `builder()` function.
|
||||
- `.path(path)`: Optional method to set the secret's path (defaults to `/`).
|
||||
- `.expand_secret_references(bool)`: Optional method to control secret reference expansion (defaults to `true`).
|
||||
- `.r#type(type)`: Optional method to set the secret type (`shared` or `personal`), defaults to `shared`.
|
||||
|
||||
#### List Secrets
|
||||
|
||||
List all secrets in a project and environment.
|
||||
|
||||
**Example**
|
||||
|
||||
```rust
|
||||
use infisical::secrets::ListSecretsRequest;
|
||||
|
||||
let request = ListSecretsRequest::builder("<your-project-id>", "dev")
|
||||
.path("/")
|
||||
.recursive(true)
|
||||
.build();
|
||||
|
||||
let secrets = client.secrets().list(request).await?;
|
||||
```
|
||||
|
||||
**Parameters**
|
||||
|
||||
- `project_id`, `environment`: Required parameters passed to the `builder()` function.
|
||||
- `.path(path)`: Optional method to set the path from which to list secrets (defaults to `/`).
|
||||
- `.expand_secret_references(bool)`: Optional method to control secret reference expansion (defaults to `true`).
|
||||
- `.recursive(bool)`: Optional method to recursively list secrets from sub-folders (defaults to `false`).
|
||||
- `.attach_to_process_env(bool)`: Optional method to attach fetched secrets to the current process's environment variables (defaults to `false`).
|
||||
|
||||
#### Update Secret
|
||||
|
||||
Update an existing secret.
|
||||
|
||||
**Example**
|
||||
|
||||
```rust
|
||||
use infisical::secrets::UpdateSecretRequest;
|
||||
|
||||
let request = UpdateSecretRequest::builder("API_KEY", "<your-project-id>", "dev")
|
||||
.secret_value("new-secret-value") // Set the new value
|
||||
.build();
|
||||
|
||||
let updated_secret = client.secrets().update(request).await?;
|
||||
```
|
||||
|
||||
**Parameters**
|
||||
|
||||
- `secret_name`, `project_id`, `environment`: Required parameters passed to the `builder()` function.
|
||||
- `.new_secret_name(name)`: Optional method to rename the secret.
|
||||
- `.secret_value(value)`: Optional method to set a new value for the secret.
|
||||
- `.path(path)`: Optional method to set the secret's path.
|
||||
- `.secret_comment(comment)`: Optional method to add or change the comment.
|
||||
- `.skip_multiline_encoding(bool)`: Optional method to control multiline encoding.
|
||||
- `.r#type(type)`: Optional method to set the secret type (`shared` or `personal`).
|
||||
|
||||
#### Delete Secret
|
||||
|
||||
Delete a secret from your project.
|
||||
|
||||
**Example**
|
||||
|
||||
```rust
|
||||
use infisical::secrets::DeleteSecretRequest;
|
||||
|
||||
let request = DeleteSecretRequest::builder("API_KEY", "<your-project-id>", "dev")
|
||||
.path("/")
|
||||
.build();
|
||||
|
||||
let deleted_secret = client.secrets().delete(request).await?;
|
||||
```
|
||||
|
||||
**Parameters**
|
||||
|
||||
- `secret_name`, `project_id`, `environment`: Required parameters passed to the `builder()` function.
|
||||
- `.path(path)`: Optional method to set the secret's path (defaults to `/`).
|
||||
- `.r#type(type)`: Optional method to set the secret type (`shared` or `personal`), defaults to `shared`.
|
||||
|
||||
### `kms`
|
||||
|
||||
All KMS (Key Management Service) operations are accessed via `client.kms()`. Each operation has a dedicated request builder.
|
||||
|
||||
#### List KMS Keys
|
||||
|
||||
List all KMS keys in a project.
|
||||
|
||||
**Example**
|
||||
|
||||
```rust
|
||||
use infisical::kms::ListKmsKeysRequest;
|
||||
|
||||
let request = ListKmsKeysRequest::builder("<your-project-id>").build();
|
||||
|
||||
let keys = client.kms().list(request).await?;
|
||||
```
|
||||
|
||||
**Parameters**
|
||||
|
||||
- `project_id`: Required parameter passed to the `builder()` function.
|
||||
|
||||
#### Get KMS Key
|
||||
|
||||
Retrieve a specific KMS key by ID.
|
||||
|
||||
**Example**
|
||||
|
||||
```rust
|
||||
use infisical::kms::GetKmsKeyRequest;
|
||||
|
||||
let request = GetKmsKeyRequest::builder("<key-id>").build();
|
||||
|
||||
let key = client.kms().get(request).await?;
|
||||
```
|
||||
|
||||
**Parameters**
|
||||
|
||||
- `key_id`: Required parameter passed to the `builder()` function.
|
||||
|
||||
#### Get KMS Key by Name
|
||||
|
||||
Retrieve a specific KMS key by name.
|
||||
|
||||
**Example**
|
||||
|
||||
```rust
|
||||
use infisical::kms::GetKmsKeyByNameRequest;
|
||||
|
||||
let request = GetKmsKeyByNameRequest::builder("<key-name>").build();
|
||||
|
||||
let key = client.kms().get_by_name(request).await?;
|
||||
```
|
||||
|
||||
**Parameters**
|
||||
|
||||
- `key_name`: Required parameter passed to the `builder()` function.
|
||||
|
||||
#### Create KMS Key
|
||||
|
||||
Create a new KMS key in your project.
|
||||
|
||||
**Example**
|
||||
|
||||
```rust
|
||||
use infisical::kms::{CreateKmsKeyRequest, EncryptionAlgorithm, KeyUsage};
|
||||
|
||||
let request = CreateKmsKeyRequest::builder("<your-project-id>", "my-key")
|
||||
.description("A key for encryption operations")
|
||||
.key_usage(KeyUsage::EncryptDecrypt)
|
||||
.encryption_algorithm(EncryptionAlgorithm::Aes256Gcm)
|
||||
.build();
|
||||
|
||||
let created_key = client.kms().create(request).await?;
|
||||
```
|
||||
|
||||
**Parameters**
|
||||
|
||||
- `project_id`, `name`: Required parameters passed to the `builder()` function.
|
||||
- `.description(description)`: Optional method to set the key description.
|
||||
- `.key_usage(usage)`: Optional method to set the key usage using the `KeyUsage` enum (defaults to `KeyUsage::EncryptDecrypt`).
|
||||
- `.encryption_algorithm(algorithm)`: Optional method to set the encryption algorithm using the `EncryptionAlgorithm` enum (defaults to `EncryptionAlgorithm::Aes256Gcm`).
|
||||
|
||||
#### Update KMS Key
|
||||
|
||||
Update an existing KMS key.
|
||||
|
||||
**Example**
|
||||
|
||||
```rust
|
||||
use infisical::kms::UpdateKmsKeyRequest;
|
||||
|
||||
let request = UpdateKmsKeyRequest::builder("<key-id>")
|
||||
.name("updated-key-name")
|
||||
.description("Updated description")
|
||||
.is_disabled(false)
|
||||
.build();
|
||||
|
||||
let updated_key = client.kms().update(request).await?;
|
||||
```
|
||||
|
||||
**Parameters**
|
||||
|
||||
- `key_id`: Required parameter passed to the `builder()` function.
|
||||
- `.name(name)`: Optional method to rename the key.
|
||||
- `.description(description)`: Optional method to update the key description.
|
||||
- `.is_disabled(disabled)`: Optional method to enable or disable the key.
|
||||
|
||||
#### Delete KMS Key
|
||||
|
||||
Delete a KMS key from your project.
|
||||
|
||||
**Example**
|
||||
|
||||
```rust
|
||||
use infisical::kms::DeleteKmsKeyRequest;
|
||||
|
||||
let request = DeleteKmsKeyRequest::builder("<key-id>").build();
|
||||
|
||||
let deleted_key = client.kms().delete(request).await?;
|
||||
```
|
||||
|
||||
**Parameters**
|
||||
|
||||
- `key_id`: Required parameter passed to the `builder()` function.
|
||||
|
||||
#### Encrypt Data
|
||||
|
||||
Encrypt data using a KMS key.
|
||||
|
||||
**Example**
|
||||
|
||||
```rust
|
||||
use infisical::kms::EncryptRequest;
|
||||
|
||||
let request = EncryptRequest::builder("<key-id>", "sensitive data").build();
|
||||
|
||||
let ciphertext = client.kms().encrypt(request).await?;
|
||||
```
|
||||
|
||||
**Parameters**
|
||||
|
||||
- `key_id`, `plaintext`: Required parameters passed to the `builder()` function.
|
||||
|
||||
#### Decrypt Data
|
||||
|
||||
Decrypt data using a KMS key.
|
||||
|
||||
**Example**
|
||||
|
||||
```rust
|
||||
use infisical::kms::DecryptRequest;
|
||||
|
||||
let request = DecryptRequest::builder("<key-id>", "encrypted-data").build();
|
||||
|
||||
let plaintext = client.kms().decrypt(request).await?;
|
||||
```
|
||||
|
||||
**Parameters**
|
||||
|
||||
- `key_id`, `ciphertext`: Required parameters passed to the `builder()` function.
|
||||
|
||||
#### Sign Data
|
||||
|
||||
Sign data using a KMS key.
|
||||
|
||||
**Example**
|
||||
|
||||
```rust
|
||||
use infisical::kms::{SigningAlgorithm, SignRequest};
|
||||
|
||||
let request = SignRequest::builder("<key-id>", "data to sign")
|
||||
.signing_algorithm(SigningAlgorithm::RsassaPkcs1V15Sha256)
|
||||
.is_digest(false)
|
||||
.build();
|
||||
|
||||
let signature = client.kms().sign(request).await?;
|
||||
```
|
||||
|
||||
**Parameters**
|
||||
|
||||
- `key_id`, `data`: Required parameters passed to the `builder()` function.
|
||||
- `.signing_algorithm(algorithm)`: Optional method to set the signing algorithm using the `SigningAlgorithm` enum (defaults to `SigningAlgorithm::RsassaPkcs1V15Sha256`).
|
||||
- `.is_digest(is_digest)`: Optional method to indicate if the data is a digest (defaults to `false`).
|
||||
|
||||
#### Verify Signature
|
||||
|
||||
Verify a signature using a KMS key.
|
||||
|
||||
**Example**
|
||||
|
||||
```rust
|
||||
use infisical::kms::{SigningAlgorithm, VerifyRequest};
|
||||
|
||||
let request = VerifyRequest::builder("<key-id>", "data to sign", "signature")
|
||||
.signing_algorithm(SigningAlgorithm::RsassaPkcs1V15Sha256)
|
||||
.is_digest(false)
|
||||
.build();
|
||||
|
||||
let verification = client.kms().verify(request).await?;
|
||||
```
|
||||
|
||||
**Parameters**
|
||||
|
||||
- `key_id`, `data`, `signature`: Required parameters passed to the `builder()` function.
|
||||
- `.signing_algorithm(algorithm)`: Optional method to set the signing algorithm using the `SigningAlgorithm` enum (defaults to `SigningAlgorithm::RsassaPkcs1V15Sha256`).
|
||||
- `.is_digest(is_digest)`: Optional method to indicate if the data is a digest (defaults to `false`).
|
||||
|
||||
#### Get Public Key
|
||||
|
||||
Get the public key for a KMS key.
|
||||
|
||||
**Example**
|
||||
|
||||
```rust
|
||||
let public_key = client.kms().get_public_key("<key-id>").await?;
|
||||
```
|
||||
|
||||
**Parameters**
|
||||
|
||||
- `key_id`: The ID of the key to get the public key for.
|
||||
|
||||
#### Get Signing Algorithms
|
||||
|
||||
Get the available signing algorithms for a KMS key.
|
||||
|
||||
**Example**
|
||||
|
||||
```rust
|
||||
let algorithms = client.kms().get_signing_algorithms("<key-id>").await?;
|
||||
```
|
||||
|
||||
**Parameters**
|
||||
|
||||
- `key_id`: The ID of the key to get signing algorithms for.
|
||||
|
||||
## Development and Testing
|
||||
|
||||
### Environment Setup
|
||||
|
||||
For development and testing, you'll need to set up environment variables. Create a `.env` file in your project root:
|
||||
|
||||
```env
|
||||
INFISICAL_CLIENT_ID=your_client_id_here
|
||||
INFISICAL_CLIENT_SECRET=your_client_secret_here
|
||||
INFISICAL_BASE_URL=http://localhost:8080 # Optional: for local development
|
||||
|
||||
# Project IDs for different resources
|
||||
INFISICAL_SECRETS_PROJECT_ID=your_project_id_here
|
||||
INFISICAL_KMS_PROJECT_ID=your_project_id_here
|
||||
```
|
||||
|
||||
### Getting Credentials
|
||||
|
||||
To obtain the required credentials:
|
||||
|
||||
1. **Client ID and Secret**: Create a Universal Auth machine identity in your Infisical project settings
|
||||
2. **Project ID**: Found in your project settings or URL when viewing a project in the Infisical dashboard
|
||||
|
||||
### Running Tests
|
||||
|
||||
Tests that require authentication are marked with `#[ignore]` and need valid credentials:
|
||||
|
||||
```bash
|
||||
# Run ignored tests (requires .env file with valid credentials)
|
||||
cargo test -- --ignored --nocapture
|
||||
|
||||
# Run a specific test
|
||||
cargo test test_kms_resource -- --ignored --nocapture
|
||||
```
|
||||
|
||||
**Note**: Integration tests require a running Infisical instance and valid authentication credentials.
|
@@ -12,22 +12,22 @@ From local development to production, Infisical SDKs provide the easiest way for
|
||||
- Fetch secrets on demand
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Node.js" href="https://github.com/Infisical/node-sdk-v2?tab=readme-ov-file#infisical-nodejs-sdk" icon="/images/sdks/languages/node.svg">
|
||||
<Card title="Node.js" href="/sdks/languages/node" icon="/images/sdks/languages/node.svg">
|
||||
Manage secrets for your Node application on demand
|
||||
</Card>
|
||||
<Card href="https://github.com/Infisical/python-sdk-official?tab=readme-ov-file#infisical-python-sdk" title="Python" icon="/images/sdks/languages/python.svg">
|
||||
<Card href="/sdks/languages/python" title="Python" icon="/images/sdks/languages/python.svg">
|
||||
Manage secrets for your Python application on demand
|
||||
</Card>
|
||||
<Card href="https://github.com/Infisical/java-sdk?tab=readme-ov-file#infisical-java-sdk" title="Java" icon="/images/sdks/languages/java.svg">
|
||||
<Card href="/sdks/languages/java" title="Java" icon="/images/sdks/languages/java.svg">
|
||||
Manage secrets for your Java application on demand
|
||||
</Card>
|
||||
<Card href="https://github.com/Infisical/infisical-dotnet-sdk?tab=readme-ov-file#infisical-net-sdk" title=".NET" icon="/images/sdks/languages/dotnet.svg">
|
||||
<Card href="/sdks/languages/csharp" title=".NET" icon="/images/sdks/languages/dotnet.svg">
|
||||
Manage secrets for your .NET application on demand
|
||||
</Card>
|
||||
<Card href="https://github.com/Infisical/infisical-cpp-sdk/?tab=readme-ov-file#infisical-c-sdk" title="C++" icon="/images/sdks/languages/cpp.svg">
|
||||
<Card href="/sdks/languages/cpp" title="C++" icon="/images/sdks/languages/cpp.svg">
|
||||
Manage secrets for your C++ application on demand
|
||||
</Card>
|
||||
<Card href="https://github.com/Infisical/rust-sdk?tab=readme-ov-file#infisical--the-official-infisical-rust-sdk" title="Rust" icon="/images/sdks/languages/rust.svg">
|
||||
<Card href="/sdks/languages/rust" title="Rust" icon="/images/sdks/languages/rust.svg">
|
||||
Manage secrets for your Rust application on demand
|
||||
</Card>
|
||||
<Card href="/sdks/languages/go" title="Go" icon="/images/sdks/languages/go.svg">
|
||||
|
440
docs/self-hosting/guides/monitoring-telemetry.mdx
Normal file
440
docs/self-hosting/guides/monitoring-telemetry.mdx
Normal file
@@ -0,0 +1,440 @@
|
||||
---
|
||||
title: "Monitoring and Telemetry Setup"
|
||||
description: "Learn how to set up monitoring and telemetry for your self-hosted Infisical instance using Grafana, Prometheus, and OpenTelemetry."
|
||||
---
|
||||
|
||||
Infisical provides comprehensive monitoring and telemetry capabilities to help you monitor the health, performance, and usage of your self-hosted instance. This guide covers setting up monitoring using Grafana with two different telemetry collection approaches.
|
||||
|
||||
## Overview
|
||||
|
||||
Infisical exports metrics in **OpenTelemetry (OTEL) format**, which provides maximum flexibility for your monitoring infrastructure. While this guide focuses on Grafana, the OTEL format means you can easily integrate with:
|
||||
|
||||
- **Cloud-native monitoring**: AWS CloudWatch, Google Cloud Monitoring, Azure Monitor
|
||||
- **Observability platforms**: Datadog, New Relic, Splunk, Dynatrace
|
||||
- **Custom backends**: Any system that supports OTEL ingestion
|
||||
- **Traditional monitoring**: Prometheus, Grafana (as covered in this guide)
|
||||
|
||||
Infisical supports two telemetry collection methods:
|
||||
|
||||
1. **Pull-based (Prometheus)**: Exposes metrics on a dedicated endpoint for Prometheus to scrape
|
||||
2. **Push-based (OTLP)**: Sends metrics to an OpenTelemetry Collector via OTLP protocol
|
||||
|
||||
Both approaches provide the same metrics data in OTEL format, so you can choose the one that best fits your infrastructure and monitoring strategy.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Self-hosted Infisical instance running
|
||||
- Access to deploy monitoring services (Prometheus, Grafana, etc.)
|
||||
- Basic understanding of Prometheus and Grafana
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Configure the following environment variables in your Infisical backend:
|
||||
|
||||
```bash
|
||||
# Enable telemetry collection
|
||||
OTEL_TELEMETRY_COLLECTION_ENABLED=true
|
||||
|
||||
# Choose export type: "prometheus" or "otlp"
|
||||
OTEL_EXPORT_TYPE=prometheus
|
||||
|
||||
# For OTLP push mode, also configure:
|
||||
# OTEL_EXPORT_OTLP_ENDPOINT=http://otel-collector:4318/v1/metrics
|
||||
# OTEL_COLLECTOR_BASIC_AUTH_USERNAME=your_collector_username
|
||||
# OTEL_COLLECTOR_BASIC_AUTH_PASSWORD=your_collector_password
|
||||
# OTEL_OTLP_PUSH_INTERVAL=30000
|
||||
```
|
||||
|
||||
**Note**: The `OTEL_COLLECTOR_BASIC_AUTH_USERNAME` and `OTEL_COLLECTOR_BASIC_AUTH_PASSWORD` values must match the credentials configured in your OpenTelemetry Collector's `basicauth/server` extension. These are not hardcoded values - you configure them in your collector configuration file.
|
||||
|
||||
## Option 1: Pull-based Monitoring (Prometheus)
|
||||
|
||||
This approach exposes metrics on port 9464 at the `/metrics` endpoint, allowing Prometheus to scrape the data. The metrics are exposed in Prometheus format but originate from OpenTelemetry instrumentation.
|
||||
|
||||
### Configuration
|
||||
|
||||
1. **Enable Prometheus export in Infisical**:
|
||||
|
||||
```bash
|
||||
OTEL_TELEMETRY_COLLECTION_ENABLED=true
|
||||
OTEL_EXPORT_TYPE=prometheus
|
||||
```
|
||||
|
||||
2. **Expose the metrics port** in your Infisical backend:
|
||||
|
||||
- **Docker**: Expose port 9464
|
||||
- **Kubernetes**: Create a service exposing port 9464
|
||||
- **Other**: Ensure port 9464 is accessible to your monitoring stack
|
||||
|
||||
3. **Create Prometheus configuration** (`prometheus.yml`):
|
||||
|
||||
```yaml
|
||||
global:
|
||||
scrape_interval: 30s
|
||||
evaluation_interval: 30s
|
||||
|
||||
scrape_configs:
|
||||
- job_name: "infisical"
|
||||
scrape_interval: 30s
|
||||
static_configs:
|
||||
- targets: ["infisical-backend:9464"] # Adjust hostname/port based on your deployment
|
||||
metrics_path: "/metrics"
|
||||
```
|
||||
|
||||
**Note**: Replace `infisical-backend:9464` with the actual hostname and port where your Infisical backend is running. This could be:
|
||||
|
||||
- **Docker Compose**: `infisical-backend:9464` (service name)
|
||||
- **Kubernetes**: `infisical-backend.default.svc.cluster.local:9464` (service name)
|
||||
- **Bare Metal**: `192.168.1.100:9464` (actual IP address)
|
||||
- **Cloud**: `your-infisical.example.com:9464` (domain name)
|
||||
|
||||
### Deployment Options
|
||||
|
||||
#### Docker Compose
|
||||
|
||||
```yaml
|
||||
services:
|
||||
prometheus:
|
||||
image: prom/prometheus:latest
|
||||
ports:
|
||||
- "9090:9090"
|
||||
volumes:
|
||||
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
||||
command:
|
||||
- "--config.file=/etc/prometheus/prometheus.yml"
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana:latest
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- GF_SECURITY_ADMIN_USER=admin
|
||||
- GF_SECURITY_ADMIN_PASSWORD=admin
|
||||
```
|
||||
|
||||
#### Kubernetes
|
||||
|
||||
```yaml
|
||||
# prometheus-deployment.yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: prometheus
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: prometheus
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: prometheus
|
||||
spec:
|
||||
containers:
|
||||
- name: prometheus
|
||||
image: prom/prometheus:latest
|
||||
ports:
|
||||
- containerPort: 9090
|
||||
volumeMounts:
|
||||
- name: config
|
||||
mountPath: /etc/prometheus
|
||||
volumes:
|
||||
- name: config
|
||||
configMap:
|
||||
name: prometheus-config
|
||||
|
||||
---
|
||||
# prometheus-service.yaml
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: prometheus
|
||||
spec:
|
||||
selector:
|
||||
app: prometheus
|
||||
ports:
|
||||
- port: 9090
|
||||
targetPort: 9090
|
||||
type: ClusterIP
|
||||
```
|
||||
|
||||
#### Helm
|
||||
|
||||
```bash
|
||||
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
|
||||
helm install prometheus prometheus-community/prometheus \
|
||||
--set server.config.global.scrape_interval=30s \
|
||||
--set server.config.scrape_configs[0].job_name=infisical \
|
||||
--set server.config.scrape_configs[0].static_configs[0].targets[0]=infisical-backend:9464
|
||||
```
|
||||
|
||||
## Option 2: Push-based Monitoring (OTLP)
|
||||
|
||||
This approach sends metrics directly to an OpenTelemetry Collector via the OTLP protocol. This gives you the most flexibility as you can configure the collector to export to multiple backends simultaneously.
|
||||
|
||||
### Configuration
|
||||
|
||||
1. **Enable OTLP export in Infisical**:
|
||||
|
||||
```bash
|
||||
OTEL_TELEMETRY_COLLECTION_ENABLED=true
|
||||
OTEL_EXPORT_TYPE=otlp
|
||||
OTEL_EXPORT_OTLP_ENDPOINT=http://otel-collector:4318/v1/metrics
|
||||
OTEL_COLLECTOR_BASIC_AUTH_USERNAME=infisical
|
||||
OTEL_COLLECTOR_BASIC_AUTH_PASSWORD=infisical
|
||||
OTEL_OTLP_PUSH_INTERVAL=30000
|
||||
```
|
||||
|
||||
2. **Create OpenTelemetry Collector configuration** (`otel-collector-config.yaml`):
|
||||
|
||||
```yaml
|
||||
extensions:
|
||||
health_check:
|
||||
pprof:
|
||||
zpages:
|
||||
basicauth/server:
|
||||
htpasswd:
|
||||
inline: |
|
||||
your_username:your_password
|
||||
|
||||
receivers:
|
||||
otlp:
|
||||
protocols:
|
||||
http:
|
||||
endpoint: 0.0.0.0:4318
|
||||
auth:
|
||||
authenticator: basicauth/server
|
||||
|
||||
prometheus:
|
||||
config:
|
||||
scrape_configs:
|
||||
- job_name: otel-collector
|
||||
scrape_interval: 30s
|
||||
static_configs:
|
||||
- targets: [infisical-backend:9464]
|
||||
metric_relabel_configs:
|
||||
- action: labeldrop
|
||||
regex: "service_instance_id|service_name"
|
||||
|
||||
processors:
|
||||
batch:
|
||||
|
||||
exporters:
|
||||
prometheus:
|
||||
endpoint: "0.0.0.0:8889"
|
||||
auth:
|
||||
authenticator: basicauth/server
|
||||
resource_to_telemetry_conversion:
|
||||
enabled: true
|
||||
|
||||
service:
|
||||
extensions: [basicauth/server, health_check, pprof, zpages]
|
||||
pipelines:
|
||||
metrics:
|
||||
receivers: [otlp]
|
||||
processors: [batch]
|
||||
exporters: [prometheus]
|
||||
```
|
||||
|
||||
**Important**: Replace `your_username:your_password` with your chosen credentials. These must match the values you set in Infisical's `OTEL_COLLECTOR_BASIC_AUTH_USERNAME` and `OTEL_COLLECTOR_BASIC_AUTH_PASSWORD` environment variables.
|
||||
|
||||
3. **Create Prometheus configuration** for the collector:
|
||||
|
||||
```yaml
|
||||
global:
|
||||
scrape_interval: 30s
|
||||
evaluation_interval: 30s
|
||||
|
||||
scrape_configs:
|
||||
- job_name: "otel-collector"
|
||||
scrape_interval: 30s
|
||||
static_configs:
|
||||
- targets: ["otel-collector:8889"] # Adjust hostname/port based on your deployment
|
||||
metrics_path: "/metrics"
|
||||
```
|
||||
|
||||
**Note**: Replace `otel-collector:8889` with the actual hostname and port where your OpenTelemetry Collector is running. This could be:
|
||||
|
||||
- **Docker Compose**: `otel-collector:8889` (service name)
|
||||
- **Kubernetes**: `otel-collector.default.svc.cluster.local:8889` (service name)
|
||||
- **Bare Metal**: `192.168.1.100:8889` (actual IP address)
|
||||
- **Cloud**: `your-collector.example.com:8889` (domain name)
|
||||
|
||||
### Deployment Options
|
||||
|
||||
#### Docker Compose
|
||||
|
||||
```yaml
|
||||
services:
|
||||
otel-collector:
|
||||
image: otel/opentelemetry-collector-contrib:latest
|
||||
ports:
|
||||
- 4318:4318 # OTLP http receiver
|
||||
- 8889:8889 # Prometheus exporter metrics
|
||||
volumes:
|
||||
- ./otel-collector-config.yaml:/etc/otelcol-contrib/config.yaml:ro
|
||||
command:
|
||||
- "--config=/etc/otelcol-contrib/config.yaml"
|
||||
```
|
||||
|
||||
#### Kubernetes
|
||||
|
||||
```yaml
|
||||
# otel-collector-deployment.yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: otel-collector
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: otel-collector
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: otel-collector
|
||||
spec:
|
||||
containers:
|
||||
- name: otel-collector
|
||||
image: otel/opentelemetry-collector-contrib:latest
|
||||
ports:
|
||||
- containerPort: 4318
|
||||
- containerPort: 8889
|
||||
volumeMounts:
|
||||
- name: config
|
||||
mountPath: /etc/otelcol-contrib
|
||||
volumes:
|
||||
- name: config
|
||||
configMap:
|
||||
name: otel-collector-config
|
||||
```
|
||||
|
||||
#### Helm
|
||||
|
||||
```bash
|
||||
helm repo add open-telemetry https://open-telemetry.github.io/opentelemetry-helm-charts
|
||||
helm install otel-collector open-telemetry/opentelemetry-collector \
|
||||
--set config.receivers.otlp.protocols.http.endpoint=0.0.0.0:4318 \
|
||||
--set config.exporters.prometheus.endpoint=0.0.0.0:8889
|
||||
```
|
||||
|
||||
## Alternative Backends
|
||||
|
||||
Since Infisical exports in OpenTelemetry format, you can easily configure the collector to send metrics to other backends instead of (or in addition to) Prometheus:
|
||||
|
||||
### Cloud-Native Examples
|
||||
|
||||
```yaml
|
||||
# Add to your otel-collector-config.yaml exporters section
|
||||
exporters:
|
||||
# AWS CloudWatch
|
||||
awsemf:
|
||||
region: us-west-2
|
||||
log_group_name: /aws/emf/infisical
|
||||
log_stream_name: metrics
|
||||
|
||||
# Google Cloud Monitoring
|
||||
googlecloud:
|
||||
project_id: your-project-id
|
||||
|
||||
# Azure Monitor
|
||||
azuremonitor:
|
||||
connection_string: "your-connection-string"
|
||||
|
||||
# Datadog
|
||||
datadog:
|
||||
api:
|
||||
key: "your-api-key"
|
||||
site: "datadoghq.com"
|
||||
|
||||
# New Relic
|
||||
newrelic:
|
||||
apikey: "your-api-key"
|
||||
host_override: "otlp.nr-data.net"
|
||||
```
|
||||
|
||||
### Multi-Backend Configuration
|
||||
|
||||
```yaml
|
||||
service:
|
||||
pipelines:
|
||||
metrics:
|
||||
receivers: [otlp]
|
||||
processors: [batch]
|
||||
exporters: [prometheus, awsemf, datadog] # Send to multiple backends
|
||||
```
|
||||
|
||||
## Setting Up Grafana
|
||||
|
||||
1. **Access Grafana**: Navigate to your Grafana instance
|
||||
2. **Login**: Use your configured credentials
|
||||
3. **Add Prometheus Data Source**:
|
||||
- Go to Configuration → Data Sources
|
||||
- Click "Add data source"
|
||||
- Select "Prometheus"
|
||||
- Set URL to your Prometheus endpoint
|
||||
- Click "Save & Test"
|
||||
|
||||
## Available Metrics
|
||||
|
||||
Infisical exposes the following key metrics in OpenTelemetry format:
|
||||
|
||||
### API Performance Metrics
|
||||
|
||||
- `API_latency` - API request latency histogram in milliseconds
|
||||
|
||||
- **Labels**: `route`, `method`, `statusCode`
|
||||
- **Example**: Monitor response times for specific endpoints
|
||||
|
||||
- `API_errors` - API error count histogram
|
||||
- **Labels**: `route`, `method`, `type`, `name`
|
||||
- **Example**: Track error rates by endpoint and error type
|
||||
|
||||
### Integration & Secret Sync Metrics
|
||||
|
||||
- `integration_secret_sync_errors` - Integration secret sync error count
|
||||
|
||||
- **Labels**: `version`, `integration`, `integrationId`, `type`, `status`, `name`, `projectId`
|
||||
- **Example**: Monitor integration sync failures across different services
|
||||
|
||||
- `secret_sync_sync_secrets_errors` - Secret sync operation error count
|
||||
|
||||
- **Labels**: `version`, `destination`, `syncId`, `projectId`, `type`, `status`, `name`
|
||||
- **Example**: Track secret sync failures to external systems
|
||||
|
||||
- `secret_sync_import_secrets_errors` - Secret import operation error count
|
||||
|
||||
- **Labels**: `version`, `destination`, `syncId`, `projectId`, `type`, `status`, `name`
|
||||
- **Example**: Monitor secret import failures
|
||||
|
||||
- `secret_sync_remove_secrets_errors` - Secret removal operation error count
|
||||
- **Labels**: `version`, `destination`, `syncId`, `projectId`, `type`, `status`, `name`
|
||||
- **Example**: Track secret removal operation failures
|
||||
|
||||
### System Metrics
|
||||
|
||||
These metrics are automatically collected by OpenTelemetry's HTTP instrumentation:
|
||||
|
||||
- `http_server_duration` - HTTP server request duration metrics (histogram buckets, count, sum)
|
||||
- `http_client_duration` - HTTP client request duration metrics (histogram buckets, count, sum)
|
||||
|
||||
### Custom Business Metrics
|
||||
|
||||
- `infisical_secret_operations_total` - Total secret operations
|
||||
- `infisical_secrets_processed_total` - Total secrets processed
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Metrics not appearing**:
|
||||
|
||||
- Check if `OTEL_TELEMETRY_COLLECTION_ENABLED=true`
|
||||
- Verify the correct `OTEL_EXPORT_TYPE` is set
|
||||
- Check network connectivity between services
|
||||
|
||||
2. **Authentication errors**:
|
||||
|
||||
- Verify basic auth credentials in OTLP configuration
|
||||
- Check if credentials match between Infisical and collector
|
@@ -53,8 +53,8 @@
|
||||
"project-id": "Project ID",
|
||||
"save-changes": "Save Changes",
|
||||
"saved": "Saved",
|
||||
"drop-zone": "Drag and drop a .env, .json, or .yml file here.",
|
||||
"drop-zone-keys": "Drag and drop a .env, .json, or .yml file here to add more secrets.",
|
||||
"drop-zone": "Drag and drop a .env, .json, .csv, or .yml file here.",
|
||||
"drop-zone-keys": "Drag and drop a .env, .json, .csv, or .yml file here to add more secrets.",
|
||||
"role": "Role",
|
||||
"role_admin": "admin",
|
||||
"display-name": "Display Name",
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faArrowUpRightFromSquare, faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { FormLabel, Tooltip } from "../v2";
|
||||
@@ -10,15 +10,18 @@ export const TtlFormLabel = ({ label }: { label: string }) => (
|
||||
label={label}
|
||||
icon={
|
||||
<Tooltip
|
||||
className="max-w-lg"
|
||||
content={
|
||||
<span>
|
||||
Examples: 30m, 1h, 3d, etc.{" "}
|
||||
<a
|
||||
href="https://github.com/vercel/ms?tab=readme-ov-file#examples"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary-700"
|
||||
className="text-primary-500 hover:text-mineshaft-100"
|
||||
>
|
||||
More
|
||||
See More Examples{" "}
|
||||
<FontAwesomeIcon size="xs" className="mt-0.5" icon={faArrowUpRightFromSquare} />
|
||||
</a>
|
||||
</span>
|
||||
}
|
||||
@@ -26,7 +29,7 @@ export const TtlFormLabel = ({ label }: { label: string }) => (
|
||||
<FontAwesomeIcon
|
||||
icon={faQuestionCircle}
|
||||
size="sm"
|
||||
className="relative bottom-px right-1"
|
||||
className="relative right-1 mt-0.5 text-mineshaft-300"
|
||||
/>
|
||||
</Tooltip>
|
||||
}
|
||||
|
29
frontend/src/components/roles/RoleOption.tsx
Normal file
29
frontend/src/components/roles/RoleOption.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { components, OptionProps } from "react-select";
|
||||
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
export const RoleOption = ({
|
||||
isSelected,
|
||||
children,
|
||||
...props
|
||||
}: OptionProps<{ name: string; slug: string; description?: string | undefined }>) => {
|
||||
return (
|
||||
<components.Option isSelected={isSelected} {...props}>
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<div>
|
||||
<p className="truncate">{children}</p>
|
||||
{props.data.description ? (
|
||||
<p className="truncate text-xs leading-4 text-mineshaft-400">
|
||||
{props.data.description}
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-xs leading-4 text-mineshaft-400/50">No Description</p>
|
||||
)}
|
||||
</div>
|
||||
{isSelected && (
|
||||
<FontAwesomeIcon className="ml-2 text-primary" icon={faCheckCircle} size="sm" />
|
||||
)}
|
||||
</div>
|
||||
</components.Option>
|
||||
);
|
||||
};
|
1
frontend/src/components/roles/index.tsx
Normal file
1
frontend/src/components/roles/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./RoleOption";
|
@@ -5,7 +5,9 @@ import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/Se
|
||||
import { FilterableSelect, FormControl, Select, SelectItem } from "@app/components/v2";
|
||||
import { RENDER_SYNC_SCOPES } from "@app/helpers/secretSyncs";
|
||||
import {
|
||||
TRenderEnvironmentGroup,
|
||||
TRenderService,
|
||||
useRenderConnectionListEnvironmentGroups,
|
||||
useRenderConnectionListServices
|
||||
} from "@app/hooks/api/appConnections/render";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
@@ -19,6 +21,7 @@ export const RenderSyncFields = () => {
|
||||
>();
|
||||
|
||||
const connectionId = useWatch({ name: "connection.id", control });
|
||||
const selectedScope = useWatch({ name: "destinationConfig.scope", control });
|
||||
|
||||
const { data: services = [], isPending: isServicesPending } = useRenderConnectionListServices(
|
||||
connectionId,
|
||||
@@ -27,11 +30,17 @@ export const RenderSyncFields = () => {
|
||||
}
|
||||
);
|
||||
|
||||
const { data: groups = [], isPending: isGroupsPending } =
|
||||
useRenderConnectionListEnvironmentGroups(connectionId, {
|
||||
enabled: Boolean(connectionId) && selectedScope === RenderSyncScope.EnvironmentGroup
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<SecretSyncConnectionField
|
||||
onChange={() => {
|
||||
setValue("destinationConfig.serviceId", "");
|
||||
setValue("destinationConfig.environmentGroupId", "");
|
||||
setValue("destinationConfig.type", RenderSyncType.Env);
|
||||
setValue("destinationConfig.scope", RenderSyncScope.Service);
|
||||
}}
|
||||
@@ -83,30 +92,67 @@ export const RenderSyncFields = () => {
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="destinationConfig.serviceId"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl errorText={error?.message} isError={Boolean(error?.message)} label="Service">
|
||||
<FilterableSelect
|
||||
isLoading={isServicesPending && Boolean(connectionId)}
|
||||
isDisabled={!connectionId}
|
||||
value={services ? (services.find((service) => service.id === value) ?? []) : []}
|
||||
onChange={(option) => {
|
||||
onChange((option as SingleValue<TRenderService>)?.id ?? null);
|
||||
setValue(
|
||||
"destinationConfig.serviceName",
|
||||
(option as SingleValue<TRenderService>)?.name ?? ""
|
||||
);
|
||||
}}
|
||||
options={services}
|
||||
placeholder="Select a service..."
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionValue={(option) => option.id.toString()}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{selectedScope === RenderSyncScope.Service && (
|
||||
<Controller
|
||||
name="destinationConfig.serviceId"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Service"
|
||||
>
|
||||
<FilterableSelect
|
||||
isLoading={isServicesPending && Boolean(connectionId)}
|
||||
isDisabled={!connectionId}
|
||||
value={services ? (services.find((service) => service.id === value) ?? []) : []}
|
||||
onChange={(option) => {
|
||||
onChange((option as SingleValue<TRenderService>)?.id ?? null);
|
||||
setValue(
|
||||
"destinationConfig.serviceName",
|
||||
(option as SingleValue<TRenderService>)?.name ?? ""
|
||||
);
|
||||
}}
|
||||
options={services}
|
||||
placeholder="Select a service..."
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionValue={(option) => option.id.toString()}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedScope === RenderSyncScope.EnvironmentGroup && (
|
||||
<Controller
|
||||
name="destinationConfig.environmentGroupId"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Environment Group"
|
||||
>
|
||||
<FilterableSelect
|
||||
isLoading={isGroupsPending && Boolean(connectionId)}
|
||||
isDisabled={!connectionId}
|
||||
value={groups ? (groups.find((g) => g.id === value) ?? []) : []}
|
||||
onChange={(option) => {
|
||||
onChange((option as SingleValue<TRenderEnvironmentGroup>)?.id ?? null);
|
||||
setValue(
|
||||
"destinationConfig.environmentGroupName",
|
||||
(option as SingleValue<TRenderEnvironmentGroup>)?.name ?? ""
|
||||
);
|
||||
}}
|
||||
options={groups}
|
||||
placeholder="Select an environment group..."
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionValue={(option) => option.id.toString()}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@@ -4,6 +4,7 @@ import { GenericFieldLabel } from "@app/components/secret-syncs";
|
||||
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
|
||||
import { Badge } from "@app/components/v2";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
import { RenderSyncScope } from "@app/hooks/api/secretSyncs/types/render-sync";
|
||||
|
||||
export const RenderSyncOptionsReviewFields = () => {
|
||||
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.Render }>();
|
||||
@@ -27,13 +28,20 @@ export const RenderSyncOptionsReviewFields = () => {
|
||||
|
||||
export const RenderSyncReviewFields = () => {
|
||||
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.Render }>();
|
||||
const serviceName = watch("destinationConfig.serviceName");
|
||||
const scope = watch("destinationConfig.scope");
|
||||
const config = watch("destinationConfig");
|
||||
|
||||
return (
|
||||
<>
|
||||
<GenericFieldLabel label="Scope">{scope}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Service">{serviceName}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Scope">{config.scope}</GenericFieldLabel>
|
||||
{config.scope === RenderSyncScope.Service ? (
|
||||
<GenericFieldLabel label="Service">
|
||||
{config.serviceName ?? config.serviceId}
|
||||
</GenericFieldLabel>
|
||||
) : (
|
||||
<GenericFieldLabel label="Service">
|
||||
{config.environmentGroupName ?? config.environmentGroupId}
|
||||
</GenericFieldLabel>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@@ -17,6 +17,12 @@ export const RenderSyncDestinationSchema = BaseSecretSyncSchema(
|
||||
serviceId: z.string().trim().min(1, "Service is required"),
|
||||
serviceName: z.string().trim().optional(),
|
||||
type: z.nativeEnum(RenderSyncType)
|
||||
}),
|
||||
z.object({
|
||||
scope: z.literal(RenderSyncScope.EnvironmentGroup),
|
||||
environmentGroupId: z.string().trim().min(1, "Environment Group ID is required"),
|
||||
environmentGroupName: z.string().trim().optional(),
|
||||
type: z.nativeEnum(RenderSyncType)
|
||||
})
|
||||
])
|
||||
})
|
||||
|
@@ -165,3 +165,61 @@ export function parseYaml(src: ArrayBuffer | string) {
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function detectSeparator(csvContent: string): string {
|
||||
const firstLine = csvContent.split("\n")[0];
|
||||
const separators = [",", ";", "\t", "|"];
|
||||
|
||||
const counts = separators.map((sep) => ({
|
||||
separator: sep,
|
||||
count: (firstLine.match(new RegExp(`\\${sep}`, "g")) || []).length
|
||||
}));
|
||||
|
||||
const detected = counts.reduce((max, curr) => (curr.count > max.count ? curr : max));
|
||||
|
||||
return detected.count > 0 ? detected.separator : ",";
|
||||
}
|
||||
|
||||
export function parseCsvToMatrix(src: ArrayBuffer | string): string[][] {
|
||||
let csvContent: string;
|
||||
if (typeof src === "string") {
|
||||
csvContent = src;
|
||||
} else {
|
||||
csvContent = new TextDecoder("utf-8").decode(src);
|
||||
}
|
||||
|
||||
const separator = detectSeparator(csvContent);
|
||||
const lines = csvContent.replace(/\r\n?/g, "\n").split("\n");
|
||||
const matrix: string[][] = [];
|
||||
|
||||
lines.forEach((line) => {
|
||||
if (line.trim() !== "") {
|
||||
const cells: string[] = [];
|
||||
let currentCell = "";
|
||||
let inQuote = false;
|
||||
|
||||
for (let i = 0; i < line.length; i += 1) {
|
||||
const char = line[i];
|
||||
const nextChar = line[i + 1];
|
||||
|
||||
if (char === '"') {
|
||||
if (inQuote && nextChar === '"') {
|
||||
currentCell += '"';
|
||||
i += 1;
|
||||
} else {
|
||||
inQuote = !inQuote;
|
||||
}
|
||||
} else if (char === separator && !inQuote) {
|
||||
cells.push(currentCell.trim());
|
||||
currentCell = "";
|
||||
} else {
|
||||
currentCell += char;
|
||||
}
|
||||
}
|
||||
cells.push(currentCell.trim());
|
||||
matrix.push(cells);
|
||||
}
|
||||
});
|
||||
|
||||
return matrix;
|
||||
}
|
||||
|
@@ -143,6 +143,13 @@ export enum ProjectPermissionSecretScanningConfigActions {
|
||||
Update = "update-configs"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionSecretEventActions {
|
||||
SubscribeCreated = "subscribe-on-created",
|
||||
SubscribeUpdated = "subscribe-on-updated",
|
||||
SubscribeDeleted = "subscribe-on-deleted",
|
||||
SubscribeImportMutations = "subscribe-on-import-mutations"
|
||||
}
|
||||
|
||||
export enum PermissionConditionOperators {
|
||||
$IN = "$in",
|
||||
$ALL = "$all",
|
||||
@@ -172,7 +179,8 @@ export type ConditionalProjectPermissionSubject =
|
||||
| ProjectPermissionSub.CertificateTemplates
|
||||
| ProjectPermissionSub.SecretFolders
|
||||
| ProjectPermissionSub.SecretImports
|
||||
| ProjectPermissionSub.SecretRotation;
|
||||
| ProjectPermissionSub.SecretRotation
|
||||
| ProjectPermissionSub.SecretEvents;
|
||||
|
||||
export const formatedConditionsOperatorNames: { [K in PermissionConditionOperators]: string } = {
|
||||
[PermissionConditionOperators.$EQ]: "equal to",
|
||||
@@ -250,7 +258,8 @@ export enum ProjectPermissionSub {
|
||||
Commits = "commits",
|
||||
SecretScanningDataSources = "secret-scanning-data-sources",
|
||||
SecretScanningFindings = "secret-scanning-findings",
|
||||
SecretScanningConfigs = "secret-scanning-configs"
|
||||
SecretScanningConfigs = "secret-scanning-configs",
|
||||
SecretEvents = "secret-events"
|
||||
}
|
||||
|
||||
export type SecretSubjectFields = {
|
||||
@@ -260,6 +269,14 @@ export type SecretSubjectFields = {
|
||||
secretTags: string[];
|
||||
};
|
||||
|
||||
export type SecretEventSubjectFields = {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
secretName: string;
|
||||
secretTags: string[];
|
||||
action: string;
|
||||
};
|
||||
|
||||
export type SecretFolderSubjectFields = {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
@@ -403,6 +420,13 @@ export type ProjectPermissionSet =
|
||||
ProjectPermissionSub.SecretScanningDataSources
|
||||
]
|
||||
| [ProjectPermissionSecretScanningFindingActions, ProjectPermissionSub.SecretScanningFindings]
|
||||
| [ProjectPermissionSecretScanningConfigActions, ProjectPermissionSub.SecretScanningConfigs];
|
||||
| [ProjectPermissionSecretScanningConfigActions, ProjectPermissionSub.SecretScanningConfigs]
|
||||
| [
|
||||
ProjectPermissionSecretEventActions,
|
||||
(
|
||||
| ProjectPermissionSub.SecretEvents
|
||||
| (ForcedSubject<ProjectPermissionSub.SecretEvents> & SecretEventSubjectFields)
|
||||
)
|
||||
];
|
||||
|
||||
export type TProjectPermission = MongoAbility<ProjectPermissionSet>;
|
||||
|
@@ -1,84 +0,0 @@
|
||||
import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm";
|
||||
import { deriveArgonKey } from "@app/components/utilities/cryptography/crypto";
|
||||
|
||||
/**
|
||||
* @param {Object} obj
|
||||
* @param {Number} obj.encryptionVersion
|
||||
* @param {String} obj.encryptedPrivateKey
|
||||
* @param {String} obj.iv
|
||||
* @param {String} obj.tag
|
||||
* @param {String} obj.password
|
||||
* @param {String} obj.salt
|
||||
* @param {String} obj.protectedKey
|
||||
* @param {String} obj.protectedKeyIV
|
||||
* @param {String} obj.protectedKeyTag
|
||||
*/
|
||||
const decryptPrivateKeyHelper = async ({
|
||||
encryptionVersion,
|
||||
encryptedPrivateKey,
|
||||
iv,
|
||||
tag,
|
||||
password,
|
||||
salt,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag
|
||||
}: {
|
||||
encryptionVersion: number;
|
||||
encryptedPrivateKey: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
password: string;
|
||||
salt: string;
|
||||
protectedKey?: string;
|
||||
protectedKeyIV?: string;
|
||||
protectedKeyTag?: string;
|
||||
}) => {
|
||||
let privateKey;
|
||||
try {
|
||||
if (encryptionVersion === 1) {
|
||||
privateKey = Aes256Gcm.decrypt({
|
||||
ciphertext: encryptedPrivateKey,
|
||||
iv,
|
||||
tag,
|
||||
secret: password
|
||||
.slice(0, 32)
|
||||
.padStart(32 + (password.slice(0, 32).length - new Blob([password]).size), "0")
|
||||
});
|
||||
} else if (encryptionVersion === 2 && protectedKey && protectedKeyIV && protectedKeyTag) {
|
||||
const derivedKey = await deriveArgonKey({
|
||||
password,
|
||||
salt,
|
||||
mem: 65536,
|
||||
time: 3,
|
||||
parallelism: 1,
|
||||
hashLen: 32
|
||||
});
|
||||
|
||||
if (!derivedKey) throw new Error("Failed to generate derived key");
|
||||
|
||||
const key = Aes256Gcm.decrypt({
|
||||
ciphertext: protectedKey,
|
||||
iv: protectedKeyIV,
|
||||
tag: protectedKeyTag,
|
||||
secret: Buffer.from(derivedKey.hash)
|
||||
});
|
||||
|
||||
// decrypt back the private key
|
||||
privateKey = Aes256Gcm.decrypt({
|
||||
ciphertext: encryptedPrivateKey,
|
||||
iv,
|
||||
tag,
|
||||
secret: Buffer.from(key, "hex")
|
||||
});
|
||||
} else {
|
||||
throw new Error("Insufficient details to decrypt private key");
|
||||
}
|
||||
} catch {
|
||||
throw new Error("Failed to decrypt private key");
|
||||
}
|
||||
|
||||
return privateKey;
|
||||
};
|
||||
|
||||
export { decryptPrivateKeyHelper };
|
@@ -212,5 +212,9 @@ export const RENDER_SYNC_SCOPES: Record<RenderSyncScope, { name: string; descrip
|
||||
[RenderSyncScope.Service]: {
|
||||
name: "Service",
|
||||
description: "Infisical will sync secrets to the specified Render service."
|
||||
},
|
||||
[RenderSyncScope.EnvironmentGroup]: {
|
||||
name: "EnvironmentGroup",
|
||||
description: "Infisical will sync secrets to the specified Render environment group."
|
||||
}
|
||||
};
|
||||
|
@@ -6,10 +6,12 @@ import { apiRequest } from "@app/config/request";
|
||||
import { accessApprovalKeys } from "./queries";
|
||||
import {
|
||||
TAccessApproval,
|
||||
TAccessApprovalRequest,
|
||||
TCreateAccessPolicyDTO,
|
||||
TCreateAccessRequestDTO,
|
||||
TDeleteSecretPolicyDTO,
|
||||
TUpdateAccessPolicyDTO
|
||||
TUpdateAccessPolicyDTO,
|
||||
TUpdateAccessRequestDTO
|
||||
} from "./types";
|
||||
|
||||
export const useCreateAccessApprovalPolicy = () => {
|
||||
@@ -134,6 +136,25 @@ export const useCreateAccessRequest = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateAccessRequest = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<TAccessApprovalRequest, object, TUpdateAccessRequestDTO>({
|
||||
mutationFn: async ({ requestId, ...payload }) => {
|
||||
const { data } = await apiRequest.patch<{ approval: TAccessApprovalRequest }>(
|
||||
`/api/v1/access-approvals/requests/${requestId}`,
|
||||
payload
|
||||
);
|
||||
|
||||
return data.approval;
|
||||
},
|
||||
onSuccess: (_, { projectSlug }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: accessApprovalKeys.getAccessApprovalRequests(projectSlug)
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useReviewAccessRequest = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<
|
||||
|
@@ -24,7 +24,7 @@ export const accessApprovalKeys = {
|
||||
envSlug?: string,
|
||||
requestedBy?: string,
|
||||
bypassReason?: string
|
||||
) => [{ projectSlug, envSlug, requestedBy, bypassReason }, "access-approvals-requests"] as const,
|
||||
) => ["access-approvals-requests", projectSlug, envSlug, requestedBy, bypassReason] as const,
|
||||
getAccessApprovalRequestCount: (projectSlug: string, policyId?: string) =>
|
||||
[{ projectSlug }, "access-approval-request-count", ...(policyId ? [policyId] : [])] as const
|
||||
};
|
||||
|
@@ -103,6 +103,8 @@ export type TAccessApprovalRequest = {
|
||||
}[];
|
||||
|
||||
note?: string;
|
||||
editNote?: string;
|
||||
editedByUserId?: string;
|
||||
};
|
||||
|
||||
export type TAccessApproval = {
|
||||
@@ -146,6 +148,13 @@ export type TCreateAccessRequestDTO = {
|
||||
note?: string;
|
||||
} & Omit<TProjectUserPrivilege, "id" | "createdAt" | "updatedAt" | "slug" | "projectMembershipId">;
|
||||
|
||||
export type TUpdateAccessRequestDTO = {
|
||||
requestId: string;
|
||||
editNote: string;
|
||||
temporaryRange: string;
|
||||
projectSlug: string;
|
||||
};
|
||||
|
||||
export type TGetAccessApprovalRequestsDTO = {
|
||||
projectSlug: string;
|
||||
policyId?: string;
|
||||
|
@@ -74,15 +74,6 @@ export type TCreateAdminUserDTO = {
|
||||
password: string;
|
||||
firstName: string;
|
||||
lastName?: string;
|
||||
protectedKey: string;
|
||||
protectedKeyTag: string;
|
||||
protectedKeyIV: string;
|
||||
encryptedPrivateKey: string;
|
||||
encryptedPrivateKeyIV: string;
|
||||
encryptedPrivateKeyTag: string;
|
||||
publicKey: string;
|
||||
verifier: string;
|
||||
salt: string;
|
||||
};
|
||||
|
||||
export type AdminGetOrganizationsFilters = {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user