mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-10 21:29:20 +00:00
Fix merge conflicts
This commit is contained in:
@@ -0,0 +1,27 @@
|
|||||||
|
name: Release Gateway Helm Chart
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release-helm:
|
||||||
|
name: Release Helm Chart
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Helm
|
||||||
|
uses: azure/setup-helm@v3
|
||||||
|
with:
|
||||||
|
version: v3.10.0
|
||||||
|
|
||||||
|
- name: Install python
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
|
||||||
|
- name: Install Cloudsmith CLI
|
||||||
|
run: pip install --upgrade cloudsmith-cli
|
||||||
|
|
||||||
|
- name: Build and push helm package to CloudSmith
|
||||||
|
run: cd helm-charts && sh upload-gateway-cloudsmith.sh
|
||||||
|
env:
|
||||||
|
CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }}
|
||||||
@@ -69,6 +69,15 @@ module.exports = {
|
|||||||
["^\\."]
|
["^\\."]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"import/extensions": [
|
||||||
|
"error",
|
||||||
|
"ignorePackages",
|
||||||
|
{
|
||||||
|
"": "never", // this is required to get the .tsx to work...
|
||||||
|
ts: "never",
|
||||||
|
tsx: "never"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Generated
+2361
-132
File diff suppressed because it is too large
Load Diff
@@ -72,7 +72,8 @@
|
|||||||
"seed:new": "tsx ./scripts/create-seed-file.ts",
|
"seed:new": "tsx ./scripts/create-seed-file.ts",
|
||||||
"seed": "knex --knexfile ./dist/db/knexfile.ts --client pg seed:run",
|
"seed": "knex --knexfile ./dist/db/knexfile.ts --client pg seed:run",
|
||||||
"seed-dev": "knex --knexfile ./src/db/knexfile.ts --client pg seed:run",
|
"seed-dev": "knex --knexfile ./src/db/knexfile.ts --client pg seed:run",
|
||||||
"db:reset": "npm run migration:rollback -- --all && npm run migration:latest"
|
"db:reset": "npm run migration:rollback -- --all && npm run migration:latest",
|
||||||
|
"email:dev": "email dev --dir src/services/smtp/emails"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
@@ -96,6 +97,7 @@
|
|||||||
"@types/picomatch": "^2.3.3",
|
"@types/picomatch": "^2.3.3",
|
||||||
"@types/pkcs11js": "^1.0.4",
|
"@types/pkcs11js": "^1.0.4",
|
||||||
"@types/prompt-sync": "^4.2.3",
|
"@types/prompt-sync": "^4.2.3",
|
||||||
|
"@types/react": "^19.1.2",
|
||||||
"@types/resolve": "^1.20.6",
|
"@types/resolve": "^1.20.6",
|
||||||
"@types/safe-regex": "^1.1.6",
|
"@types/safe-regex": "^1.1.6",
|
||||||
"@types/sjcl": "^1.0.34",
|
"@types/sjcl": "^1.0.34",
|
||||||
@@ -115,6 +117,7 @@
|
|||||||
"nodemon": "^3.0.2",
|
"nodemon": "^3.0.2",
|
||||||
"pino-pretty": "^10.2.3",
|
"pino-pretty": "^10.2.3",
|
||||||
"prompt-sync": "^4.2.0",
|
"prompt-sync": "^4.2.0",
|
||||||
|
"react-email": "4.0.7",
|
||||||
"rimraf": "^5.0.5",
|
"rimraf": "^5.0.5",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"tsc-alias": "^1.8.8",
|
"tsc-alias": "^1.8.8",
|
||||||
@@ -164,6 +167,7 @@
|
|||||||
"@opentelemetry/semantic-conventions": "^1.27.0",
|
"@opentelemetry/semantic-conventions": "^1.27.0",
|
||||||
"@peculiar/asn1-schema": "^2.3.8",
|
"@peculiar/asn1-schema": "^2.3.8",
|
||||||
"@peculiar/x509": "^1.12.1",
|
"@peculiar/x509": "^1.12.1",
|
||||||
|
"@react-email/components": "0.0.36",
|
||||||
"@serdnam/pino-cloudwatch-transport": "^1.0.4",
|
"@serdnam/pino-cloudwatch-transport": "^1.0.4",
|
||||||
"@sindresorhus/slugify": "1.1.0",
|
"@sindresorhus/slugify": "1.1.0",
|
||||||
"@slack/oauth": "^3.0.2",
|
"@slack/oauth": "^3.0.2",
|
||||||
@@ -223,6 +227,8 @@
|
|||||||
"posthog-node": "^3.6.2",
|
"posthog-node": "^3.6.2",
|
||||||
"probot": "^13.3.8",
|
"probot": "^13.3.8",
|
||||||
"re2": "^1.21.4",
|
"re2": "^1.21.4",
|
||||||
|
"react": "19.1.0",
|
||||||
|
"react-dom": "19.1.0",
|
||||||
"safe-regex": "^2.1.1",
|
"safe-regex": "^2.1.1",
|
||||||
"scim-patch": "^0.8.3",
|
"scim-patch": "^0.8.3",
|
||||||
"scim2-parse-filter": "^0.2.10",
|
"scim2-parse-filter": "^0.2.10",
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasTable(TableName.CertificateBody)) {
|
||||||
|
await knex.schema.alterTable(TableName.CertificateBody, (t) => {
|
||||||
|
t.binary("encryptedCertificateChain").nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await knex.schema.hasTable(TableName.CertificateSecret))) {
|
||||||
|
await knex.schema.createTable(TableName.CertificateSecret, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
t.uuid("certId").notNullable().unique();
|
||||||
|
t.foreign("certId").references("id").inTable(TableName.Certificate).onDelete("CASCADE");
|
||||||
|
t.binary("encryptedPrivateKey").notNullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasTable(TableName.CertificateSecret)) {
|
||||||
|
await knex.schema.dropTable(TableName.CertificateSecret);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await knex.schema.hasTable(TableName.CertificateBody)) {
|
||||||
|
await knex.schema.alterTable(TableName.CertificateBody, (t) => {
|
||||||
|
t.dropColumn("encryptedCertificateChain");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,7 +14,8 @@ export const CertificateBodiesSchema = z.object({
|
|||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
certId: z.string().uuid(),
|
certId: z.string().uuid(),
|
||||||
encryptedCertificate: zodBuffer
|
encryptedCertificate: zodBuffer,
|
||||||
|
encryptedCertificateChain: zodBuffer.nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TCertificateBodies = z.infer<typeof CertificateBodiesSchema>;
|
export type TCertificateBodies = z.infer<typeof CertificateBodiesSchema>;
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { zodBuffer } from "@app/lib/zod";
|
||||||
|
|
||||||
import { TImmutableDBKeys } from "./models";
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
export const CertificateSecretsSchema = z.object({
|
export const CertificateSecretsSchema = z.object({
|
||||||
@@ -12,8 +14,7 @@ export const CertificateSecretsSchema = z.object({
|
|||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
certId: z.string().uuid(),
|
certId: z.string().uuid(),
|
||||||
pk: z.string(),
|
encryptedPrivateKey: zodBuffer
|
||||||
sk: z.string()
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TCertificateSecrets = z.infer<typeof CertificateSecretsSchema>;
|
export type TCertificateSecrets = z.infer<typeof CertificateSecretsSchema>;
|
||||||
|
|||||||
@@ -225,6 +225,8 @@ export enum EventType {
|
|||||||
DELETE_CERT = "delete-cert",
|
DELETE_CERT = "delete-cert",
|
||||||
REVOKE_CERT = "revoke-cert",
|
REVOKE_CERT = "revoke-cert",
|
||||||
GET_CERT_BODY = "get-cert-body",
|
GET_CERT_BODY = "get-cert-body",
|
||||||
|
GET_CERT_PRIVATE_KEY = "get-cert-private-key",
|
||||||
|
GET_CERT_BUNDLE = "get-cert-bundle",
|
||||||
CREATE_PKI_ALERT = "create-pki-alert",
|
CREATE_PKI_ALERT = "create-pki-alert",
|
||||||
GET_PKI_ALERT = "get-pki-alert",
|
GET_PKI_ALERT = "get-pki-alert",
|
||||||
UPDATE_PKI_ALERT = "update-pki-alert",
|
UPDATE_PKI_ALERT = "update-pki-alert",
|
||||||
@@ -1795,6 +1797,24 @@ interface GetCertBody {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface GetCertPrivateKey {
|
||||||
|
type: EventType.GET_CERT_PRIVATE_KEY;
|
||||||
|
metadata: {
|
||||||
|
certId: string;
|
||||||
|
cn: string;
|
||||||
|
serialNumber: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetCertBundle {
|
||||||
|
type: EventType.GET_CERT_BUNDLE;
|
||||||
|
metadata: {
|
||||||
|
certId: string;
|
||||||
|
cn: string;
|
||||||
|
serialNumber: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface CreatePkiAlert {
|
interface CreatePkiAlert {
|
||||||
type: EventType.CREATE_PKI_ALERT;
|
type: EventType.CREATE_PKI_ALERT;
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -2871,6 +2891,8 @@ export type Event =
|
|||||||
| DeleteCert
|
| DeleteCert
|
||||||
| RevokeCert
|
| RevokeCert
|
||||||
| GetCertBody
|
| GetCertBody
|
||||||
|
| GetCertPrivateKey
|
||||||
|
| GetCertBundle
|
||||||
| CreatePkiAlert
|
| CreatePkiAlert
|
||||||
| GetPkiAlert
|
| GetPkiAlert
|
||||||
| UpdatePkiAlert
|
| UpdatePkiAlert
|
||||||
|
|||||||
@@ -24,8 +24,16 @@ export const verifyHostInputValidity = async (host: string, isGateway = false) =
|
|||||||
if (net.isIPv4(el)) {
|
if (net.isIPv4(el)) {
|
||||||
exclusiveIps.push(el);
|
exclusiveIps.push(el);
|
||||||
} else {
|
} else {
|
||||||
|
try {
|
||||||
const resolvedIps = await dns.resolve4(el);
|
const resolvedIps = await dns.resolve4(el);
|
||||||
exclusiveIps.push(...resolvedIps);
|
exclusiveIps.push(...resolvedIps);
|
||||||
|
} catch (error) {
|
||||||
|
// only try lookup if not found
|
||||||
|
if ((error as { code: string })?.code !== "ENOTFOUND") throw error;
|
||||||
|
|
||||||
|
const resolvedIps = (await dns.lookup(el, { all: true, family: 4 })).map(({ address }) => address);
|
||||||
|
exclusiveIps.push(...resolvedIps);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -38,8 +46,16 @@ export const verifyHostInputValidity = async (host: string, isGateway = false) =
|
|||||||
if (normalizedHost === "localhost" || normalizedHost === "host.docker.internal") {
|
if (normalizedHost === "localhost" || normalizedHost === "host.docker.internal") {
|
||||||
throw new BadRequestError({ message: "Invalid db host" });
|
throw new BadRequestError({ message: "Invalid db host" });
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
const resolvedIps = await dns.resolve4(host);
|
const resolvedIps = await dns.resolve4(host);
|
||||||
inputHostIps.push(...resolvedIps);
|
inputHostIps.push(...resolvedIps);
|
||||||
|
} catch (error) {
|
||||||
|
// only try lookup if not found
|
||||||
|
if ((error as { code: string })?.code !== "ENOTFOUND") throw error;
|
||||||
|
|
||||||
|
const resolvedIps = (await dns.lookup(host, { all: true, family: 4 })).map(({ address }) => address);
|
||||||
|
inputHostIps.push(...resolvedIps);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isGateway && !(appCfg.DYNAMIC_SECRET_ALLOW_INTERNAL_IP || appCfg.ALLOW_INTERNAL_IP_CONNECTIONS)) {
|
if (!isGateway && !(appCfg.DYNAMIC_SECRET_ALLOW_INTERNAL_IP || appCfg.ALLOW_INTERNAL_IP_CONNECTIONS)) {
|
||||||
|
|||||||
@@ -17,6 +17,14 @@ export enum ProjectPermissionActions {
|
|||||||
Delete = "delete"
|
Delete = "delete"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ProjectPermissionCertificateActions {
|
||||||
|
Read = "read",
|
||||||
|
Create = "create",
|
||||||
|
Edit = "edit",
|
||||||
|
Delete = "delete",
|
||||||
|
ReadPrivateKey = "read-private-key"
|
||||||
|
}
|
||||||
|
|
||||||
export enum ProjectPermissionSecretActions {
|
export enum ProjectPermissionSecretActions {
|
||||||
DescribeAndReadValue = "read",
|
DescribeAndReadValue = "read",
|
||||||
DescribeSecret = "describeSecret",
|
DescribeSecret = "describeSecret",
|
||||||
@@ -242,7 +250,7 @@ export type ProjectPermissionSet =
|
|||||||
ProjectPermissionSub.Identity | (ForcedSubject<ProjectPermissionSub.Identity> & IdentityManagementSubjectFields)
|
ProjectPermissionSub.Identity | (ForcedSubject<ProjectPermissionSub.Identity> & IdentityManagementSubjectFields)
|
||||||
]
|
]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities]
|
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.Certificates]
|
| [ProjectPermissionCertificateActions, ProjectPermissionSub.Certificates]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates]
|
| [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateAuthorities]
|
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateAuthorities]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificates]
|
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificates]
|
||||||
@@ -489,7 +497,7 @@ const GeneralPermissionSchema = [
|
|||||||
}),
|
}),
|
||||||
z.object({
|
z.object({
|
||||||
subject: z.literal(ProjectPermissionSub.Certificates).describe("The entity this permission pertains to."),
|
subject: z.literal(ProjectPermissionSub.Certificates).describe("The entity this permission pertains to."),
|
||||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionCertificateActions).describe(
|
||||||
"Describe what action an entity can take."
|
"Describe what action an entity can take."
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
@@ -699,7 +707,6 @@ const buildAdminPermissionRules = () => {
|
|||||||
ProjectPermissionSub.AuditLogs,
|
ProjectPermissionSub.AuditLogs,
|
||||||
ProjectPermissionSub.IpAllowList,
|
ProjectPermissionSub.IpAllowList,
|
||||||
ProjectPermissionSub.CertificateAuthorities,
|
ProjectPermissionSub.CertificateAuthorities,
|
||||||
ProjectPermissionSub.Certificates,
|
|
||||||
ProjectPermissionSub.CertificateTemplates,
|
ProjectPermissionSub.CertificateTemplates,
|
||||||
ProjectPermissionSub.PkiAlerts,
|
ProjectPermissionSub.PkiAlerts,
|
||||||
ProjectPermissionSub.PkiCollections,
|
ProjectPermissionSub.PkiCollections,
|
||||||
@@ -719,6 +726,17 @@ const buildAdminPermissionRules = () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
can(
|
||||||
|
[
|
||||||
|
ProjectPermissionCertificateActions.Read,
|
||||||
|
ProjectPermissionCertificateActions.Edit,
|
||||||
|
ProjectPermissionCertificateActions.Create,
|
||||||
|
ProjectPermissionCertificateActions.Delete,
|
||||||
|
ProjectPermissionCertificateActions.ReadPrivateKey
|
||||||
|
],
|
||||||
|
ProjectPermissionSub.Certificates
|
||||||
|
);
|
||||||
|
|
||||||
can(
|
can(
|
||||||
[
|
[
|
||||||
ProjectPermissionSshHostActions.Edit,
|
ProjectPermissionSshHostActions.Edit,
|
||||||
@@ -987,10 +1005,10 @@ const buildMemberPermissionRules = () => {
|
|||||||
|
|
||||||
can(
|
can(
|
||||||
[
|
[
|
||||||
ProjectPermissionActions.Read,
|
ProjectPermissionCertificateActions.Read,
|
||||||
ProjectPermissionActions.Edit,
|
ProjectPermissionCertificateActions.Edit,
|
||||||
ProjectPermissionActions.Create,
|
ProjectPermissionCertificateActions.Create,
|
||||||
ProjectPermissionActions.Delete
|
ProjectPermissionCertificateActions.Delete
|
||||||
],
|
],
|
||||||
ProjectPermissionSub.Certificates
|
ProjectPermissionSub.Certificates
|
||||||
);
|
);
|
||||||
@@ -1064,7 +1082,7 @@ const buildViewerPermissionRules = () => {
|
|||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
|
can(ProjectPermissionCertificateActions.Read, ProjectPermissionSub.Certificates);
|
||||||
can(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek);
|
can(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificates);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificates);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateTemplates);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateTemplates);
|
||||||
|
|||||||
@@ -219,7 +219,7 @@ export const parseRotationErrorMessage = (err: unknown): string => {
|
|||||||
if (err instanceof AxiosError) {
|
if (err instanceof AxiosError) {
|
||||||
errorMessage += err?.response?.data
|
errorMessage += err?.response?.data
|
||||||
? JSON.stringify(err?.response?.data)
|
? JSON.stringify(err?.response?.data)
|
||||||
: err?.message ?? "An unknown error occurred.";
|
: (err?.message ?? "An unknown error occurred.");
|
||||||
} else {
|
} else {
|
||||||
errorMessage += (err as Error)?.message || "An unknown error occurred.";
|
errorMessage += (err as Error)?.message || "An unknown error occurred.";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -282,7 +282,7 @@ export const sshCertificateAuthorityServiceFactory = ({
|
|||||||
|
|
||||||
// set [keyId] depending on if [allowCustomKeyIds] is true or false
|
// set [keyId] depending on if [allowCustomKeyIds] is true or false
|
||||||
const keyId = sshCertificateTemplate.allowCustomKeyIds
|
const keyId = sshCertificateTemplate.allowCustomKeyIds
|
||||||
? requestedKeyId ?? `${actor}-${actorId}`
|
? (requestedKeyId ?? `${actor}-${actorId}`)
|
||||||
: `${actor}-${actorId}`;
|
: `${actor}-${actorId}`;
|
||||||
|
|
||||||
const sshCaSecret = await sshCertificateAuthoritySecretDAL.findOne({ sshCaId: sshCertificateTemplate.sshCaId });
|
const sshCaSecret = await sshCertificateAuthoritySecretDAL.findOne({ sshCaId: sshCertificateTemplate.sshCaId });
|
||||||
@@ -404,7 +404,7 @@ export const sshCertificateAuthorityServiceFactory = ({
|
|||||||
|
|
||||||
// set [keyId] depending on if [allowCustomKeyIds] is true or false
|
// set [keyId] depending on if [allowCustomKeyIds] is true or false
|
||||||
const keyId = sshCertificateTemplate.allowCustomKeyIds
|
const keyId = sshCertificateTemplate.allowCustomKeyIds
|
||||||
? requestedKeyId ?? `${actor}-${actorId}`
|
? (requestedKeyId ?? `${actor}-${actorId}`)
|
||||||
: `${actor}-${actorId}`;
|
: `${actor}-${actorId}`;
|
||||||
|
|
||||||
const sshCaSecret = await sshCertificateAuthoritySecretDAL.findOne({ sshCaId: sshCertificateTemplate.sshCaId });
|
const sshCaSecret = await sshCertificateAuthoritySecretDAL.findOne({ sshCaId: sshCertificateTemplate.sshCaId });
|
||||||
|
|||||||
@@ -1623,7 +1623,8 @@ export const CERTIFICATES = {
|
|||||||
serialNumber: "The serial number of the certificate to get the certificate body and certificate chain for.",
|
serialNumber: "The serial number of the certificate to get the certificate body and certificate chain for.",
|
||||||
certificate: "The certificate body of the certificate.",
|
certificate: "The certificate body of the certificate.",
|
||||||
certificateChain: "The certificate chain of the certificate.",
|
certificateChain: "The certificate chain of the certificate.",
|
||||||
serialNumberRes: "The serial number of the certificate."
|
serialNumberRes: "The serial number of the certificate.",
|
||||||
|
privateKey: "The private key of the certificate."
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { FastifyReply } from "fastify";
|
||||||
|
|
||||||
|
export const addNoCacheHeaders = (reply: FastifyReply) => {
|
||||||
|
void reply.header("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate");
|
||||||
|
void reply.header("Pragma", "no-cache");
|
||||||
|
void reply.header("Expires", "0");
|
||||||
|
void reply.header("Surrogate-Control", "no-store");
|
||||||
|
};
|
||||||
@@ -126,6 +126,7 @@ import { tokenDALFactory } from "@app/services/auth-token/auth-token-dal";
|
|||||||
import { tokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
import { tokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
||||||
import { certificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
|
import { certificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
|
||||||
import { certificateDALFactory } from "@app/services/certificate/certificate-dal";
|
import { certificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||||
|
import { certificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
|
||||||
import { certificateServiceFactory } from "@app/services/certificate/certificate-service";
|
import { certificateServiceFactory } from "@app/services/certificate/certificate-service";
|
||||||
import { certificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
|
import { certificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
|
||||||
import { certificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
|
import { certificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
|
||||||
@@ -814,6 +815,7 @@ export const registerRoutes = async (
|
|||||||
|
|
||||||
const certificateDAL = certificateDALFactory(db);
|
const certificateDAL = certificateDALFactory(db);
|
||||||
const certificateBodyDAL = certificateBodyDALFactory(db);
|
const certificateBodyDAL = certificateBodyDALFactory(db);
|
||||||
|
const certificateSecretDAL = certificateSecretDALFactory(db);
|
||||||
|
|
||||||
const pkiAlertDAL = pkiAlertDALFactory(db);
|
const pkiAlertDAL = pkiAlertDALFactory(db);
|
||||||
const pkiCollectionDAL = pkiCollectionDALFactory(db);
|
const pkiCollectionDAL = pkiCollectionDALFactory(db);
|
||||||
@@ -823,6 +825,7 @@ export const registerRoutes = async (
|
|||||||
const certificateService = certificateServiceFactory({
|
const certificateService = certificateServiceFactory({
|
||||||
certificateDAL,
|
certificateDAL,
|
||||||
certificateBodyDAL,
|
certificateBodyDAL,
|
||||||
|
certificateSecretDAL,
|
||||||
certificateAuthorityDAL,
|
certificateAuthorityDAL,
|
||||||
certificateAuthorityCertDAL,
|
certificateAuthorityCertDAL,
|
||||||
certificateAuthorityCrlDAL,
|
certificateAuthorityCrlDAL,
|
||||||
@@ -894,6 +897,7 @@ export const registerRoutes = async (
|
|||||||
certificateAuthorityQueue,
|
certificateAuthorityQueue,
|
||||||
certificateDAL,
|
certificateDAL,
|
||||||
certificateBodyDAL,
|
certificateBodyDAL,
|
||||||
|
certificateSecretDAL,
|
||||||
pkiCollectionDAL,
|
pkiCollectionDAL,
|
||||||
pkiCollectionItemDAL,
|
pkiCollectionItemDAL,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-floating-promises */
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { CertificatesSchema } from "@app/db/schemas";
|
import { CertificatesSchema } from "@app/db/schemas";
|
||||||
@@ -5,6 +6,7 @@ import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
|||||||
import { ApiDocsTags, CERTIFICATE_AUTHORITIES, CERTIFICATES } from "@app/lib/api-docs";
|
import { ApiDocsTags, CERTIFICATE_AUTHORITIES, CERTIFICATES } from "@app/lib/api-docs";
|
||||||
import { ms } from "@app/lib/ms";
|
import { ms } from "@app/lib/ms";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { addNoCacheHeaders } from "@app/server/lib/caching";
|
||||||
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
@@ -64,6 +66,111 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// TODO: In the future add support for other formats outside of PEM (such as DER). Adding a "format" query param may be best.
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:serialNumber/private-key",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
hide: false,
|
||||||
|
tags: [ApiDocsTags.PkiCertificates],
|
||||||
|
description: "Get certificate private key",
|
||||||
|
params: z.object({
|
||||||
|
serialNumber: z.string().trim().describe(CERTIFICATES.GET.serialNumber)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.string().trim()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req, reply) => {
|
||||||
|
const { ca, cert, certPrivateKey } = await server.services.certificate.getCertPrivateKey({
|
||||||
|
serialNumber: req.params.serialNumber,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: ca.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_CERT_PRIVATE_KEY,
|
||||||
|
metadata: {
|
||||||
|
certId: cert.id,
|
||||||
|
cn: cert.commonName,
|
||||||
|
serialNumber: cert.serialNumber
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
addNoCacheHeaders(reply);
|
||||||
|
|
||||||
|
return certPrivateKey;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: In the future add support for other formats outside of PEM (such as DER). Adding a "format" query param may be best.
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:serialNumber/bundle",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
hide: false,
|
||||||
|
tags: [ApiDocsTags.PkiCertificates],
|
||||||
|
description: "Get certificate bundle including the certificate, chain, and private key.",
|
||||||
|
params: z.object({
|
||||||
|
serialNumber: z.string().trim().describe(CERTIFICATES.GET_CERT.serialNumber)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
certificate: z.string().trim().describe(CERTIFICATES.GET_CERT.certificate),
|
||||||
|
certificateChain: z.string().trim().nullish().describe(CERTIFICATES.GET_CERT.certificateChain),
|
||||||
|
privateKey: z.string().trim().describe(CERTIFICATES.GET_CERT.privateKey),
|
||||||
|
serialNumber: z.string().trim().describe(CERTIFICATES.GET_CERT.serialNumberRes)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req, reply) => {
|
||||||
|
const { certificate, certificateChain, serialNumber, cert, ca, privateKey } =
|
||||||
|
await server.services.certificate.getCertBundle({
|
||||||
|
serialNumber: req.params.serialNumber,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: ca.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_CERT_BUNDLE,
|
||||||
|
metadata: {
|
||||||
|
certId: cert.id,
|
||||||
|
cn: cert.commonName,
|
||||||
|
serialNumber: cert.serialNumber
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
addNoCacheHeaders(reply);
|
||||||
|
|
||||||
|
return {
|
||||||
|
certificate,
|
||||||
|
certificateChain,
|
||||||
|
serialNumber,
|
||||||
|
privateKey
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "/issue-certificate",
|
url: "/issue-certificate",
|
||||||
@@ -411,7 +518,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
|||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
certificate: z.string().trim().describe(CERTIFICATES.GET_CERT.certificate),
|
certificate: z.string().trim().describe(CERTIFICATES.GET_CERT.certificate),
|
||||||
certificateChain: z.string().trim().describe(CERTIFICATES.GET_CERT.certificateChain),
|
certificateChain: z.string().trim().nullish().describe(CERTIFICATES.GET_CERT.certificateChain),
|
||||||
serialNumber: z.string().trim().describe(CERTIFICATES.GET_CERT.serialNumberRes)
|
serialNumber: z.string().trim().describe(CERTIFICATES.GET_CERT.serialNumberRes)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -429,7 +536,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
|||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
projectId: ca.projectId,
|
projectId: ca.projectId,
|
||||||
event: {
|
event: {
|
||||||
type: EventType.DELETE_CERT,
|
type: EventType.GET_CERT_BODY,
|
||||||
metadata: {
|
metadata: {
|
||||||
certId: cert.id,
|
certId: cert.id,
|
||||||
cn: cert.commonName,
|
cn: cert.commonName,
|
||||||
|
|||||||
@@ -401,8 +401,8 @@ export const authLoginServiceFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const shouldCheckMfa = selectedOrg.enforceMfa || user.isMfaEnabled;
|
const shouldCheckMfa = selectedOrg.enforceMfa || user.isMfaEnabled;
|
||||||
const orgMfaMethod = selectedOrg.enforceMfa ? selectedOrg.selectedMfaMethod ?? MfaMethod.EMAIL : undefined;
|
const orgMfaMethod = selectedOrg.enforceMfa ? (selectedOrg.selectedMfaMethod ?? MfaMethod.EMAIL) : undefined;
|
||||||
const userMfaMethod = user.isMfaEnabled ? user.selectedMfaMethod ?? MfaMethod.EMAIL : undefined;
|
const userMfaMethod = user.isMfaEnabled ? (user.selectedMfaMethod ?? MfaMethod.EMAIL) : undefined;
|
||||||
const mfaMethod = orgMfaMethod ?? userMfaMethod;
|
const mfaMethod = orgMfaMethod ?? userMfaMethod;
|
||||||
|
|
||||||
if (shouldCheckMfa && (!decodedToken.isMfaVerified || decodedToken.mfaMethod !== mfaMethod)) {
|
if (shouldCheckMfa && (!decodedToken.isMfaVerified || decodedToken.mfaMethod !== mfaMethod)) {
|
||||||
@@ -573,9 +573,9 @@ export const authLoginServiceFactory = ({
|
|||||||
}: TVerifyMfaTokenDTO) => {
|
}: TVerifyMfaTokenDTO) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
const user = await userDAL.findById(userId);
|
const user = await userDAL.findById(userId);
|
||||||
enforceUserLockStatus(Boolean(user.isLocked), user.temporaryLockDateEnd);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
enforceUserLockStatus(Boolean(user.isLocked), user.temporaryLockDateEnd);
|
||||||
if (mfaMethod === MfaMethod.EMAIL) {
|
if (mfaMethod === MfaMethod.EMAIL) {
|
||||||
await tokenService.validateTokenForUser({
|
await tokenService.validateTokenForUser({
|
||||||
type: TokenType.TOKEN_EMAIL_MFA,
|
type: TokenType.TOKEN_EMAIL_MFA,
|
||||||
|
|||||||
@@ -6,7 +6,11 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import { ActionProjectType, ProjectType, TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas";
|
import { ActionProjectType, ProjectType, TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import {
|
||||||
|
ProjectPermissionActions,
|
||||||
|
ProjectPermissionCertificateActions,
|
||||||
|
ProjectPermissionSub
|
||||||
|
} from "@app/ee/services/permission/project-permission";
|
||||||
import { extractX509CertFromChain } from "@app/lib/certificates/extract-certificate";
|
import { extractX509CertFromChain } from "@app/lib/certificates/extract-certificate";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
@@ -21,6 +25,7 @@ import { TProjectDALFactory } from "@app/services/project/project-dal";
|
|||||||
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
|
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
|
||||||
|
|
||||||
import { TCertificateAuthorityCrlDALFactory } from "../../ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
import { TCertificateAuthorityCrlDALFactory } from "../../ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
||||||
|
import { TCertificateSecretDALFactory } from "../certificate/certificate-secret-dal";
|
||||||
import {
|
import {
|
||||||
CertExtendedKeyUsage,
|
CertExtendedKeyUsage,
|
||||||
CertExtendedKeyUsageOIDToName,
|
CertExtendedKeyUsageOIDToName,
|
||||||
@@ -75,6 +80,7 @@ type TCertificateAuthorityServiceFactoryDep = {
|
|||||||
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "getById" | "find">;
|
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "getById" | "find">;
|
||||||
certificateAuthorityQueue: TCertificateAuthorityQueueFactory; // TODO: Pick
|
certificateAuthorityQueue: TCertificateAuthorityQueueFactory; // TODO: Pick
|
||||||
certificateDAL: Pick<TCertificateDALFactory, "transaction" | "create" | "find">;
|
certificateDAL: Pick<TCertificateDALFactory, "transaction" | "create" | "find">;
|
||||||
|
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "create">;
|
||||||
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
|
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
|
||||||
pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "findById">;
|
pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "findById">;
|
||||||
pkiCollectionItemDAL: Pick<TPkiCollectionItemDALFactory, "create">;
|
pkiCollectionItemDAL: Pick<TPkiCollectionItemDALFactory, "create">;
|
||||||
@@ -96,6 +102,7 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
certificateTemplateDAL,
|
certificateTemplateDAL,
|
||||||
certificateDAL,
|
certificateDAL,
|
||||||
certificateBodyDAL,
|
certificateBodyDAL,
|
||||||
|
certificateSecretDAL,
|
||||||
pkiCollectionDAL,
|
pkiCollectionDAL,
|
||||||
pkiCollectionItemDAL,
|
pkiCollectionItemDAL,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
@@ -1157,7 +1164,10 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
actionProjectType: ActionProjectType.CertificateManager
|
actionProjectType: ActionProjectType.CertificateManager
|
||||||
});
|
});
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates);
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionCertificateActions.Create,
|
||||||
|
ProjectPermissionSub.Certificates
|
||||||
|
);
|
||||||
|
|
||||||
if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" });
|
if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" });
|
||||||
if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" });
|
if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" });
|
||||||
@@ -1373,6 +1383,23 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
|
const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
|
||||||
plainText: Buffer.from(new Uint8Array(leafCert.rawData))
|
plainText: Buffer.from(new Uint8Array(leafCert.rawData))
|
||||||
});
|
});
|
||||||
|
const { cipherTextBlob: encryptedPrivateKey } = await kmsEncryptor({
|
||||||
|
plainText: Buffer.from(skLeaf)
|
||||||
|
});
|
||||||
|
|
||||||
|
const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({
|
||||||
|
caCertId: caCert.id,
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
certificateAuthorityCertDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
const certificateChainPem = `${issuingCaCertificate}\n${caCertChain}`.trim();
|
||||||
|
|
||||||
|
const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({
|
||||||
|
plainText: Buffer.from(certificateChainPem)
|
||||||
|
});
|
||||||
|
|
||||||
await certificateDAL.transaction(async (tx) => {
|
await certificateDAL.transaction(async (tx) => {
|
||||||
const cert = await certificateDAL.create(
|
const cert = await certificateDAL.create(
|
||||||
@@ -1396,7 +1423,16 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
await certificateBodyDAL.create(
|
await certificateBodyDAL.create(
|
||||||
{
|
{
|
||||||
certId: cert.id,
|
certId: cert.id,
|
||||||
encryptedCertificate
|
encryptedCertificate,
|
||||||
|
encryptedCertificateChain
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
await certificateSecretDAL.create(
|
||||||
|
{
|
||||||
|
certId: cert.id,
|
||||||
|
encryptedPrivateKey
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
@@ -1414,17 +1450,9 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
return cert;
|
return cert;
|
||||||
});
|
});
|
||||||
|
|
||||||
const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({
|
|
||||||
caCertId: caCert.id,
|
|
||||||
certificateAuthorityDAL,
|
|
||||||
certificateAuthorityCertDAL,
|
|
||||||
projectDAL,
|
|
||||||
kmsService
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
certificate: leafCert.toString("pem"),
|
certificate: leafCert.toString("pem"),
|
||||||
certificateChain: `${issuingCaCertificate}\n${caCertChain}`.trim(),
|
certificateChain: certificateChainPem,
|
||||||
issuingCaCertificate,
|
issuingCaCertificate,
|
||||||
privateKey: skLeaf,
|
privateKey: skLeaf,
|
||||||
serialNumber,
|
serialNumber,
|
||||||
@@ -1487,7 +1515,7 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionActions.Create,
|
ProjectPermissionCertificateActions.Create,
|
||||||
ProjectPermissionSub.Certificates
|
ProjectPermissionSub.Certificates
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
|
import crypto from "node:crypto";
|
||||||
|
|
||||||
import * as x509 from "@peculiar/x509";
|
import * as x509 from "@peculiar/x509";
|
||||||
|
|
||||||
import { CrlReason } from "./certificate-types";
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
|
||||||
|
import { getProjectKmsCertificateKeyId } from "../project/project-fns";
|
||||||
|
import { CrlReason, TBuildCertificateChainDTO, TGetCertificateCredentialsDTO } from "./certificate-types";
|
||||||
|
|
||||||
export const revocationReasonToCrlCode = (crlReason: CrlReason) => {
|
export const revocationReasonToCrlCode = (crlReason: CrlReason) => {
|
||||||
switch (crlReason) {
|
switch (crlReason) {
|
||||||
@@ -46,3 +51,73 @@ export const constructPemChainFromCerts = (certificates: x509.X509Certificate[])
|
|||||||
.map((cert) => cert.toString("pem"))
|
.map((cert) => cert.toString("pem"))
|
||||||
.join("\n")
|
.join("\n")
|
||||||
.trim();
|
.trim();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the public and private key of certificate
|
||||||
|
* Note: credentials are returned as PEM strings
|
||||||
|
*/
|
||||||
|
export const getCertificateCredentials = async ({
|
||||||
|
certId,
|
||||||
|
projectId,
|
||||||
|
certificateSecretDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
}: TGetCertificateCredentialsDTO) => {
|
||||||
|
const certificateSecret = await certificateSecretDAL.findOne({ certId });
|
||||||
|
if (!certificateSecret)
|
||||||
|
throw new NotFoundError({ message: `Certificate secret for certificate with ID '${certId}' not found` });
|
||||||
|
|
||||||
|
const keyId = await getProjectKmsCertificateKeyId({
|
||||||
|
projectId,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
const kmsDecryptor = await kmsService.decryptWithKmsKey({
|
||||||
|
kmsId: keyId
|
||||||
|
});
|
||||||
|
const decryptedPrivateKey = await kmsDecryptor({
|
||||||
|
cipherTextBlob: certificateSecret.encryptedPrivateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const skObj = crypto.createPrivateKey({ key: decryptedPrivateKey, format: "pem", type: "pkcs8" });
|
||||||
|
const certPrivateKey = skObj.export({ format: "pem", type: "pkcs8" }).toString();
|
||||||
|
|
||||||
|
const pkObj = crypto.createPublicKey(skObj);
|
||||||
|
const certPublicKey = pkObj.export({ format: "pem", type: "spki" }).toString();
|
||||||
|
|
||||||
|
return {
|
||||||
|
certificateSecret,
|
||||||
|
certPrivateKey,
|
||||||
|
certPublicKey
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
throw new BadRequestError({ message: `Failed to process private key for certificate with ID '${certId}'` });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// If the certificate was generated after ~05/01/25 it will have a encryptedCertificateChain attached to it's body
|
||||||
|
// Otherwise we'll fallback to manually building the chain
|
||||||
|
export const buildCertificateChain = async ({
|
||||||
|
caCert,
|
||||||
|
caCertChain,
|
||||||
|
encryptedCertificateChain,
|
||||||
|
kmsService,
|
||||||
|
kmsId
|
||||||
|
}: TBuildCertificateChainDTO) => {
|
||||||
|
if (!encryptedCertificateChain && (!caCert || !caCertChain)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let certificateChain = `${caCert}\n${caCertChain}`.trim();
|
||||||
|
|
||||||
|
if (encryptedCertificateChain) {
|
||||||
|
const kmsDecryptor = await kmsService.decryptWithKmsKey({ kmsId });
|
||||||
|
const decryptedCertChain = await kmsDecryptor({
|
||||||
|
cipherTextBlob: encryptedCertificateChain
|
||||||
|
});
|
||||||
|
certificateChain = decryptedCertChain.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return certificateChain;
|
||||||
|
};
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TCertificateSecretDALFactory = ReturnType<typeof certificateSecretDALFactory>;
|
||||||
|
|
||||||
|
export const certificateSecretDALFactory = (db: TDbClient) => {
|
||||||
|
const certSecretOrm = ormify(db, TableName.CertificateSecret);
|
||||||
|
return certSecretOrm;
|
||||||
|
};
|
||||||
@@ -4,7 +4,10 @@ import * as x509 from "@peculiar/x509";
|
|||||||
import { ActionProjectType } from "@app/db/schemas";
|
import { ActionProjectType } from "@app/db/schemas";
|
||||||
import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import {
|
||||||
|
ProjectPermissionCertificateActions,
|
||||||
|
ProjectPermissionSub
|
||||||
|
} from "@app/ee/services/permission/project-permission";
|
||||||
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
|
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
|
||||||
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||||
import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
|
import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
|
||||||
@@ -15,11 +18,21 @@ import { TProjectDALFactory } from "@app/services/project/project-dal";
|
|||||||
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
|
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
|
||||||
|
|
||||||
import { getCaCertChain, rebuildCaCrl } from "../certificate-authority/certificate-authority-fns";
|
import { getCaCertChain, rebuildCaCrl } from "../certificate-authority/certificate-authority-fns";
|
||||||
import { revocationReasonToCrlCode } from "./certificate-fns";
|
import { buildCertificateChain, getCertificateCredentials, revocationReasonToCrlCode } from "./certificate-fns";
|
||||||
import { CertStatus, TDeleteCertDTO, TGetCertBodyDTO, TGetCertDTO, TRevokeCertDTO } from "./certificate-types";
|
import { TCertificateSecretDALFactory } from "./certificate-secret-dal";
|
||||||
|
import {
|
||||||
|
CertStatus,
|
||||||
|
TDeleteCertDTO,
|
||||||
|
TGetCertBodyDTO,
|
||||||
|
TGetCertBundleDTO,
|
||||||
|
TGetCertDTO,
|
||||||
|
TGetCertPrivateKeyDTO,
|
||||||
|
TRevokeCertDTO
|
||||||
|
} from "./certificate-types";
|
||||||
|
|
||||||
type TCertificateServiceFactoryDep = {
|
type TCertificateServiceFactoryDep = {
|
||||||
certificateDAL: Pick<TCertificateDALFactory, "findOne" | "deleteById" | "update" | "find">;
|
certificateDAL: Pick<TCertificateDALFactory, "findOne" | "deleteById" | "update" | "find">;
|
||||||
|
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne">;
|
||||||
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "findOne">;
|
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "findOne">;
|
||||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
|
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
|
||||||
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findById">;
|
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findById">;
|
||||||
@@ -34,6 +47,7 @@ export type TCertificateServiceFactory = ReturnType<typeof certificateServiceFac
|
|||||||
|
|
||||||
export const certificateServiceFactory = ({
|
export const certificateServiceFactory = ({
|
||||||
certificateDAL,
|
certificateDAL,
|
||||||
|
certificateSecretDAL,
|
||||||
certificateBodyDAL,
|
certificateBodyDAL,
|
||||||
certificateAuthorityDAL,
|
certificateAuthorityDAL,
|
||||||
certificateAuthorityCertDAL,
|
certificateAuthorityCertDAL,
|
||||||
@@ -59,7 +73,10 @@ export const certificateServiceFactory = ({
|
|||||||
actionProjectType: ActionProjectType.CertificateManager
|
actionProjectType: ActionProjectType.CertificateManager
|
||||||
});
|
});
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionCertificateActions.Read,
|
||||||
|
ProjectPermissionSub.Certificates
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
cert,
|
cert,
|
||||||
@@ -67,6 +84,48 @@ export const certificateServiceFactory = ({
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get certificate private key.
|
||||||
|
*/
|
||||||
|
const getCertPrivateKey = async ({
|
||||||
|
serialNumber,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actor,
|
||||||
|
actorOrgId
|
||||||
|
}: TGetCertPrivateKeyDTO) => {
|
||||||
|
const cert = await certificateDAL.findOne({ serialNumber });
|
||||||
|
const ca = await certificateAuthorityDAL.findById(cert.caId);
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId: ca.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actionProjectType: ActionProjectType.CertificateManager
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionCertificateActions.ReadPrivateKey,
|
||||||
|
ProjectPermissionSub.Certificates
|
||||||
|
);
|
||||||
|
|
||||||
|
const { certPrivateKey } = await getCertificateCredentials({
|
||||||
|
certId: cert.id,
|
||||||
|
projectId: ca.projectId,
|
||||||
|
certificateSecretDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
ca,
|
||||||
|
cert,
|
||||||
|
certPrivateKey
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete certificate with serial number [serialNumber]
|
* Delete certificate with serial number [serialNumber]
|
||||||
*/
|
*/
|
||||||
@@ -83,7 +142,10 @@ export const certificateServiceFactory = ({
|
|||||||
actionProjectType: ActionProjectType.CertificateManager
|
actionProjectType: ActionProjectType.CertificateManager
|
||||||
});
|
});
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates);
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionCertificateActions.Delete,
|
||||||
|
ProjectPermissionSub.Certificates
|
||||||
|
);
|
||||||
|
|
||||||
const deletedCert = await certificateDAL.deleteById(cert.id);
|
const deletedCert = await certificateDAL.deleteById(cert.id);
|
||||||
|
|
||||||
@@ -118,7 +180,10 @@ export const certificateServiceFactory = ({
|
|||||||
actionProjectType: ActionProjectType.CertificateManager
|
actionProjectType: ActionProjectType.CertificateManager
|
||||||
});
|
});
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates);
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionCertificateActions.Delete,
|
||||||
|
ProjectPermissionSub.Certificates
|
||||||
|
);
|
||||||
|
|
||||||
if (cert.status === CertStatus.REVOKED) throw new Error("Certificate already revoked");
|
if (cert.status === CertStatus.REVOKED) throw new Error("Certificate already revoked");
|
||||||
|
|
||||||
@@ -165,7 +230,10 @@ export const certificateServiceFactory = ({
|
|||||||
actionProjectType: ActionProjectType.CertificateManager
|
actionProjectType: ActionProjectType.CertificateManager
|
||||||
});
|
});
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionCertificateActions.Read,
|
||||||
|
ProjectPermissionSub.Certificates
|
||||||
|
);
|
||||||
|
|
||||||
const certBody = await certificateBodyDAL.findOne({ certId: cert.id });
|
const certBody = await certificateBodyDAL.findOne({ certId: cert.id });
|
||||||
|
|
||||||
@@ -192,19 +260,107 @@ export const certificateServiceFactory = ({
|
|||||||
kmsService
|
kmsService
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const certificateChain = await buildCertificateChain({
|
||||||
|
caCert,
|
||||||
|
caCertChain,
|
||||||
|
kmsId: certificateManagerKeyId,
|
||||||
|
kmsService,
|
||||||
|
encryptedCertificateChain: certBody.encryptedCertificateChain || undefined
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
certificate: certObj.toString("pem"),
|
certificate: certObj.toString("pem"),
|
||||||
certificateChain: `${caCert}\n${caCertChain}`.trim(),
|
certificateChain,
|
||||||
serialNumber: certObj.serialNumber,
|
serialNumber: certObj.serialNumber,
|
||||||
cert,
|
cert,
|
||||||
ca
|
ca
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return certificate body and certificate chain for certificate with
|
||||||
|
* serial number [serialNumber]
|
||||||
|
*/
|
||||||
|
const getCertBundle = async ({ serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TGetCertBundleDTO) => {
|
||||||
|
const cert = await certificateDAL.findOne({ serialNumber });
|
||||||
|
const ca = await certificateAuthorityDAL.findById(cert.caId);
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId: ca.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actionProjectType: ActionProjectType.CertificateManager
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionCertificateActions.Read,
|
||||||
|
ProjectPermissionSub.Certificates
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionCertificateActions.ReadPrivateKey,
|
||||||
|
ProjectPermissionSub.Certificates
|
||||||
|
);
|
||||||
|
|
||||||
|
const certBody = await certificateBodyDAL.findOne({ certId: cert.id });
|
||||||
|
|
||||||
|
const certificateManagerKeyId = await getProjectKmsCertificateKeyId({
|
||||||
|
projectId: ca.projectId,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
const kmsDecryptor = await kmsService.decryptWithKmsKey({
|
||||||
|
kmsId: certificateManagerKeyId
|
||||||
|
});
|
||||||
|
const decryptedCert = await kmsDecryptor({
|
||||||
|
cipherTextBlob: certBody.encryptedCertificate
|
||||||
|
});
|
||||||
|
|
||||||
|
const certObj = new x509.X509Certificate(decryptedCert);
|
||||||
|
const certificate = certObj.toString("pem");
|
||||||
|
|
||||||
|
const { caCert, caCertChain } = await getCaCertChain({
|
||||||
|
caCertId: cert.caCertId,
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
certificateAuthorityCertDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
const certificateChain = await buildCertificateChain({
|
||||||
|
caCert,
|
||||||
|
caCertChain,
|
||||||
|
kmsId: certificateManagerKeyId,
|
||||||
|
kmsService,
|
||||||
|
encryptedCertificateChain: certBody.encryptedCertificateChain || undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
const { certPrivateKey } = await getCertificateCredentials({
|
||||||
|
certId: cert.id,
|
||||||
|
projectId: ca.projectId,
|
||||||
|
certificateSecretDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
certificate,
|
||||||
|
certificateChain,
|
||||||
|
privateKey: certPrivateKey,
|
||||||
|
serialNumber,
|
||||||
|
cert,
|
||||||
|
ca
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getCert,
|
getCert,
|
||||||
|
getCertPrivateKey,
|
||||||
deleteCert,
|
deleteCert,
|
||||||
revokeCert,
|
revokeCert,
|
||||||
getCertBody
|
getCertBody,
|
||||||
|
getCertBundle
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ import * as x509 from "@peculiar/x509";
|
|||||||
|
|
||||||
import { TProjectPermission } from "@app/lib/types";
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
|
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||||
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
|
import { TCertificateSecretDALFactory } from "./certificate-secret-dal";
|
||||||
|
|
||||||
export enum CertStatus {
|
export enum CertStatus {
|
||||||
ACTIVE = "active",
|
ACTIVE = "active",
|
||||||
REVOKED = "revoked"
|
REVOKED = "revoked"
|
||||||
@@ -73,3 +77,27 @@ export type TRevokeCertDTO = {
|
|||||||
export type TGetCertBodyDTO = {
|
export type TGetCertBodyDTO = {
|
||||||
serialNumber: string;
|
serialNumber: string;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TGetCertPrivateKeyDTO = {
|
||||||
|
serialNumber: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TGetCertBundleDTO = {
|
||||||
|
serialNumber: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TGetCertificateCredentialsDTO = {
|
||||||
|
certId: string;
|
||||||
|
projectId: string;
|
||||||
|
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne">;
|
||||||
|
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TBuildCertificateChainDTO = {
|
||||||
|
caCert?: string;
|
||||||
|
caCertChain?: string;
|
||||||
|
encryptedCertificateChain?: Buffer;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey">;
|
||||||
|
kmsId: string;
|
||||||
|
};
|
||||||
|
|||||||
@@ -698,6 +698,8 @@ export const orgServiceFactory = ({
|
|||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
|
||||||
|
|
||||||
|
const invitingUser = await userDAL.findOne({ id: actorId });
|
||||||
|
|
||||||
const org = await orgDAL.findOrgById(orgId);
|
const org = await orgDAL.findOrgById(orgId);
|
||||||
|
|
||||||
const [inviteeOrgMembership] = await orgDAL.findMembership({
|
const [inviteeOrgMembership] = await orgDAL.findMembership({
|
||||||
@@ -731,8 +733,8 @@ export const orgServiceFactory = ({
|
|||||||
subjectLine: "Infisical organization invitation",
|
subjectLine: "Infisical organization invitation",
|
||||||
recipients: [inviteeOrgMembership.email as string],
|
recipients: [inviteeOrgMembership.email as string],
|
||||||
substitutions: {
|
substitutions: {
|
||||||
inviterFirstName: inviteeOrgMembership.firstName,
|
inviterFirstName: invitingUser.firstName,
|
||||||
inviterUsername: inviteeOrgMembership.email,
|
inviterUsername: invitingUser.email,
|
||||||
organizationName: org?.name,
|
organizationName: org?.name,
|
||||||
email: inviteeOrgMembership.email,
|
email: inviteeOrgMembership.email,
|
||||||
organizationId: org?.id.toString(),
|
organizationId: org?.id.toString(),
|
||||||
@@ -761,6 +763,8 @@ export const orgServiceFactory = ({
|
|||||||
|
|
||||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||||
|
|
||||||
|
const invitingUser = await userDAL.findOne({ id: actorId });
|
||||||
|
|
||||||
const org = await orgDAL.findOrgById(orgId);
|
const org = await orgDAL.findOrgById(orgId);
|
||||||
|
|
||||||
const isEmailInvalid = await isDisposableEmail(inviteeEmails);
|
const isEmailInvalid = await isDisposableEmail(inviteeEmails);
|
||||||
@@ -1179,8 +1183,8 @@ export const orgServiceFactory = ({
|
|||||||
subjectLine: "Infisical organization invitation",
|
subjectLine: "Infisical organization invitation",
|
||||||
recipients: [el.email],
|
recipients: [el.email],
|
||||||
substitutions: {
|
substitutions: {
|
||||||
inviterFirstName: el.firstName,
|
inviterFirstName: invitingUser.firstName,
|
||||||
inviterUsername: el.email,
|
inviterUsername: invitingUser.email,
|
||||||
organizationName: org?.name,
|
organizationName: org?.name,
|
||||||
email: el.email,
|
email: el.email,
|
||||||
organizationId: org?.id.toString(),
|
organizationId: org?.id.toString(),
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { throwIfMissingSecretReadValueOrDescribePermission } from "@app/ee/servi
|
|||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import {
|
import {
|
||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
|
ProjectPermissionCertificateActions,
|
||||||
ProjectPermissionPkiSubscriberActions,
|
ProjectPermissionPkiSubscriberActions,
|
||||||
ProjectPermissionSecretActions,
|
ProjectPermissionSecretActions,
|
||||||
ProjectPermissionSshHostActions,
|
ProjectPermissionSshHostActions,
|
||||||
@@ -953,7 +954,10 @@ export const projectServiceFactory = ({
|
|||||||
actionProjectType: ActionProjectType.CertificateManager
|
actionProjectType: ActionProjectType.CertificateManager
|
||||||
});
|
});
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionCertificateActions.Read,
|
||||||
|
ProjectPermissionSub.Certificates
|
||||||
|
);
|
||||||
|
|
||||||
const cas = await certificateAuthorityDAL.find({ projectId });
|
const cas = await certificateAuthorityDAL.find({ projectId });
|
||||||
|
|
||||||
|
|||||||
@@ -291,7 +291,7 @@ export const parseSyncErrorMessage = (err: unknown): string => {
|
|||||||
} else if (err instanceof AxiosError) {
|
} else if (err instanceof AxiosError) {
|
||||||
errorMessage = err?.response?.data
|
errorMessage = err?.response?.data
|
||||||
? JSON.stringify(err?.response?.data)
|
? JSON.stringify(err?.response?.data)
|
||||||
: err?.message ?? "An unknown error occurred.";
|
: (err?.message ?? "An unknown error occurred.");
|
||||||
} else {
|
} else {
|
||||||
errorMessage = (err as Error)?.message || "An unknown error occurred.";
|
errorMessage = (err as Error)?.message || "An unknown error occurred.";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -834,7 +834,7 @@ export const secretSyncQueueFactory = ({
|
|||||||
secretPath: folder?.path,
|
secretPath: folder?.path,
|
||||||
environment: environment?.name,
|
environment: environment?.name,
|
||||||
projectName: project.name,
|
projectName: project.name,
|
||||||
syncUrl: `${appCfg.SITE_URL}/integrations/secret-syncs/${destination}/${secretSync.id}`
|
syncUrl: `${appCfg.SITE_URL}/secret-manager/${projectId}/integrations/secret-syncs/${destination}/${secretSync.id}`
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -740,7 +740,7 @@ export const secretQueueFactory = ({
|
|||||||
environment: jobPayload.environmentName,
|
environment: jobPayload.environmentName,
|
||||||
count: jobPayload.count,
|
count: jobPayload.count,
|
||||||
projectName: project.name,
|
projectName: project.name,
|
||||||
integrationUrl: `${appCfg.SITE_URL}/integrations/${project.id}`
|
integrationUrl: `${appCfg.SITE_URL}/secret-manager/${project.id}/integrations?selectedTab=native-integrations`
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import { Button, Heading, Link, Section, Text } from "@react-email/components";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
|
||||||
|
|
||||||
|
interface AccessApprovalRequestTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
|
||||||
|
projectName: string;
|
||||||
|
requesterFullName: string;
|
||||||
|
requesterEmail: string;
|
||||||
|
isTemporary: boolean;
|
||||||
|
secretPath: string;
|
||||||
|
environment: string;
|
||||||
|
expiresIn: string;
|
||||||
|
permissions: string[];
|
||||||
|
note?: string;
|
||||||
|
approvalUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AccessApprovalRequestTemplate = ({
|
||||||
|
projectName,
|
||||||
|
siteUrl,
|
||||||
|
requesterFullName,
|
||||||
|
requesterEmail,
|
||||||
|
isTemporary,
|
||||||
|
secretPath,
|
||||||
|
environment,
|
||||||
|
expiresIn,
|
||||||
|
permissions,
|
||||||
|
note,
|
||||||
|
approvalUrl
|
||||||
|
}: AccessApprovalRequestTemplateProps) => {
|
||||||
|
return (
|
||||||
|
<BaseEmailWrapper
|
||||||
|
title="Access Approval Request"
|
||||||
|
preview="A new access approval request is pending your review."
|
||||||
|
siteUrl={siteUrl}
|
||||||
|
>
|
||||||
|
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
|
||||||
|
You have a new access approval request pending review for the project <strong>{projectName}</strong>
|
||||||
|
</Heading>
|
||||||
|
<Section className="px-[24px] 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>{requesterFullName}</strong> (
|
||||||
|
<Link href={`mailto:${requesterEmail}`} className="text-slate-700 no-underline">
|
||||||
|
{requesterEmail}
|
||||||
|
</Link>
|
||||||
|
) has requested {isTemporary ? "temporary" : "permanent"} access to <strong>{secretPath}</strong> in the{" "}
|
||||||
|
<strong>{environment}</strong> environment.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{isTemporary && (
|
||||||
|
<Text className="text-[14px] text-red-500 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>
|
||||||
|
))}
|
||||||
|
{note && (
|
||||||
|
<Text className="text-[14px] text-slate-700 leading-[24px]">
|
||||||
|
<strong className="text-black">User Note:</strong> "{note}"
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Section>
|
||||||
|
<Section className="text-center mt-[28px]">
|
||||||
|
<Button
|
||||||
|
href={approvalUrl}
|
||||||
|
className="rounded-md p-3 px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
|
||||||
|
>
|
||||||
|
Review Request
|
||||||
|
</Button>
|
||||||
|
</Section>
|
||||||
|
</BaseEmailWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AccessApprovalRequestTemplate;
|
||||||
|
|
||||||
|
AccessApprovalRequestTemplate.PreviewProps = {
|
||||||
|
requesterFullName: "Abigail Williams",
|
||||||
|
requesterEmail: "[email protected]",
|
||||||
|
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"],
|
||||||
|
note: "I need access to these permissions for the new initiative for HR."
|
||||||
|
} as AccessApprovalRequestTemplateProps;
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { Body, Container, Head, Hr, Html, Img, Link, Preview, Section, Tailwind, Text } from "@react-email/components";
|
||||||
|
import React, { ReactNode } from "react";
|
||||||
|
|
||||||
|
export interface BaseEmailWrapperProps {
|
||||||
|
title: string;
|
||||||
|
preview: string;
|
||||||
|
siteUrl: string;
|
||||||
|
children?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BaseEmailWrapper = ({ title, preview, children, siteUrl }: BaseEmailWrapperProps) => {
|
||||||
|
return (
|
||||||
|
<Html>
|
||||||
|
<Head title={title} />
|
||||||
|
<Tailwind>
|
||||||
|
<Body className="bg-gray-300 my-auto mx-auto font-sans px-[8px]">
|
||||||
|
<Preview>{preview}</Preview>
|
||||||
|
<Container className="bg-white rounded-xl my-[40px] mx-auto pb-[0px] max-w-[500px]">
|
||||||
|
<Section className="border-0 border-b border-[#d1e309] border-solid bg-[#EBF852] mb-[44px] h-[10px] rounded-t-xl" />
|
||||||
|
<Section className="px-[32px] mb-[18px]">
|
||||||
|
<Section className="w-[48px] h-[48px] border border-solid border-gray-300 rounded-full bg-gray-100 mx-auto">
|
||||||
|
<Img
|
||||||
|
src={`https://infisical.com/_next/image?url=%2Fimages%2Flogo-black.png&w=64&q=75`}
|
||||||
|
width="32"
|
||||||
|
alt="Infisical Logo"
|
||||||
|
className="mx-auto"
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
|
</Section>
|
||||||
|
<Section className="px-[28px]">{children}</Section>
|
||||||
|
<Hr className=" mt-[32px] mb-[0px] h-[1px]" />
|
||||||
|
<Section className="px-[24px] text-center">
|
||||||
|
<Text className="text-gray-500 text-[12px]">
|
||||||
|
Email sent via{" "}
|
||||||
|
<Link href={siteUrl} className="text-slate-700 no-underline">
|
||||||
|
Infisical
|
||||||
|
</Link>
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
</Container>
|
||||||
|
</Body>
|
||||||
|
</Tailwind>
|
||||||
|
</Html>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { Heading, Link, Section, Text } from "@react-email/components";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
|
||||||
|
|
||||||
|
interface EmailMfaTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
|
||||||
|
code: string;
|
||||||
|
isCloud: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EmailMfaTemplate = ({ code, siteUrl, isCloud }: EmailMfaTemplateProps) => {
|
||||||
|
return (
|
||||||
|
<BaseEmailWrapper title="MFA Code" preview="Sign-in attempt requires further verification." siteUrl={siteUrl}>
|
||||||
|
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
|
||||||
|
<strong>MFA required</strong>
|
||||||
|
</Heading>
|
||||||
|
<Section className="px-[24px] mt-[36px] pt-[8px] text-center pb-[8px] text-[14px] border border-solid border-gray-200 rounded-md bg-gray-50">
|
||||||
|
<Text>Enter the MFA code shown below in the browser where you started sign-in.</Text>
|
||||||
|
<Text className="text-[24px] mt-[16px]">
|
||||||
|
<strong>{code}</strong>
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
<Section className="mt-[24px] bg-gray-50 pt-[2px] pb-[16px] border border-solid border-gray-200 px-[24px] rounded-md text-gray-800">
|
||||||
|
<Text className="mb-[0px]">
|
||||||
|
<strong>Not you?</strong>{" "}
|
||||||
|
{isCloud ? (
|
||||||
|
<>
|
||||||
|
Contact us at{" "}
|
||||||
|
<Link href="mailto:[email protected]" className="text-slate-700 no-underline">
|
||||||
|
support@infisical.com
|
||||||
|
</Link>{" "}
|
||||||
|
immediately
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"Contact your administrator immediately"
|
||||||
|
)}
|
||||||
|
.
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
</BaseEmailWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EmailMfaTemplate;
|
||||||
|
|
||||||
|
EmailMfaTemplate.PreviewProps = {
|
||||||
|
code: "124356",
|
||||||
|
isCloud: true,
|
||||||
|
siteUrl: "https://infisical.com"
|
||||||
|
} as EmailMfaTemplateProps;
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { Heading, Link, Section, Text } from "@react-email/components";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
|
||||||
|
|
||||||
|
interface EmailVerificationTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
|
||||||
|
code: string;
|
||||||
|
isCloud: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EmailVerificationTemplate = ({ code, siteUrl, isCloud }: EmailVerificationTemplateProps) => {
|
||||||
|
return (
|
||||||
|
<BaseEmailWrapper
|
||||||
|
title="Confirm Your Email Address"
|
||||||
|
preview="Verify your email address to continue with Infisical."
|
||||||
|
siteUrl={siteUrl}
|
||||||
|
>
|
||||||
|
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
|
||||||
|
<strong>Confirm your email address</strong>
|
||||||
|
</Heading>
|
||||||
|
<Section className="px-[24px] mt-[36px] pt-[8px] text-center pb-[8px] text-[14px] border border-solid border-gray-200 rounded-md bg-gray-50">
|
||||||
|
<Text>Enter the confirmation code shown below in the browser window requiring confirmation.</Text>
|
||||||
|
<Text className="text-[24px] mt-[16px]">
|
||||||
|
<strong>{code}</strong>
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
<Section className="mt-[24px] bg-gray-50 pt-[2px] pb-[16px] border border-solid border-gray-200 px-[24px] rounded-md text-gray-800">
|
||||||
|
<Text className="mb-[0px]">
|
||||||
|
<strong>Questions about Infisical?</strong>{" "}
|
||||||
|
{isCloud ? (
|
||||||
|
<>
|
||||||
|
Email us at{" "}
|
||||||
|
<Link href="mailto:[email protected]" className="text-slate-700 no-underline">
|
||||||
|
support@infisical.com
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"Contact your administrator"
|
||||||
|
)}
|
||||||
|
.
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
</BaseEmailWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EmailVerificationTemplate;
|
||||||
|
|
||||||
|
EmailVerificationTemplate.PreviewProps = {
|
||||||
|
code: "124356",
|
||||||
|
isCloud: true,
|
||||||
|
siteUrl: "https://infisical.com"
|
||||||
|
} as EmailVerificationTemplateProps;
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { Heading, Link, Section, Text } from "@react-email/components";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
|
||||||
|
|
||||||
|
interface ExternalImportFailedTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
|
||||||
|
error: string;
|
||||||
|
provider: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ExternalImportFailedTemplate = ({ error, siteUrl, provider }: ExternalImportFailedTemplateProps) => {
|
||||||
|
return (
|
||||||
|
<BaseEmailWrapper title="Import Failed" preview={`An import from ${provider} has failed.`} siteUrl={siteUrl}>
|
||||||
|
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
|
||||||
|
An import from <strong>{provider}</strong> to Infisical has failed
|
||||||
|
</Heading>
|
||||||
|
<Section className="px-[24px] 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]">
|
||||||
|
An import from <strong>{provider}</strong> to Infisical has failed due to unforeseen circumstances. Please
|
||||||
|
re-try your import.
|
||||||
|
</Text>
|
||||||
|
<Text className="text-black text-[14px] leading-[24px]">
|
||||||
|
If your issue persists, you can contact the Infisical team at{" "}
|
||||||
|
<Link href="mailto:[email protected]" className="text-slate-700 no-underline">
|
||||||
|
support@infisical.com
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</Text>
|
||||||
|
<Text className="text-[14px] text-red-500 leading-[24px]">
|
||||||
|
<strong>Error:</strong> "{error}"
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
</BaseEmailWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExternalImportFailedTemplate;
|
||||||
|
|
||||||
|
ExternalImportFailedTemplate.PreviewProps = {
|
||||||
|
provider: "EnvKey",
|
||||||
|
error: "Something went wrong. Please try again.",
|
||||||
|
siteUrl: "https://infisical.com"
|
||||||
|
} as ExternalImportFailedTemplateProps;
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { Heading, Section, Text } from "@react-email/components";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
|
||||||
|
|
||||||
|
interface ExternalImportStartedTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
|
||||||
|
provider: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ExternalImportStartedTemplate = ({ siteUrl, provider }: ExternalImportStartedTemplateProps) => {
|
||||||
|
return (
|
||||||
|
<BaseEmailWrapper title="Import in Progress" preview={`An import from ${provider} has started.`} siteUrl={siteUrl}>
|
||||||
|
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
|
||||||
|
An import from <strong>{provider}</strong> to Infisical has been started
|
||||||
|
</Heading>
|
||||||
|
<Section className="px-[24px] 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]">
|
||||||
|
An import from <strong>{provider}</strong> to Infisical is in progress. The import process may take up to 30
|
||||||
|
minutes. You will receive an email once the import has completed.
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
</BaseEmailWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExternalImportStartedTemplate;
|
||||||
|
|
||||||
|
ExternalImportStartedTemplate.PreviewProps = {
|
||||||
|
provider: "EnvKey",
|
||||||
|
siteUrl: "https://infisical.com"
|
||||||
|
} as ExternalImportStartedTemplateProps;
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { Heading, Section, Text } from "@react-email/components";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
|
||||||
|
|
||||||
|
interface ExternalImportSucceededTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
|
||||||
|
provider: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ExternalImportSucceededTemplate = ({ siteUrl, provider }: ExternalImportSucceededTemplateProps) => {
|
||||||
|
return (
|
||||||
|
<BaseEmailWrapper title="Import Complete" preview={`An import from ${provider} has completed.`} siteUrl={siteUrl}>
|
||||||
|
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
|
||||||
|
An import from <strong>{provider}</strong> to Infisical has completed
|
||||||
|
</Heading>
|
||||||
|
<Section className="px-[24px] 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]">
|
||||||
|
An import from <strong>{provider}</strong> to Infisical was successful. Your data is now available in
|
||||||
|
Infisical.
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
</BaseEmailWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExternalImportSucceededTemplate;
|
||||||
|
|
||||||
|
ExternalImportSucceededTemplate.PreviewProps = {
|
||||||
|
provider: "EnvKey",
|
||||||
|
siteUrl: "https://infisical.com"
|
||||||
|
} as ExternalImportSucceededTemplateProps;
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { Button, Heading, Section, Text } from "@react-email/components";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
|
||||||
|
|
||||||
|
interface IntegrationSyncFailedTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
|
||||||
|
count: number;
|
||||||
|
projectName: string;
|
||||||
|
secretPath: string;
|
||||||
|
environment: string;
|
||||||
|
syncMessage: string;
|
||||||
|
integrationUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const IntegrationSyncFailedTemplate = ({
|
||||||
|
count,
|
||||||
|
siteUrl,
|
||||||
|
projectName,
|
||||||
|
secretPath,
|
||||||
|
environment,
|
||||||
|
syncMessage,
|
||||||
|
integrationUrl
|
||||||
|
}: IntegrationSyncFailedTemplateProps) => {
|
||||||
|
return (
|
||||||
|
<BaseEmailWrapper
|
||||||
|
title="Integration Sync Failed"
|
||||||
|
preview="An integration sync error has occurred."
|
||||||
|
siteUrl={siteUrl}
|
||||||
|
>
|
||||||
|
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
|
||||||
|
<strong>{count}</strong> integration(s) failed to sync
|
||||||
|
</Heading>
|
||||||
|
<Section className="px-[24px] mt-[36px] pt-[26px] pb-[4px] text-[14px] border border-solid border-gray-200 rounded-md bg-gray-50">
|
||||||
|
<strong>Project</strong>
|
||||||
|
<Text className="text-[14px] mt-[4px]">{projectName}</Text>
|
||||||
|
<strong>Environment</strong>
|
||||||
|
<Text className="text-[14px] mt-[4px]">{environment}</Text>
|
||||||
|
<strong>Secret Path</strong>
|
||||||
|
<Text className="text-[14px] mt-[4px]">{secretPath}</Text>
|
||||||
|
<strong className="text-black">Failure Reason:</strong>
|
||||||
|
<Text className="text-[14px] mt-[4px] text-red-500 leading-[24px]">"{syncMessage}"</Text>
|
||||||
|
</Section>
|
||||||
|
<Section className="text-center mt-[28px]">
|
||||||
|
<Button
|
||||||
|
href={integrationUrl}
|
||||||
|
className="rounded-md p-3 px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
|
||||||
|
>
|
||||||
|
View Integrations
|
||||||
|
</Button>
|
||||||
|
</Section>
|
||||||
|
</BaseEmailWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IntegrationSyncFailedTemplate;
|
||||||
|
|
||||||
|
IntegrationSyncFailedTemplate.PreviewProps = {
|
||||||
|
projectName: "Example Project",
|
||||||
|
secretPath: "/api/secrets",
|
||||||
|
environment: "Production",
|
||||||
|
siteUrl: "https://infisical.com",
|
||||||
|
integrationUrl: "https://infisical.com",
|
||||||
|
count: 2,
|
||||||
|
syncMessage: "Secret key cannot contain a colon (:)"
|
||||||
|
} as IntegrationSyncFailedTemplateProps;
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { Heading, Link, Section, Text } from "@react-email/components";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
|
||||||
|
|
||||||
|
interface NewDeviceLoginTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
|
||||||
|
email: string;
|
||||||
|
timestamp: string;
|
||||||
|
ip: string;
|
||||||
|
userAgent: string;
|
||||||
|
isCloud: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NewDeviceLoginTemplate = ({
|
||||||
|
email,
|
||||||
|
timestamp,
|
||||||
|
ip,
|
||||||
|
userAgent,
|
||||||
|
siteUrl,
|
||||||
|
isCloud
|
||||||
|
}: NewDeviceLoginTemplateProps) => {
|
||||||
|
return (
|
||||||
|
<BaseEmailWrapper
|
||||||
|
title="Successful Login from New Device"
|
||||||
|
preview="New device login from Infisical."
|
||||||
|
siteUrl={siteUrl}
|
||||||
|
>
|
||||||
|
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
|
||||||
|
We're verifying a recent login for
|
||||||
|
<br />
|
||||||
|
<strong>{email}</strong>
|
||||||
|
</Heading>
|
||||||
|
<Section className="px-[24px] mt-[36px] pt-[26px] pb-[4px] text-[14px] border border-solid border-gray-200 rounded-md bg-gray-50">
|
||||||
|
<strong>Timestamp</strong>
|
||||||
|
<Text className="text-[14px] mt-[4px]">{timestamp}</Text>
|
||||||
|
<strong>IP Address</strong>
|
||||||
|
<Text className="text-[14px] mt-[4px]">{ip}</Text>
|
||||||
|
<strong>User Agent</strong>
|
||||||
|
<Text className="text-[14px] mt-[4px]">{userAgent}</Text>
|
||||||
|
</Section>
|
||||||
|
<Section className="mt-[24px] bg-gray-50 px-[24px] pt-[2px] pb-[16px] border border-solid border-gray-200 rounded-md text-gray-800">
|
||||||
|
<Text className="mb-[0px]">
|
||||||
|
If you believe that this login is suspicious, please contact{" "}
|
||||||
|
{isCloud ? (
|
||||||
|
<Link href="mailto:[email protected]" className="text-slate-700 no-underline">
|
||||||
|
support@infisical.com
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
"your administrator"
|
||||||
|
)}{" "}
|
||||||
|
or reset your password immediately.
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
</BaseEmailWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NewDeviceLoginTemplate;
|
||||||
|
|
||||||
|
NewDeviceLoginTemplate.PreviewProps = {
|
||||||
|
email: "[email protected]",
|
||||||
|
ip: "127.0.0.1",
|
||||||
|
userAgent:
|
||||||
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.3.1 Safari/605.1.15",
|
||||||
|
timestamp: "Tue Apr 29 2025 23:03:27 GMT+0000 (Coordinated Universal Time)",
|
||||||
|
isCloud: true,
|
||||||
|
siteUrl: "https://infisical.com"
|
||||||
|
} as NewDeviceLoginTemplateProps;
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { Heading, Link, Section, Text } from "@react-email/components";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
|
||||||
|
|
||||||
|
interface OrgAdminBreakglassAccessTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
|
||||||
|
email: string;
|
||||||
|
timestamp: string;
|
||||||
|
ip: string;
|
||||||
|
userAgent: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OrgAdminBreakglassAccessTemplate = ({
|
||||||
|
email,
|
||||||
|
siteUrl,
|
||||||
|
timestamp,
|
||||||
|
ip,
|
||||||
|
userAgent
|
||||||
|
}: OrgAdminBreakglassAccessTemplateProps) => {
|
||||||
|
return (
|
||||||
|
<BaseEmailWrapper
|
||||||
|
title="Organization Admin has Bypassed SSO"
|
||||||
|
preview="An organization admin has bypassed SSO."
|
||||||
|
siteUrl={siteUrl}
|
||||||
|
>
|
||||||
|
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
|
||||||
|
The organization admin <strong>{email}</strong> has bypassed enforced SSO login
|
||||||
|
</Heading>
|
||||||
|
<Section className="px-[24px] mt-[36px] pt-[24px] pb-[8px] border border-solid border-gray-200 rounded-md bg-gray-50">
|
||||||
|
<strong className="text-[14px]">Timestamp</strong>
|
||||||
|
<Text className="text-[14px] mt-[4px]">{timestamp}</Text>
|
||||||
|
<strong className="text-[14px]">IP Address</strong>
|
||||||
|
<Text className="text-[14px] mt-[4px]">{ip}</Text>
|
||||||
|
<strong className="text-[14px]">User Agent</strong>
|
||||||
|
<Text className="text-[14px] mt-[4px]">{userAgent}</Text>
|
||||||
|
<Text className="text-[14px]">
|
||||||
|
If you'd like to disable Admin SSO Bypass, please visit{" "}
|
||||||
|
<Link href={`${siteUrl}/organization/settings`} className="text-slate-700 no-underline">
|
||||||
|
Organization Security Settings
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
</BaseEmailWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OrgAdminBreakglassAccessTemplate;
|
||||||
|
|
||||||
|
OrgAdminBreakglassAccessTemplate.PreviewProps = {
|
||||||
|
ip: "127.0.0.1",
|
||||||
|
userAgent:
|
||||||
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.3.1 Safari/605.1.15",
|
||||||
|
timestamp: "Tue Apr 29 2025 23:03:27 GMT+0000 (Coordinated Universal Time)",
|
||||||
|
siteUrl: "https://infisical.com",
|
||||||
|
email: "[email protected]"
|
||||||
|
} as OrgAdminBreakglassAccessTemplateProps;
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { Heading, Section, Text } from "@react-email/components";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
|
||||||
|
|
||||||
|
interface OrgAdminProjectGrantAccessTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview"> {
|
||||||
|
email: string;
|
||||||
|
projectName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OrgAdminProjectGrantAccessTemplate = ({
|
||||||
|
email,
|
||||||
|
siteUrl,
|
||||||
|
projectName
|
||||||
|
}: OrgAdminProjectGrantAccessTemplateProps) => {
|
||||||
|
return (
|
||||||
|
<BaseEmailWrapper
|
||||||
|
title="Project Access Granted to Organization Admin"
|
||||||
|
preview="An organization admin has self-issued direct access to a project in Infisical."
|
||||||
|
siteUrl={siteUrl}
|
||||||
|
>
|
||||||
|
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
|
||||||
|
An organization admin has joined the project <strong>{projectName}</strong>
|
||||||
|
</Heading>
|
||||||
|
<Section className="px-[24px] mt-[36px] pt-[24px] pb-[8px] border border-solid border-gray-200 rounded-md bg-gray-50">
|
||||||
|
<Text className="text-[14px] mt-[4px]">
|
||||||
|
The organization admin <strong>{email}</strong> has self-issued direct access to the project{" "}
|
||||||
|
<strong>{projectName}</strong>.
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
</BaseEmailWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OrgAdminProjectGrantAccessTemplate;
|
||||||
|
|
||||||
|
OrgAdminProjectGrantAccessTemplate.PreviewProps = {
|
||||||
|
email: "[email protected]",
|
||||||
|
projectName: "Example Project",
|
||||||
|
siteUrl: "https://infisical.com"
|
||||||
|
} as OrgAdminProjectGrantAccessTemplateProps;
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import { Button, Heading, Link, Section, Text } from "@react-email/components";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
|
||||||
|
|
||||||
|
interface OrganizationInvitationTemplateProps extends Omit<BaseEmailWrapperProps, "preview" | "title"> {
|
||||||
|
metadata?: string;
|
||||||
|
inviterFirstName: string;
|
||||||
|
inviterUsername: string;
|
||||||
|
organizationName: string;
|
||||||
|
email: string;
|
||||||
|
organizationId: string;
|
||||||
|
token: string;
|
||||||
|
callback_url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OrganizationInvitationTemplate = ({
|
||||||
|
organizationName,
|
||||||
|
inviterFirstName,
|
||||||
|
inviterUsername,
|
||||||
|
token,
|
||||||
|
callback_url,
|
||||||
|
metadata,
|
||||||
|
email,
|
||||||
|
organizationId,
|
||||||
|
siteUrl
|
||||||
|
}: OrganizationInvitationTemplateProps) => {
|
||||||
|
return (
|
||||||
|
<BaseEmailWrapper
|
||||||
|
title="Organization Invitation"
|
||||||
|
preview="You've been invited to join an organization on Infisical."
|
||||||
|
siteUrl={siteUrl}
|
||||||
|
>
|
||||||
|
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
|
||||||
|
You've been invited to join
|
||||||
|
<br />
|
||||||
|
<strong>{organizationName}</strong> on <strong>Infisical</strong>
|
||||||
|
</Heading>
|
||||||
|
<Section className="px-[24px] mt-[36px] pt-[12px] pb-[8px] border text-center border-solid border-gray-200 rounded-md bg-gray-50">
|
||||||
|
<Text className="text-black text-[14px] leading-[24px]">
|
||||||
|
<strong>{inviterFirstName}</strong> (
|
||||||
|
<Link href={`mailto:${inviterUsername}`} className="text-slate-700 no-underline">
|
||||||
|
{inviterUsername}
|
||||||
|
</Link>
|
||||||
|
) has invited you to collaborate on <strong>{organizationName}</strong>.
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
<Section className="text-center mt-[28px]">
|
||||||
|
<Button
|
||||||
|
href={`${callback_url}?token=${token}${metadata ? `&metadata=${metadata}` : ""}&to=${encodeURIComponent(email)}&organization_id=${organizationId}`}
|
||||||
|
className="rounded-md p-3 px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
|
||||||
|
>
|
||||||
|
Accept Invite
|
||||||
|
</Button>
|
||||||
|
</Section>
|
||||||
|
<Section className="mt-[24px] bg-gray-50 pt-[2px] pb-[16px] border border-solid border-gray-200 px-[24px] rounded-md text-gray-800">
|
||||||
|
<Text className="mb-[0px]">
|
||||||
|
<strong>About Infisical:</strong> Infisical is an all-in-one platform to securely manage application secrets,
|
||||||
|
certificates, SSH keys, and configurations across your team and infrastructure.
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
</BaseEmailWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OrganizationInvitationTemplate;
|
||||||
|
|
||||||
|
OrganizationInvitationTemplate.PreviewProps = {
|
||||||
|
organizationName: "Example Organization",
|
||||||
|
inviterFirstName: "Jane",
|
||||||
|
inviterUsername: "[email protected]",
|
||||||
|
email: "[email protected]",
|
||||||
|
siteUrl: "https://infisical.com",
|
||||||
|
callback_url: "https://app.infisical.com",
|
||||||
|
token: "preview-token",
|
||||||
|
organizationId: "1ae1c2c7-8068-461c-b15e-421737868a6a"
|
||||||
|
} as OrganizationInvitationTemplateProps;
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { Button, Heading, Link, Section, Text } from "@react-email/components";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
|
||||||
|
|
||||||
|
interface PasswordResetTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
|
||||||
|
email: string;
|
||||||
|
callback_url: string;
|
||||||
|
token: string;
|
||||||
|
isCloud: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PasswordResetTemplate = ({ email, isCloud, siteUrl, callback_url, token }: PasswordResetTemplateProps) => {
|
||||||
|
return (
|
||||||
|
<BaseEmailWrapper
|
||||||
|
title="Account Recovery"
|
||||||
|
preview="A password reset was requested for your Infisical account."
|
||||||
|
siteUrl={siteUrl}
|
||||||
|
>
|
||||||
|
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
|
||||||
|
<strong>Account Recovery</strong>
|
||||||
|
</Heading>
|
||||||
|
<Section className="px-[24px] mt-[36px] pt-[12px] pb-[8px] border border-solid border-gray-200 rounded-md bg-gray-50">
|
||||||
|
<Text className="text-[14px]">A password reset was requested for your Infisical account.</Text>
|
||||||
|
<Text className="text-[14px]">
|
||||||
|
If you did not initiate this request, please contact{" "}
|
||||||
|
{isCloud ? (
|
||||||
|
<>
|
||||||
|
us immediately at{" "}
|
||||||
|
<Link href="mailto:[email protected]" className="text-slate-700 no-underline">
|
||||||
|
support@infisical.com
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"your administrator immediately"
|
||||||
|
)}
|
||||||
|
.
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
<Section className="text-center mt-[28px]">
|
||||||
|
<Button
|
||||||
|
href={`${callback_url}?token=${token}&to=${encodeURIComponent(email)}`}
|
||||||
|
className="rounded-md p-3 px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
|
||||||
|
>
|
||||||
|
Reset Password
|
||||||
|
</Button>
|
||||||
|
</Section>
|
||||||
|
</BaseEmailWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PasswordResetTemplate;
|
||||||
|
|
||||||
|
PasswordResetTemplate.PreviewProps = {
|
||||||
|
email: "[email protected]",
|
||||||
|
callback_url: "https://app.infisical.com",
|
||||||
|
isCloud: true,
|
||||||
|
token: "preview-token",
|
||||||
|
siteUrl: "https://infisical.com"
|
||||||
|
} as PasswordResetTemplateProps;
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { Button, Heading, Link, Section, Text } from "@react-email/components";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
|
||||||
|
|
||||||
|
interface PasswordSetupTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
|
||||||
|
email: string;
|
||||||
|
callback_url: string;
|
||||||
|
token: string;
|
||||||
|
isCloud: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PasswordSetupTemplate = ({ email, isCloud, siteUrl, callback_url, token }: PasswordSetupTemplateProps) => {
|
||||||
|
return (
|
||||||
|
<BaseEmailWrapper title="Password Setup" preview="Setup your password for Infisical." siteUrl={siteUrl}>
|
||||||
|
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
|
||||||
|
<strong>Password Setup</strong>
|
||||||
|
</Heading>
|
||||||
|
<Section className="px-[24px] mt-[36px] pt-[12px] pb-[8px] border border-solid border-gray-200 rounded-md bg-gray-50">
|
||||||
|
<Text className="text-[14px]">Someone requested to set up a password for your Infisical account.</Text>
|
||||||
|
<Text className="text-[14px] text-red-500">
|
||||||
|
Make sure you are already logged in to Infisical in the current browser before clicking the link below.
|
||||||
|
</Text>
|
||||||
|
<Text className="text-[14px]">
|
||||||
|
If you did not initiate this request, please contact{" "}
|
||||||
|
{isCloud ? (
|
||||||
|
<>
|
||||||
|
us immediately at{" "}
|
||||||
|
<Link href="mailto:[email protected]" className="text-slate-700 no-underline">
|
||||||
|
support@infisical.com
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"your administrator immediately"
|
||||||
|
)}
|
||||||
|
.
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
<Section className="text-center mt-[28px]">
|
||||||
|
<Button
|
||||||
|
href={`${callback_url}?token=${token}&to=${encodeURIComponent(email)}`}
|
||||||
|
className="rounded-md p-3 px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
|
||||||
|
>
|
||||||
|
Set Up Password
|
||||||
|
</Button>
|
||||||
|
</Section>
|
||||||
|
</BaseEmailWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PasswordSetupTemplate;
|
||||||
|
|
||||||
|
PasswordSetupTemplate.PreviewProps = {
|
||||||
|
email: "[email protected]",
|
||||||
|
callback_url: "https://app.infisical.com",
|
||||||
|
isCloud: true,
|
||||||
|
siteUrl: "https://infisical.com",
|
||||||
|
token: "preview-token"
|
||||||
|
} as PasswordSetupTemplateProps;
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { Heading, Hr, Section, Text } from "@react-email/components";
|
||||||
|
import React, { Fragment } from "react";
|
||||||
|
|
||||||
|
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
|
||||||
|
|
||||||
|
interface PkiExpirationAlertTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
|
||||||
|
alertName: string;
|
||||||
|
alertBeforeDays: number;
|
||||||
|
items: { type: string; friendlyName: string; serialNumber: string; expiryDate: string }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PkiExpirationAlertTemplate = ({
|
||||||
|
alertName,
|
||||||
|
siteUrl,
|
||||||
|
alertBeforeDays,
|
||||||
|
items
|
||||||
|
}: PkiExpirationAlertTemplateProps) => {
|
||||||
|
return (
|
||||||
|
<BaseEmailWrapper
|
||||||
|
title="Infisical CA/Certificate Expiration Notice"
|
||||||
|
preview="One or more of your Infisical certificates is about to expire."
|
||||||
|
siteUrl={siteUrl}
|
||||||
|
>
|
||||||
|
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
|
||||||
|
<strong>CA/Certificate Expiration Notice</strong>
|
||||||
|
</Heading>
|
||||||
|
<Section className="px-[24px] mt-[36px] pt-[12px] pb-[8px] border border-solid border-gray-200 rounded-md bg-gray-50">
|
||||||
|
<Text>Hello,</Text>
|
||||||
|
<Text className="text-black text-[14px] leading-[24px]">
|
||||||
|
This is an automated alert for <strong>{alertName}</strong> triggered for CAs/Certificates expiring in{" "}
|
||||||
|
<strong>{alertBeforeDays}</strong> days.
|
||||||
|
</Text>
|
||||||
|
<Text className="text-[14px] leading-[24px] mb-[4px]">
|
||||||
|
<strong>Expiring Items:</strong>
|
||||||
|
</Text>
|
||||||
|
{items.map((item) => (
|
||||||
|
<Fragment key={item.serialNumber}>
|
||||||
|
<Hr className="mb-[16px]" />
|
||||||
|
<strong className="text-[14px]">{item.type}:</strong>
|
||||||
|
<Text className="text-[14px] my-[2px] leading-[24px]">{item.friendlyName}</Text>
|
||||||
|
<strong className="text-[14px]">Serial Number:</strong>
|
||||||
|
<Text className="text-[14px] my-[2px] leading-[24px]">{item.serialNumber}</Text>
|
||||||
|
<strong className="text-[14px]">Expires On:</strong>
|
||||||
|
<Text className="text-[14px] mt-[2px] mb-[16px] leading-[24px]">{item.expiryDate}</Text>
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
<Hr />
|
||||||
|
<Text className="text-[14px] leading-[24px]">
|
||||||
|
Please take the necessary actions to renew these items before they expire.
|
||||||
|
</Text>
|
||||||
|
<Text className="text-[14px] leading-[24px]">
|
||||||
|
For more details, please log in to your Infisical account and check your PKI management section.
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
</BaseEmailWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PkiExpirationAlertTemplate;
|
||||||
|
|
||||||
|
PkiExpirationAlertTemplate.PreviewProps = {
|
||||||
|
alertBeforeDays: 5,
|
||||||
|
items: [
|
||||||
|
{ type: "CA", friendlyName: "Example CA", serialNumber: "1234567890", expiryDate: "2032-01-01" },
|
||||||
|
{ type: "Certificate", friendlyName: "Example Certificate", serialNumber: "2345678901", expiryDate: "2032-01-01" }
|
||||||
|
],
|
||||||
|
alertName: "My PKI Alert",
|
||||||
|
siteUrl: "https://infisical.com"
|
||||||
|
} as PkiExpirationAlertTemplateProps;
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { Button, Heading, Link, Section, Text } from "@react-email/components";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
|
||||||
|
|
||||||
|
interface ProjectAccessRequestTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
|
||||||
|
projectName: string;
|
||||||
|
requesterName: string;
|
||||||
|
requesterEmail: string;
|
||||||
|
orgName: string;
|
||||||
|
note: string;
|
||||||
|
callback_url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProjectAccessRequestTemplate = ({
|
||||||
|
projectName,
|
||||||
|
siteUrl,
|
||||||
|
requesterName,
|
||||||
|
requesterEmail,
|
||||||
|
orgName,
|
||||||
|
note,
|
||||||
|
callback_url
|
||||||
|
}: ProjectAccessRequestTemplateProps) => {
|
||||||
|
return (
|
||||||
|
<BaseEmailWrapper
|
||||||
|
title="Project Access Request"
|
||||||
|
preview="A user has requested access to an Infisical project."
|
||||||
|
siteUrl={siteUrl}
|
||||||
|
>
|
||||||
|
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
|
||||||
|
A user has requested access to the project <strong>{projectName}</strong>
|
||||||
|
</Heading>
|
||||||
|
<Section className="px-[24px] 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>{requesterName}</strong> (
|
||||||
|
<Link href={`mailto:${requesterEmail}`} className="text-slate-700 no-underline">
|
||||||
|
{requesterEmail}
|
||||||
|
</Link>
|
||||||
|
) has requested access to the project <strong>{projectName}</strong> in the organization{" "}
|
||||||
|
<strong>{orgName}</strong>.
|
||||||
|
</Text>
|
||||||
|
<Text className="text-[14px] text-slate-700 leading-[24px]">
|
||||||
|
<strong className="text-black">User note:</strong> "{note}"
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
<Section className="text-center mt-[28px]">
|
||||||
|
<Button
|
||||||
|
href={callback_url}
|
||||||
|
className="rounded-md p-3 px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
|
||||||
|
>
|
||||||
|
Grant Access
|
||||||
|
</Button>
|
||||||
|
</Section>
|
||||||
|
</BaseEmailWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProjectAccessRequestTemplate;
|
||||||
|
|
||||||
|
ProjectAccessRequestTemplate.PreviewProps = {
|
||||||
|
requesterName: "Abigail Williams",
|
||||||
|
requesterEmail: "[email protected]",
|
||||||
|
orgName: "Example Org",
|
||||||
|
siteUrl: "https://infisical.com",
|
||||||
|
projectName: "Example Project",
|
||||||
|
note: "I need access to the project for the new initiative for HR.",
|
||||||
|
callback_url: "https://infisical.com"
|
||||||
|
} as ProjectAccessRequestTemplateProps;
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { Button, Heading, Section, Text } from "@react-email/components";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
|
||||||
|
|
||||||
|
interface ProjectInvitationTemplateProps extends Omit<BaseEmailWrapperProps, "preview" | "title"> {
|
||||||
|
callback_url: string;
|
||||||
|
workspaceName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProjectInvitationTemplate = ({ callback_url, workspaceName, siteUrl }: ProjectInvitationTemplateProps) => {
|
||||||
|
return (
|
||||||
|
<BaseEmailWrapper
|
||||||
|
title="Project Invitation"
|
||||||
|
preview="You've been invited to join a project on Infisical."
|
||||||
|
siteUrl={siteUrl}
|
||||||
|
>
|
||||||
|
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
|
||||||
|
You've been invited to join a project on Infisical
|
||||||
|
</Heading>
|
||||||
|
<Section className="px-[24px] mt-[36px] pt-[12px] pb-[8px] border text-center border-solid border-gray-200 rounded-md bg-gray-50">
|
||||||
|
<Text className="text-black text-[14px] leading-[24px]">
|
||||||
|
You've been invited to join the project <strong>{workspaceName}</strong>.
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
<Section className="text-center mt-[28px]">
|
||||||
|
<Button
|
||||||
|
href={callback_url}
|
||||||
|
className="rounded-md p-3 px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
|
||||||
|
>
|
||||||
|
Join Project
|
||||||
|
</Button>
|
||||||
|
</Section>
|
||||||
|
<Section className="mt-[24px] bg-gray-50 pt-[2px] pb-[16px] border border-solid border-gray-200 px-[24px] rounded-md text-gray-800">
|
||||||
|
<Text className="mb-[0px]">
|
||||||
|
<strong>About Infisical:</strong> Infisical is an all-in-one platform to securely manage application secrets,
|
||||||
|
certificates, SSH keys, and configurations across your team and infrastructure.
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
</BaseEmailWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProjectInvitationTemplate;
|
||||||
|
|
||||||
|
ProjectInvitationTemplate.PreviewProps = {
|
||||||
|
workspaceName: "Example Project",
|
||||||
|
siteUrl: "https://infisical.com",
|
||||||
|
callback_url: "https://app.infisical.com"
|
||||||
|
} as ProjectInvitationTemplateProps;
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { Button, Heading, Section, Text } from "@react-email/components";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
|
||||||
|
|
||||||
|
interface ScimUserProvisionedTemplateProps extends Omit<BaseEmailWrapperProps, "preview" | "title"> {
|
||||||
|
organizationName: string;
|
||||||
|
callback_url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ScimUserProvisionedTemplate = ({
|
||||||
|
organizationName,
|
||||||
|
callback_url,
|
||||||
|
siteUrl
|
||||||
|
}: ScimUserProvisionedTemplateProps) => {
|
||||||
|
return (
|
||||||
|
<BaseEmailWrapper
|
||||||
|
title="Organization Invitation"
|
||||||
|
preview="You've been invited to join an organization on Infisical."
|
||||||
|
siteUrl={siteUrl}
|
||||||
|
>
|
||||||
|
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
|
||||||
|
You've been invited to join
|
||||||
|
<br />
|
||||||
|
<strong>{organizationName}</strong> on <strong>Infisical</strong>
|
||||||
|
</Heading>
|
||||||
|
<Section className="px-[24px] mt-[36px] pt-[12px] pb-[8px] border text-center border-solid border-gray-200 rounded-md bg-gray-50">
|
||||||
|
<Text className="text-black text-[14px] leading-[24px]">
|
||||||
|
You've been invited to collaborate on <strong>{organizationName}</strong>.
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
<Section className="text-center mt-[28px]">
|
||||||
|
<Button
|
||||||
|
href={callback_url}
|
||||||
|
className="rounded-md p-3 px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
|
||||||
|
>
|
||||||
|
Accept Invite
|
||||||
|
</Button>
|
||||||
|
</Section>
|
||||||
|
<Section className="mt-[24px] bg-gray-50 pt-[2px] pb-[16px] border border-solid border-gray-200 px-[24px] rounded-md text-gray-800">
|
||||||
|
<Text className="mb-[0px]">
|
||||||
|
<strong>About Infisical:</strong> Infisical is an all-in-one platform to securely manage application secrets,
|
||||||
|
certificates, SSH keys, and configurations across your team and infrastructure.
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
</BaseEmailWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ScimUserProvisionedTemplate;
|
||||||
|
|
||||||
|
ScimUserProvisionedTemplate.PreviewProps = {
|
||||||
|
organizationName: "Example Organization",
|
||||||
|
callback_url: "https://app.infisical.com",
|
||||||
|
siteUrl: "https://app.infisical.com"
|
||||||
|
} as ScimUserProvisionedTemplateProps;
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { Button, Heading, Link, Section, Text } from "@react-email/components";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
|
||||||
|
|
||||||
|
interface SecretApprovalRequestBypassedTemplateProps
|
||||||
|
extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
|
||||||
|
projectName: string;
|
||||||
|
requesterFullName: string;
|
||||||
|
requesterEmail: string;
|
||||||
|
secretPath: string;
|
||||||
|
environment: string;
|
||||||
|
bypassReason: string;
|
||||||
|
approvalUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SecretApprovalRequestBypassedTemplate = ({
|
||||||
|
projectName,
|
||||||
|
siteUrl,
|
||||||
|
requesterFullName,
|
||||||
|
requesterEmail,
|
||||||
|
secretPath,
|
||||||
|
environment,
|
||||||
|
bypassReason,
|
||||||
|
approvalUrl
|
||||||
|
}: SecretApprovalRequestBypassedTemplateProps) => {
|
||||||
|
return (
|
||||||
|
<BaseEmailWrapper
|
||||||
|
title="Secret Approval Request Bypassed"
|
||||||
|
preview="A secret approval request has been bypassed."
|
||||||
|
siteUrl={siteUrl}
|
||||||
|
>
|
||||||
|
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
|
||||||
|
A secret approval request has been bypassed in the project <strong>{projectName}</strong>
|
||||||
|
</Heading>
|
||||||
|
<Section className="px-[24px] 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>{requesterFullName}</strong> (
|
||||||
|
<Link href={`mailto:${requesterEmail}`} className="text-slate-700 no-underline">
|
||||||
|
{requesterEmail}
|
||||||
|
</Link>
|
||||||
|
) has merged a secret to <strong>{secretPath}</strong> in the <strong>{environment}</strong> environment
|
||||||
|
without obtaining the required approval.
|
||||||
|
</Text>
|
||||||
|
<Text className="text-[14px] text-slate-700 leading-[24px]">
|
||||||
|
<strong className="text-black">The following reason was provided for bypassing the policy:</strong> "
|
||||||
|
{bypassReason}"
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
<Section className="text-center mt-[28px]">
|
||||||
|
<Button
|
||||||
|
href={approvalUrl}
|
||||||
|
className="rounded-md p-3 px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
|
||||||
|
>
|
||||||
|
Review Bypass
|
||||||
|
</Button>
|
||||||
|
</Section>
|
||||||
|
</BaseEmailWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SecretApprovalRequestBypassedTemplate;
|
||||||
|
|
||||||
|
SecretApprovalRequestBypassedTemplate.PreviewProps = {
|
||||||
|
requesterFullName: "Abigail Williams",
|
||||||
|
requesterEmail: "[email protected]",
|
||||||
|
secretPath: "/api/secrets",
|
||||||
|
environment: "Production",
|
||||||
|
siteUrl: "https://infisical.com",
|
||||||
|
projectName: "Example Project",
|
||||||
|
bypassReason: "I needed urgent access for a production misconfiguration."
|
||||||
|
} as SecretApprovalRequestBypassedTemplateProps;
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { Button, Heading, Section, Text } from "@react-email/components";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
|
||||||
|
|
||||||
|
interface SecretApprovalRequestNeedsReviewTemplateProps
|
||||||
|
extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
|
||||||
|
projectName: string;
|
||||||
|
firstName: string;
|
||||||
|
organizationName: string;
|
||||||
|
approvalUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SecretApprovalRequestNeedsReviewTemplate = ({
|
||||||
|
projectName,
|
||||||
|
siteUrl,
|
||||||
|
firstName,
|
||||||
|
organizationName,
|
||||||
|
approvalUrl
|
||||||
|
}: SecretApprovalRequestNeedsReviewTemplateProps) => {
|
||||||
|
return (
|
||||||
|
<BaseEmailWrapper
|
||||||
|
title="Secret Change Approval Request"
|
||||||
|
preview="A secret change approval request requires review."
|
||||||
|
siteUrl={siteUrl}
|
||||||
|
>
|
||||||
|
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
|
||||||
|
A secret approval request for the project <strong>{projectName}</strong> requires review
|
||||||
|
</Heading>
|
||||||
|
<Section className="px-[24px] mt-[36px] pt-[12px] pb-[8px] border border-solid border-gray-200 rounded-md bg-gray-50">
|
||||||
|
<Text className="text-[14px]">Hello {firstName},</Text>
|
||||||
|
<Text className="text-black text-[14px] leading-[24px]">
|
||||||
|
You have a new secret change request pending your review for the project <strong>{projectName}</strong> in the
|
||||||
|
organization <strong>{organizationName}</strong>.
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
<Section className="text-center mt-[28px]">
|
||||||
|
<Button
|
||||||
|
href={approvalUrl}
|
||||||
|
className="rounded-md p-3 px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
|
||||||
|
>
|
||||||
|
Review Changes
|
||||||
|
</Button>
|
||||||
|
</Section>
|
||||||
|
</BaseEmailWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SecretApprovalRequestNeedsReviewTemplate;
|
||||||
|
|
||||||
|
SecretApprovalRequestNeedsReviewTemplate.PreviewProps = {
|
||||||
|
firstName: "Gordon",
|
||||||
|
organizationName: "Example Org",
|
||||||
|
siteUrl: "https://infisical.com",
|
||||||
|
approvalUrl: "https://infisical.com",
|
||||||
|
projectName: "Example Project"
|
||||||
|
} as SecretApprovalRequestNeedsReviewTemplateProps;
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import { Button, Heading, Link, Section, Text } from "@react-email/components";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
|
||||||
|
|
||||||
|
interface SecretLeakIncidentTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
|
||||||
|
numberOfSecrets: number;
|
||||||
|
pusher_email: string;
|
||||||
|
pusher_name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SecretLeakIncidentTemplate = ({
|
||||||
|
numberOfSecrets,
|
||||||
|
siteUrl,
|
||||||
|
pusher_name,
|
||||||
|
pusher_email
|
||||||
|
}: SecretLeakIncidentTemplateProps) => {
|
||||||
|
return (
|
||||||
|
<BaseEmailWrapper
|
||||||
|
title="Incident Alert: Secret(s) Leaked"
|
||||||
|
preview="Infisical uncovered one or more leaked secrets."
|
||||||
|
siteUrl={siteUrl}
|
||||||
|
>
|
||||||
|
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
|
||||||
|
Infisical has uncovered <strong>{numberOfSecrets}</strong> secret(s) from a recent commit
|
||||||
|
</Heading>
|
||||||
|
<Section className="px-[24px] mt-[36px] pt-[8px] pb-[8px] text-[14px] border border-solid border-gray-200 rounded-md bg-gray-50">
|
||||||
|
<Text className="text-[14px]">
|
||||||
|
You are receiving this notification because one or more leaked secrets have been detected in a recent commit
|
||||||
|
{(pusher_email || pusher_name) && (
|
||||||
|
<>
|
||||||
|
{" "}
|
||||||
|
pushed by <strong>{pusher_name ?? "Unknown Pusher"}</strong>{" "}
|
||||||
|
{pusher_email && (
|
||||||
|
<>
|
||||||
|
(
|
||||||
|
<Link href={`mailto:${pusher_email}`} className="text-slate-700 no-underline">
|
||||||
|
{pusher_email}
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
.
|
||||||
|
</Text>
|
||||||
|
<Text className="text-[14px]">
|
||||||
|
If these are test secrets, please add `infisical-scan:ignore` at the end of the line containing the secret as
|
||||||
|
a comment in the given programming language. This will prevent future notifications from being sent out for
|
||||||
|
these secrets.
|
||||||
|
</Text>
|
||||||
|
<Text className="text-[14px] text-red-500">
|
||||||
|
If these are production secrets, please rotate them immediately.
|
||||||
|
</Text>
|
||||||
|
<Text className="text-[14px]">
|
||||||
|
Once you have taken action, be sure to update the status of the risk in the{" "}
|
||||||
|
<Link href={`${siteUrl}/organization/secret-scanning`} className="text-slate-700 no-underline">
|
||||||
|
Infisical Dashboard
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
<Section className="text-center mt-[28px]">
|
||||||
|
<Button
|
||||||
|
href={`${siteUrl}/organization/secret-scanning`}
|
||||||
|
className="rounded-md p-3 px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
|
||||||
|
>
|
||||||
|
View Leaked Secrets
|
||||||
|
</Button>
|
||||||
|
</Section>
|
||||||
|
</BaseEmailWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SecretLeakIncidentTemplate;
|
||||||
|
|
||||||
|
SecretLeakIncidentTemplate.PreviewProps = {
|
||||||
|
pusher_name: "Jim",
|
||||||
|
pusher_email: "[email protected]",
|
||||||
|
numberOfSecrets: 3,
|
||||||
|
siteUrl: "https://infisical.com"
|
||||||
|
} as SecretLeakIncidentTemplateProps;
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { Heading, Section, Text } from "@react-email/components";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
|
||||||
|
|
||||||
|
interface SecretReminderTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
|
||||||
|
projectName: string;
|
||||||
|
organizationName: string;
|
||||||
|
reminderNote?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SecretReminderTemplate = ({
|
||||||
|
siteUrl,
|
||||||
|
reminderNote,
|
||||||
|
projectName,
|
||||||
|
organizationName
|
||||||
|
}: SecretReminderTemplateProps) => {
|
||||||
|
return (
|
||||||
|
<BaseEmailWrapper title="Secret Reminder" preview="You have a new secret reminder." siteUrl={siteUrl}>
|
||||||
|
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
|
||||||
|
<strong>Secret Reminder</strong>
|
||||||
|
</Heading>
|
||||||
|
<Section className="px-[24px] mt-[36px] pt-[8px] pb-[8px] text-[14px] border border-solid border-gray-200 rounded-md bg-gray-50">
|
||||||
|
<Text className="text-[14px]">
|
||||||
|
You have a new secret reminder from the project <strong>{projectName}</strong> in the{" "}
|
||||||
|
<strong>{organizationName}</strong> organization.
|
||||||
|
</Text>
|
||||||
|
{reminderNote && (
|
||||||
|
<Text className="text-[14px] text-slate-700">
|
||||||
|
<strong className="text-black">Reminder Note:</strong> "{reminderNote}"
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Section>
|
||||||
|
</BaseEmailWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SecretReminderTemplate;
|
||||||
|
|
||||||
|
SecretReminderTemplate.PreviewProps = {
|
||||||
|
reminderNote: "Remember to rotate secret.",
|
||||||
|
projectName: "Example Project",
|
||||||
|
organizationName: "Example Organization",
|
||||||
|
siteUrl: "https://infisical.com"
|
||||||
|
} as SecretReminderTemplateProps;
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { Button, Heading, Section, Text } from "@react-email/components";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
|
||||||
|
|
||||||
|
interface SecretRequestCompletedTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
|
||||||
|
name?: string;
|
||||||
|
respondentUsername: string;
|
||||||
|
secretRequestUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SecretRequestCompletedTemplate = ({
|
||||||
|
name,
|
||||||
|
siteUrl,
|
||||||
|
respondentUsername,
|
||||||
|
secretRequestUrl
|
||||||
|
}: SecretRequestCompletedTemplateProps) => {
|
||||||
|
return (
|
||||||
|
<BaseEmailWrapper title="Shared Secret" preview="A secret has been shared with you." siteUrl={siteUrl}>
|
||||||
|
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
|
||||||
|
<strong>A secret has been shared with you</strong>
|
||||||
|
</Heading>
|
||||||
|
<Section className="px-[24px] mt-[36px] pt-[12px] text-center pb-[8px] border border-solid border-gray-200 rounded-md bg-gray-50">
|
||||||
|
<Text className="text-[14px]">
|
||||||
|
{respondentUsername ? <strong>{respondentUsername}</strong> : "Someone"} shared a secret{" "}
|
||||||
|
{name && (
|
||||||
|
<>
|
||||||
|
<strong>{name}</strong>{" "}
|
||||||
|
</>
|
||||||
|
)}{" "}
|
||||||
|
with you.
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
<Section className="text-center mt-[28px]">
|
||||||
|
<Button
|
||||||
|
href={secretRequestUrl}
|
||||||
|
className="rounded-md p-3 px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
|
||||||
|
>
|
||||||
|
View Secret
|
||||||
|
</Button>
|
||||||
|
</Section>
|
||||||
|
</BaseEmailWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SecretRequestCompletedTemplate;
|
||||||
|
|
||||||
|
SecretRequestCompletedTemplate.PreviewProps = {
|
||||||
|
respondentUsername: "Gracie",
|
||||||
|
siteUrl: "https://infisical.com",
|
||||||
|
secretRequestUrl: "https://infisical.com",
|
||||||
|
name: "API_TOKEN"
|
||||||
|
} as SecretRequestCompletedTemplateProps;
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { Button, Heading, Section, Text } from "@react-email/components";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
|
||||||
|
|
||||||
|
interface SecretRotationFailedTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
|
||||||
|
rotationType: string;
|
||||||
|
rotationName: string;
|
||||||
|
rotationUrl: string;
|
||||||
|
projectName: string;
|
||||||
|
environment: string;
|
||||||
|
secretPath: string;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SecretRotationFailedTemplate = ({
|
||||||
|
rotationType,
|
||||||
|
rotationName,
|
||||||
|
rotationUrl,
|
||||||
|
projectName,
|
||||||
|
siteUrl,
|
||||||
|
environment,
|
||||||
|
secretPath,
|
||||||
|
content
|
||||||
|
}: SecretRotationFailedTemplateProps) => {
|
||||||
|
return (
|
||||||
|
<BaseEmailWrapper title="Secret Rotation Failed" preview="A secret rotation failed." siteUrl={siteUrl}>
|
||||||
|
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
|
||||||
|
Your <strong>{rotationType}</strong> rotation <strong>{rotationName}</strong> failed to rotate
|
||||||
|
</Heading>
|
||||||
|
<Section className="px-[24px] mt-[36px] pt-[26px] pb-[4px] text-[14px] border border-solid border-gray-200 rounded-md bg-gray-50">
|
||||||
|
<strong>Name</strong>
|
||||||
|
<Text className="text-[14px] mt-[4px]">{rotationName}</Text>
|
||||||
|
<strong>Type</strong>
|
||||||
|
<Text className="text-[14px] mt-[4px]">{rotationType}</Text>
|
||||||
|
<strong>Project</strong>
|
||||||
|
<Text className="text-[14px] mt-[4px]">{projectName}</Text>
|
||||||
|
<strong>Environment</strong>
|
||||||
|
<Text className="text-[14px] mt-[4px]">{environment}</Text>
|
||||||
|
<strong>Secret Path</strong>
|
||||||
|
<Text className="text-[14px] mt-[4px]">{secretPath}</Text>
|
||||||
|
<strong>Reason:</strong>
|
||||||
|
<Text className="text-[14px] text-red-500 mt-[4px]">{content}</Text>
|
||||||
|
</Section>
|
||||||
|
<Section className="text-center mt-[28px]">
|
||||||
|
<Button
|
||||||
|
href={`${rotationUrl}?search=${rotationName}&secretPath=${secretPath}`}
|
||||||
|
className="rounded-md p-3 px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
|
||||||
|
>
|
||||||
|
View in Infisical
|
||||||
|
</Button>
|
||||||
|
</Section>
|
||||||
|
</BaseEmailWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SecretRotationFailedTemplate;
|
||||||
|
|
||||||
|
SecretRotationFailedTemplate.PreviewProps = {
|
||||||
|
rotationType: "Auth0 Client Secret",
|
||||||
|
rotationUrl: "https://infisical.com",
|
||||||
|
content: "See Rotation status for details",
|
||||||
|
projectName: "Example Project",
|
||||||
|
secretPath: "/api/secrets",
|
||||||
|
environment: "Production",
|
||||||
|
rotationName: "my-auth0-rotation",
|
||||||
|
siteUrl: "https://infisical.com"
|
||||||
|
} as SecretRotationFailedTemplateProps;
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import { Button, Heading, Section, Text } from "@react-email/components";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
|
||||||
|
|
||||||
|
interface SecretSyncFailedTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
|
||||||
|
syncDestination: string;
|
||||||
|
syncName: string;
|
||||||
|
syncUrl: string;
|
||||||
|
projectName: string;
|
||||||
|
environment: string;
|
||||||
|
secretPath: string;
|
||||||
|
failureMessage: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SecretSyncFailedTemplate = ({
|
||||||
|
syncDestination,
|
||||||
|
syncName,
|
||||||
|
syncUrl,
|
||||||
|
projectName,
|
||||||
|
siteUrl,
|
||||||
|
environment,
|
||||||
|
secretPath,
|
||||||
|
failureMessage
|
||||||
|
}: SecretSyncFailedTemplateProps) => {
|
||||||
|
return (
|
||||||
|
<BaseEmailWrapper title="Secret Sync Failed" preview="A secret sync failed." siteUrl={siteUrl}>
|
||||||
|
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
|
||||||
|
Your <strong>{syncDestination}</strong> sync <strong>{syncName}</strong> failed to complete
|
||||||
|
</Heading>
|
||||||
|
<Section className="px-[24px] mt-[36px] pt-[26px] pb-[4px] text-[14px] border border-solid border-gray-200 rounded-md bg-gray-50">
|
||||||
|
<strong>Name</strong>
|
||||||
|
<Text className="text-[14px] mt-[4px]">{syncName}</Text>
|
||||||
|
<strong>Destination</strong>
|
||||||
|
<Text className="text-[14px] mt-[4px]">{syncDestination}</Text>
|
||||||
|
<strong>Project</strong>
|
||||||
|
<Text className="text-[14px] mt-[4px]">{projectName}</Text>
|
||||||
|
{environment && (
|
||||||
|
<>
|
||||||
|
<strong>Environment</strong>
|
||||||
|
<Text className="text-[14px] mt-[4px]">{environment}</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{secretPath && (
|
||||||
|
<>
|
||||||
|
<strong>Secret Path</strong>
|
||||||
|
<Text className="text-[14px] mt-[4px]">{secretPath}</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{failureMessage && (
|
||||||
|
<>
|
||||||
|
<strong>Reason:</strong>
|
||||||
|
<Text className="text-[14px] text-red-500 mt-[4px]">{failureMessage}</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Section>
|
||||||
|
<Section className="text-center mt-[28px]">
|
||||||
|
<Button
|
||||||
|
href={syncUrl}
|
||||||
|
className="rounded-md p-3 px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
|
||||||
|
>
|
||||||
|
View in Infisical
|
||||||
|
</Button>
|
||||||
|
</Section>
|
||||||
|
</BaseEmailWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SecretSyncFailedTemplate;
|
||||||
|
|
||||||
|
SecretSyncFailedTemplate.PreviewProps = {
|
||||||
|
syncDestination: "AWS Parameter Store",
|
||||||
|
syncUrl: "https://infisical.com",
|
||||||
|
failureMessage: "Key name cannot contain a colon (:) or a forward slash (/).",
|
||||||
|
projectName: "Example Project",
|
||||||
|
secretPath: "/api/secrets",
|
||||||
|
environment: "Production",
|
||||||
|
syncName: "my-aws-sync",
|
||||||
|
siteUrl: "https://infisical.com"
|
||||||
|
} as SecretSyncFailedTemplateProps;
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { Button, Heading, Section, Text } from "@react-email/components";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
|
||||||
|
|
||||||
|
interface ServiceTokenExpiryNoticeTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
|
||||||
|
tokenName: string;
|
||||||
|
projectName: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ServiceTokenExpiryNoticeTemplate = ({
|
||||||
|
tokenName,
|
||||||
|
siteUrl,
|
||||||
|
projectName,
|
||||||
|
url
|
||||||
|
}: ServiceTokenExpiryNoticeTemplateProps) => {
|
||||||
|
return (
|
||||||
|
<BaseEmailWrapper
|
||||||
|
title="Service Token Expiring Soon"
|
||||||
|
preview="A service token is about to expire."
|
||||||
|
siteUrl={siteUrl}
|
||||||
|
>
|
||||||
|
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
|
||||||
|
<strong>Service token expiry notice</strong>
|
||||||
|
</Heading>
|
||||||
|
<Section className="px-[24px] mt-[36px] pt-[12px] pb-[8px] border border-solid border-gray-200 rounded-md bg-gray-50">
|
||||||
|
<Text className="text-[14px]">
|
||||||
|
Your service token <strong>{tokenName}</strong> for the project <strong>{projectName}</strong> will expire
|
||||||
|
within 24 hours.
|
||||||
|
</Text>
|
||||||
|
<Text>If this token is still needed for your workflow, please create a new one before it expires.</Text>
|
||||||
|
</Section>
|
||||||
|
<Section className="text-center mt-[28px]">
|
||||||
|
<Button
|
||||||
|
href={url}
|
||||||
|
className="rounded-md p-3 px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
|
||||||
|
>
|
||||||
|
Create New Token
|
||||||
|
</Button>
|
||||||
|
</Section>
|
||||||
|
</BaseEmailWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ServiceTokenExpiryNoticeTemplate;
|
||||||
|
|
||||||
|
ServiceTokenExpiryNoticeTemplate.PreviewProps = {
|
||||||
|
projectName: "Example Project",
|
||||||
|
siteUrl: "https://infisical.com",
|
||||||
|
url: "https://infisical.com",
|
||||||
|
tokenName: "Example Token"
|
||||||
|
} as ServiceTokenExpiryNoticeTemplateProps;
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { Heading, Link, Section, Text } from "@react-email/components";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
|
||||||
|
|
||||||
|
interface SignupEmailVerificationTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
|
||||||
|
code: string;
|
||||||
|
isCloud: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SignupEmailVerificationTemplate = ({ code, siteUrl, isCloud }: SignupEmailVerificationTemplateProps) => {
|
||||||
|
return (
|
||||||
|
<BaseEmailWrapper
|
||||||
|
title="Confirm Your Email Address"
|
||||||
|
preview="Verify your email address to get started with Infisical."
|
||||||
|
siteUrl={siteUrl}
|
||||||
|
>
|
||||||
|
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
|
||||||
|
<strong>Confirm your email address</strong>
|
||||||
|
</Heading>
|
||||||
|
<Section className="px-[24px] mt-[36px] pt-[8px] text-center pb-[8px] text-[14px] border border-solid border-gray-200 rounded-md bg-gray-50">
|
||||||
|
<Text>Enter the confirmation code shown below in the browser where you started sign-up.</Text>
|
||||||
|
<Text className="text-[24px] mt-[16px]">
|
||||||
|
<strong>{code}</strong>
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
<Section className="mt-[24px] bg-gray-50 pt-[2px] pb-[16px] border border-solid border-gray-200 px-[24px] rounded-md text-gray-800">
|
||||||
|
<Text className="mb-[0px]">
|
||||||
|
<strong>Questions about setting up Infisical?</strong>{" "}
|
||||||
|
{isCloud ? (
|
||||||
|
<>
|
||||||
|
Email us at{" "}
|
||||||
|
<Link href="mailto:[email protected]" className="text-slate-700 no-underline">
|
||||||
|
support@infisical.com
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"Contact your administrator"
|
||||||
|
)}
|
||||||
|
.
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
</BaseEmailWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SignupEmailVerificationTemplate;
|
||||||
|
|
||||||
|
SignupEmailVerificationTemplate.PreviewProps = {
|
||||||
|
code: "124356",
|
||||||
|
isCloud: true,
|
||||||
|
siteUrl: "https://infisical.com"
|
||||||
|
} as SignupEmailVerificationTemplateProps;
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { Button, Heading, Section, Text } from "@react-email/components";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
|
||||||
|
|
||||||
|
interface UnlockAccountTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
|
||||||
|
token: string;
|
||||||
|
callback_url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UnlockAccountTemplate = ({ token, siteUrl, callback_url }: UnlockAccountTemplateProps) => {
|
||||||
|
return (
|
||||||
|
<BaseEmailWrapper
|
||||||
|
title="Your Infisical Account Has Been Locked"
|
||||||
|
preview="Unlock your Infisical account to continue."
|
||||||
|
siteUrl={siteUrl}
|
||||||
|
>
|
||||||
|
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
|
||||||
|
<strong>Unlock your Infisical account</strong>
|
||||||
|
</Heading>
|
||||||
|
<Section className="px-[24px] mt-[36px] pt-[12px] pb-[8px] border border-solid border-gray-200 rounded-md bg-gray-50">
|
||||||
|
<Text className="text-[14px]">
|
||||||
|
Your account has been temporarily locked due to multiple failed login attempts.
|
||||||
|
</Text>
|
||||||
|
<Text>If these attempts were not made by you, reset your password immediately.</Text>
|
||||||
|
</Section>
|
||||||
|
<Section className="text-center mt-[28px]">
|
||||||
|
<Button
|
||||||
|
href={`${callback_url}?token=${token}`}
|
||||||
|
className="rounded-md p-3 px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
|
||||||
|
>
|
||||||
|
Unlock Account
|
||||||
|
</Button>
|
||||||
|
</Section>
|
||||||
|
</BaseEmailWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UnlockAccountTemplate;
|
||||||
|
|
||||||
|
UnlockAccountTemplate.PreviewProps = {
|
||||||
|
callback_url: "Example Project",
|
||||||
|
siteUrl: "https://infisical.com",
|
||||||
|
token: "preview-token"
|
||||||
|
} as UnlockAccountTemplateProps;
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
export * from "./AccessApprovalRequestTemplate";
|
||||||
|
export * from "./EmailMfaTemplate";
|
||||||
|
export * from "./EmailVerificationTemplate";
|
||||||
|
export * from "./ExternalImportFailedTemplate";
|
||||||
|
export * from "./ExternalImportStartedTemplate";
|
||||||
|
export * from "./ExternalImportSucceededTemplate";
|
||||||
|
export * from "./IntegrationSyncFailedTemplate";
|
||||||
|
export * from "./NewDeviceLoginTemplate";
|
||||||
|
export * from "./OrgAdminBreakglassAccessTemplate";
|
||||||
|
export * from "./OrgAdminProjectGrantAccessTemplate";
|
||||||
|
export * from "./OrganizationInvitationTemplate";
|
||||||
|
export * from "./PasswordResetTemplate";
|
||||||
|
export * from "./PasswordSetupTemplate";
|
||||||
|
export * from "./PkiExpirationAlertTemplate";
|
||||||
|
export * from "./ProjectAccessRequestTemplate";
|
||||||
|
export * from "./ProjectInvitationTemplate";
|
||||||
|
export * from "./ScimUserProvisionedTemplate";
|
||||||
|
export * from "./SecretApprovalRequestBypassedTemplate";
|
||||||
|
export * from "./SecretApprovalRequestNeedsReviewTemplate";
|
||||||
|
export * from "./SecretLeakIncidentTemplate";
|
||||||
|
export * from "./SecretReminderTemplate";
|
||||||
|
export * from "./SecretRequestCompletedTemplate";
|
||||||
|
export * from "./SecretRotationFailedTemplate";
|
||||||
|
export * from "./SecretSyncFailedTemplate";
|
||||||
|
export * from "./ServiceTokenExpiryNoticeTemplate";
|
||||||
|
export * from "./SignupEmailVerificationTemplate";
|
||||||
|
export * from "./UnlockAccountTemplate";
|
||||||
@@ -1,13 +1,41 @@
|
|||||||
import fs from "node:fs/promises";
|
import { render } from "@react-email/components";
|
||||||
import path from "node:path";
|
|
||||||
|
|
||||||
import handlebars from "handlebars";
|
|
||||||
import { createTransport } from "nodemailer";
|
import { createTransport } from "nodemailer";
|
||||||
import SMTPTransport from "nodemailer/lib/smtp-transport";
|
import SMTPTransport from "nodemailer/lib/smtp-transport";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
|
|
||||||
|
import {
|
||||||
|
AccessApprovalRequestTemplate,
|
||||||
|
EmailMfaTemplate,
|
||||||
|
EmailVerificationTemplate,
|
||||||
|
ExternalImportFailedTemplate,
|
||||||
|
ExternalImportStartedTemplate,
|
||||||
|
ExternalImportSucceededTemplate,
|
||||||
|
IntegrationSyncFailedTemplate,
|
||||||
|
NewDeviceLoginTemplate,
|
||||||
|
OrgAdminBreakglassAccessTemplate,
|
||||||
|
OrgAdminProjectGrantAccessTemplate,
|
||||||
|
OrganizationInvitationTemplate,
|
||||||
|
PasswordResetTemplate,
|
||||||
|
PasswordSetupTemplate,
|
||||||
|
PkiExpirationAlertTemplate,
|
||||||
|
ProjectAccessRequestTemplate,
|
||||||
|
ProjectInvitationTemplate,
|
||||||
|
ScimUserProvisionedTemplate,
|
||||||
|
SecretApprovalRequestBypassedTemplate,
|
||||||
|
SecretApprovalRequestNeedsReviewTemplate,
|
||||||
|
SecretLeakIncidentTemplate,
|
||||||
|
SecretReminderTemplate,
|
||||||
|
SecretRequestCompletedTemplate,
|
||||||
|
SecretRotationFailedTemplate,
|
||||||
|
SecretSyncFailedTemplate,
|
||||||
|
ServiceTokenExpiryNoticeTemplate,
|
||||||
|
SignupEmailVerificationTemplate,
|
||||||
|
UnlockAccountTemplate
|
||||||
|
} from "./emails";
|
||||||
|
|
||||||
export type TSmtpConfig = SMTPTransport.Options;
|
export type TSmtpConfig = SMTPTransport.Options;
|
||||||
export type TSmtpSendMail = {
|
export type TSmtpSendMail = {
|
||||||
template: SmtpTemplates;
|
template: SmtpTemplates;
|
||||||
@@ -18,61 +46,96 @@ export type TSmtpSendMail = {
|
|||||||
export type TSmtpService = ReturnType<typeof smtpServiceFactory>;
|
export type TSmtpService = ReturnType<typeof smtpServiceFactory>;
|
||||||
|
|
||||||
export enum SmtpTemplates {
|
export enum SmtpTemplates {
|
||||||
SignupEmailVerification = "signupEmailVerification.handlebars",
|
SignupEmailVerification = "signupEmailVerification",
|
||||||
EmailVerification = "emailVerification.handlebars",
|
EmailVerification = "emailVerification",
|
||||||
SecretReminder = "secretReminder.handlebars",
|
SecretReminder = "secretReminder",
|
||||||
EmailMfa = "emailMfa.handlebars",
|
EmailMfa = "emailMfa",
|
||||||
UnlockAccount = "unlockAccount.handlebars",
|
UnlockAccount = "unlockAccount",
|
||||||
AccessApprovalRequest = "accessApprovalRequest.handlebars",
|
AccessApprovalRequest = "accessApprovalRequest",
|
||||||
AccessSecretRequestBypassed = "accessSecretRequestBypassed.handlebars",
|
AccessSecretRequestBypassed = "accessSecretRequestBypassed",
|
||||||
SecretApprovalRequestNeedsReview = "secretApprovalRequestNeedsReview.handlebars",
|
SecretApprovalRequestNeedsReview = "secretApprovalRequestNeedsReview",
|
||||||
HistoricalSecretList = "historicalSecretLeakIncident.handlebars",
|
// HistoricalSecretList = "historicalSecretLeakIncident", not used anymore?
|
||||||
NewDeviceJoin = "newDevice.handlebars",
|
NewDeviceJoin = "newDevice",
|
||||||
OrgInvite = "organizationInvitation.handlebars",
|
OrgInvite = "organizationInvitation",
|
||||||
ResetPassword = "passwordReset.handlebars",
|
ResetPassword = "passwordReset",
|
||||||
SetupPassword = "passwordSetup.handlebars",
|
SetupPassword = "passwordSetup",
|
||||||
SecretLeakIncident = "secretLeakIncident.handlebars",
|
SecretLeakIncident = "secretLeakIncident",
|
||||||
WorkspaceInvite = "workspaceInvitation.handlebars",
|
WorkspaceInvite = "workspaceInvitation",
|
||||||
ScimUserProvisioned = "scimUserProvisioned.handlebars",
|
ScimUserProvisioned = "scimUserProvisioned",
|
||||||
PkiExpirationAlert = "pkiExpirationAlert.handlebars",
|
PkiExpirationAlert = "pkiExpirationAlert",
|
||||||
IntegrationSyncFailed = "integrationSyncFailed.handlebars",
|
IntegrationSyncFailed = "integrationSyncFailed",
|
||||||
SecretSyncFailed = "secretSyncFailed.handlebars",
|
SecretSyncFailed = "secretSyncFailed",
|
||||||
ExternalImportSuccessful = "externalImportSuccessful.handlebars",
|
ExternalImportSuccessful = "externalImportSuccessful",
|
||||||
ExternalImportFailed = "externalImportFailed.handlebars",
|
ExternalImportFailed = "externalImportFailed",
|
||||||
ExternalImportStarted = "externalImportStarted.handlebars",
|
ExternalImportStarted = "externalImportStarted",
|
||||||
SecretRequestCompleted = "secretRequestCompleted.handlebars",
|
SecretRequestCompleted = "secretRequestCompleted",
|
||||||
SecretRotationFailed = "secretRotationFailed.handlebars",
|
SecretRotationFailed = "secretRotationFailed",
|
||||||
ProjectAccessRequest = "projectAccess.handlebars",
|
ProjectAccessRequest = "projectAccess",
|
||||||
OrgAdminProjectDirectAccess = "orgAdminProjectGrantAccess.handlebars",
|
OrgAdminProjectDirectAccess = "orgAdminProjectGrantAccess",
|
||||||
OrgAdminBreakglassAccess = "orgAdminBreakglassAccess.handlebars",
|
OrgAdminBreakglassAccess = "orgAdminBreakglassAccess",
|
||||||
ServiceTokenExpired = "serviceTokenExpired.handlebars"
|
ServiceTokenExpired = "serviceTokenExpired"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SmtpHost {
|
export enum SmtpHost {
|
||||||
Sendgrid = "smtp.sendgrid.net",
|
Sendgrid = "smtp.sendgrid.net",
|
||||||
Mailgun = "smtp.mailgun.org",
|
Mailgun = "smtp.mailgun.org",
|
||||||
SocketLabs = "smtp.sockerlabs.com",
|
SocketLabs = "smtp.socketlabs.com",
|
||||||
Zohomail = "smtp.zoho.com",
|
Zohomail = "smtp.zoho.com",
|
||||||
Gmail = "smtp.gmail.com",
|
Gmail = "smtp.gmail.com",
|
||||||
Office365 = "smtp.office365.com"
|
Office365 = "smtp.office365.com"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const EmailTemplateMap: Record<SmtpTemplates, React.FC<any>> = {
|
||||||
|
[SmtpTemplates.OrgInvite]: OrganizationInvitationTemplate,
|
||||||
|
[SmtpTemplates.NewDeviceJoin]: NewDeviceLoginTemplate,
|
||||||
|
[SmtpTemplates.SignupEmailVerification]: SignupEmailVerificationTemplate,
|
||||||
|
[SmtpTemplates.EmailMfa]: EmailMfaTemplate,
|
||||||
|
[SmtpTemplates.AccessApprovalRequest]: AccessApprovalRequestTemplate,
|
||||||
|
[SmtpTemplates.EmailVerification]: EmailVerificationTemplate,
|
||||||
|
[SmtpTemplates.ExternalImportFailed]: ExternalImportFailedTemplate,
|
||||||
|
[SmtpTemplates.ExternalImportStarted]: ExternalImportStartedTemplate,
|
||||||
|
[SmtpTemplates.ExternalImportSuccessful]: ExternalImportSucceededTemplate,
|
||||||
|
[SmtpTemplates.AccessSecretRequestBypassed]: SecretApprovalRequestBypassedTemplate,
|
||||||
|
[SmtpTemplates.IntegrationSyncFailed]: IntegrationSyncFailedTemplate,
|
||||||
|
[SmtpTemplates.OrgAdminBreakglassAccess]: OrgAdminBreakglassAccessTemplate,
|
||||||
|
[SmtpTemplates.SecretLeakIncident]: SecretLeakIncidentTemplate,
|
||||||
|
[SmtpTemplates.WorkspaceInvite]: ProjectInvitationTemplate,
|
||||||
|
[SmtpTemplates.ScimUserProvisioned]: ScimUserProvisionedTemplate,
|
||||||
|
[SmtpTemplates.SecretRequestCompleted]: SecretRequestCompletedTemplate,
|
||||||
|
[SmtpTemplates.UnlockAccount]: UnlockAccountTemplate,
|
||||||
|
[SmtpTemplates.ServiceTokenExpired]: ServiceTokenExpiryNoticeTemplate,
|
||||||
|
[SmtpTemplates.SecretReminder]: SecretReminderTemplate,
|
||||||
|
[SmtpTemplates.SecretRotationFailed]: SecretRotationFailedTemplate,
|
||||||
|
[SmtpTemplates.SecretSyncFailed]: SecretSyncFailedTemplate,
|
||||||
|
[SmtpTemplates.OrgAdminProjectDirectAccess]: OrgAdminProjectGrantAccessTemplate,
|
||||||
|
[SmtpTemplates.ProjectAccessRequest]: ProjectAccessRequestTemplate,
|
||||||
|
[SmtpTemplates.SecretApprovalRequestNeedsReview]: SecretApprovalRequestNeedsReviewTemplate,
|
||||||
|
[SmtpTemplates.ResetPassword]: PasswordResetTemplate,
|
||||||
|
[SmtpTemplates.SetupPassword]: PasswordSetupTemplate,
|
||||||
|
[SmtpTemplates.PkiExpirationAlert]: PkiExpirationAlertTemplate
|
||||||
|
};
|
||||||
|
|
||||||
export const smtpServiceFactory = (cfg: TSmtpConfig) => {
|
export const smtpServiceFactory = (cfg: TSmtpConfig) => {
|
||||||
const smtp = createTransport(cfg);
|
const smtp = createTransport(cfg);
|
||||||
const isSmtpOn = Boolean(cfg.host);
|
const isSmtpOn = Boolean(cfg.host);
|
||||||
|
|
||||||
handlebars.registerHelper("emailFooter", () => {
|
|
||||||
const { SITE_URL } = getConfig();
|
|
||||||
return new handlebars.SafeString(
|
|
||||||
`<p style="font-size: 12px;">Email sent via Infisical at <a href="${SITE_URL}">${SITE_URL}</a></p>`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const sendMail = async ({ substitutions, recipients, template, subjectLine }: TSmtpSendMail) => {
|
const sendMail = async ({ substitutions, recipients, template, subjectLine }: TSmtpSendMail) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
const html = await fs.readFile(path.resolve(__dirname, "./templates/", template), "utf8");
|
|
||||||
const temp = handlebars.compile(html);
|
const EmailTemplate = EmailTemplateMap[template];
|
||||||
const htmlToSend = temp({ isCloud: appCfg.isCloud, siteUrl: appCfg.SITE_URL, ...substitutions });
|
|
||||||
|
if (!EmailTemplate) {
|
||||||
|
throw new Error(`Email template ${template} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const htmlToSend = await render(
|
||||||
|
React.createElement(EmailTemplate, {
|
||||||
|
...substitutions,
|
||||||
|
isCloud: appCfg.isCloud,
|
||||||
|
siteUrl: appCfg.SITE_URL
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
if (isSmtpOn) {
|
if (isSmtpOn) {
|
||||||
await smtp.sendMail({
|
await smtp.sendMail({
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
<html>
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
|
||||||
<title>Access Approval Request</title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<h2>Infisical</h2>
|
|
||||||
<h2>New access approval request pending your review</h2>
|
|
||||||
<p>You have a new access approval request pending review in project "{{projectName}}".</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
{{requesterFullName}}
|
|
||||||
({{requesterEmail}}) has requested
|
|
||||||
{{#if isTemporary}}
|
|
||||||
temporary
|
|
||||||
{{else}}
|
|
||||||
permanent
|
|
||||||
{{/if}}
|
|
||||||
access to
|
|
||||||
{{secretPath}}
|
|
||||||
in the
|
|
||||||
{{environment}}
|
|
||||||
environment.
|
|
||||||
|
|
||||||
{{#if isTemporary}}
|
|
||||||
<br />
|
|
||||||
This access will expire
|
|
||||||
{{expiresIn}}
|
|
||||||
after it has been approved.
|
|
||||||
{{/if}}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
The following permissions are requested:
|
|
||||||
<ul>
|
|
||||||
{{#each permissions}}
|
|
||||||
<li>{{this}}</li>
|
|
||||||
{{/each}}
|
|
||||||
</ul>
|
|
||||||
</p>
|
|
||||||
{{#if note}}
|
|
||||||
<p>User Note: "{{note}}"</p>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
<p>
|
|
||||||
View the request and approve or deny it
|
|
||||||
<a href="{{approvalUrl}}">here</a>.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{{emailFooter}}
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
|
||||||
<title>Secret Approval Request Policy Bypassed</title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<h1>Infisical</h1>
|
|
||||||
<h2>Secret Approval Request Bypassed</h2>
|
|
||||||
<p>A secret approval request has been bypassed in the project "{{projectName}}".</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
{{requesterFullName}}
|
|
||||||
({{requesterEmail}}) has merged a secret to environment
|
|
||||||
{{environment}}
|
|
||||||
at secret path
|
|
||||||
{{secretPath}}
|
|
||||||
without obtaining the required approvals.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
The following reason was provided for bypassing the policy:
|
|
||||||
<em>{{bypassReason}}</em>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
To review this action, please visit the request panel
|
|
||||||
<a href="{{approvalUrl}}">here</a>.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{{emailFooter}}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
<html>
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
|
||||||
<title>MFA Code</title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<h2>Infisical</h2>
|
|
||||||
<h2>Sign in attempt requires further verification</h2>
|
|
||||||
<p>Your MFA code is below — enter it where you started signing in to Infisical.</p>
|
|
||||||
<h2>{{code}}</h2>
|
|
||||||
<p>The MFA code will be valid for 2 minutes.</p>
|
|
||||||
<p>Not you? Contact {{#if isCloud}}Infisical{{else}}your administrator{{/if}} immediately.</p>
|
|
||||||
|
|
||||||
{{emailFooter}}
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
<html>
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
|
||||||
<title>Code</title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<h2>Confirm your email address</h2>
|
|
||||||
<p>Your confirmation code is below — enter it in the browser window where you've started confirming your email.</p>
|
|
||||||
<h1>{{code}}</h1>
|
|
||||||
|
|
||||||
{{emailFooter}}
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
<html>
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
|
||||||
<title>Import failed</title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<h2>An import from {{provider}} to Infisical has failed</h2>
|
|
||||||
<p>An import from
|
|
||||||
{{provider}}
|
|
||||||
to Infisical has failed due to unforeseen circumstances. Please re-try your import, and if the issue persists, you
|
|
||||||
can contact the Infisical team at [email protected].
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>Error: {{error}}</p>
|
|
||||||
|
|
||||||
{{emailFooter}}
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
<html>
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
|
||||||
<title>Import in progress</title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<h2>An import from {{provider}} to Infisical is in progress</h2>
|
|
||||||
<p>An import from
|
|
||||||
{{provider}}
|
|
||||||
to Infisical is in progress. The import process may take up to 30 minutes, and you will receive once the import
|
|
||||||
has finished or if it fails.</p>
|
|
||||||
|
|
||||||
{{emailFooter}}
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<html>
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
|
||||||
<title>Import successful</title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<h2>An import from {{provider}} to Infisical was successful</h2>
|
|
||||||
<p>An import from {{provider}} was successful. Your data is now available in Infisical.</p>
|
|
||||||
|
|
||||||
{{emailFooter}}
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
<html>
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
|
||||||
<title>Incident alert: secrets potentially leaked</title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<h3>Infisical has uncovered {{numberOfSecrets}} secret(s) from historical commits to your repo</h3>
|
|
||||||
<p><a href="{{siteUrl}}/secret-scanning"><strong>View leaked secrets</strong></a></p>
|
|
||||||
|
|
||||||
<p>If these are production secrets, please rotate them immediately.</p>
|
|
||||||
|
|
||||||
<p>Once you have taken action, be sure to update the status of the risk in your
|
|
||||||
<a href="{{siteUrl}}">Infisical dashboard</a>.</p>
|
|
||||||
|
|
||||||
{{emailFooter}}
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
<html>
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
|
||||||
<title>Integration Sync Failed</title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<h2>Infisical</h2>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<p>{{count}} integration(s) failed to sync.</p>
|
|
||||||
<a href="{{integrationUrl}}">
|
|
||||||
View your project integrations.
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<br />
|
|
||||||
<div>
|
|
||||||
<p><strong>Project</strong>: {{projectName}}</p>
|
|
||||||
<p><strong>Environment</strong>: {{environment}}</p>
|
|
||||||
<p><strong>Secret Path</strong>: {{secretPath}}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{#if syncMessage}}
|
|
||||||
<p><b>Reason: </b>{{syncMessage}}</p>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{emailFooter}}
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
<html>
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
|
||||||
<title>Successful login for {{email}} from new device</title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<h2>Infisical</h2>
|
|
||||||
<p>We're verifying a recent login for {{email}}:</p>
|
|
||||||
<p><strong>Timestamp</strong>: {{timestamp}}</p>
|
|
||||||
<p><strong>IP address</strong>: {{ip}}</p>
|
|
||||||
<p><strong>User agent</strong>: {{userAgent}}</p>
|
|
||||||
<p>If you believe that this login is suspicious, please contact
|
|
||||||
{{#if isCloud}}Infisical{{else}}your administrator{{/if}}
|
|
||||||
or reset your password immediately.</p>
|
|
||||||
|
|
||||||
{{emailFooter}}
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
<html>
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
|
||||||
<title>Organization admin has bypassed SSO</title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<h2>Infisical</h2>
|
|
||||||
<p>The organization admin {{email}} has bypassed enforced SSO login.</p>
|
|
||||||
<p><strong>Timestamp</strong>: {{timestamp}}</p>
|
|
||||||
<p><strong>IP address</strong>: {{ip}}</p>
|
|
||||||
<p><strong>User agent</strong>: {{userAgent}}</p>
|
|
||||||
<p>If you'd like to disable Admin SSO Bypass, please visit <a href="{{siteUrl}}/organization/settings">Organization Settings</a> > Security.</p>
|
|
||||||
|
|
||||||
{{emailFooter}}
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<html>
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
|
||||||
<title>Organization admin issued direct access to project</title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<h2>Infisical</h2>
|
|
||||||
<p>The organization admin {{email}} has granted direct access to the project "{{projectName}}".</p>
|
|
||||||
|
|
||||||
{{emailFooter}}
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
|
||||||
<title>Organization Invitation</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h2>Join your organization on Infisical</h2>
|
|
||||||
<p>{{inviterFirstName}} ({{inviterUsername}}) has invited you to their Infisical organization named {{organizationName}}</p>
|
|
||||||
<a href="{{callback_url}}?token={{token}}{{#if metadata}}&metadata={{metadata}}{{/if}}&to={{email}}&organization_id={{organizationId}}">Click to join</a>
|
|
||||||
<h3>What is Infisical?</h3>
|
|
||||||
<p>Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets and configs.</p>
|
|
||||||
|
|
||||||
{{emailFooter}}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
|
||||||
<title>Account Recovery</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h2>Reset your password</h2>
|
|
||||||
<p>Someone requested a password reset.</p>
|
|
||||||
<a href="{{callback_url}}?token={{token}}&to={{email}}">Reset password</a>
|
|
||||||
<p>If you didn't initiate this request, please contact
|
|
||||||
{{#if isCloud}}us immediately at [email protected].{{else}}your administrator immediately.{{/if}}</p>
|
|
||||||
|
|
||||||
{{emailFooter}}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
|
||||||
<title>Password Setup</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h2>Setup your password</h2>
|
|
||||||
<p>Someone requested to set up a password for your account.</p>
|
|
||||||
<p><strong>Make sure you are already logged in to Infisical in the current browser before clicking the link below.</strong></p>
|
|
||||||
<a href="{{callback_url}}?token={{token}}&to={{email}}">Setup password</a>
|
|
||||||
<p>If you didn't initiate this request, please contact
|
|
||||||
{{#if isCloud}}us immediately at [email protected].{{else}}your administrator immediately.{{/if}}</p>
|
|
||||||
|
|
||||||
{{emailFooter}}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
|
||||||
<title>Infisical CA/Certificate expiration notice</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<p>Hello,</p>
|
|
||||||
<p>This is an automated alert for "{{alertName}}" triggered for CAs/Certificates expiring in
|
|
||||||
{{alertBeforeDays}}
|
|
||||||
days.</p>
|
|
||||||
|
|
||||||
<p>Expiring Items:</p>
|
|
||||||
<ul>
|
|
||||||
{{#each items}}
|
|
||||||
<li>
|
|
||||||
{{type}}:
|
|
||||||
<strong>{{friendlyName}}</strong>
|
|
||||||
<br />Serial Number:
|
|
||||||
{{serialNumber}}
|
|
||||||
<br />Expires On:
|
|
||||||
{{expiryDate}}
|
|
||||||
</li>
|
|
||||||
{{/each}}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p>Please take necessary actions to renew these items before they expire.</p>
|
|
||||||
|
|
||||||
<p>For more details, please log in to your Infisical account and check your PKI management section.</p>
|
|
||||||
|
|
||||||
{{emailFooter}}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
<html>
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
|
||||||
<title>Project Access Request</title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<h2>Infisical</h2>
|
|
||||||
<h2>You have a new project access request!</h2>
|
|
||||||
<ul>
|
|
||||||
<li>Requester Name: "{{requesterName}}"</li>
|
|
||||||
<li>Requester Email: "{{requesterEmail}}"</li>
|
|
||||||
<li>Project Name: "{{projectName}}"</li>
|
|
||||||
<li>Organization Name: "{{orgName}}"</li>
|
|
||||||
<li>User Note: "{{note}}"</li>
|
|
||||||
</ul>
|
|
||||||
<p>
|
|
||||||
Please click on the link below to grant access
|
|
||||||
</p>
|
|
||||||
<a href="{{callback_url}}">Grant Access</a>
|
|
||||||
{{emailFooter}}
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
<html>
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
|
||||||
<title>Organization Invitation</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h2>Join your organization on Infisical</h2>
|
|
||||||
<p>You've been invited to join the Infisical organization — {{organizationName}}</p>
|
|
||||||
<a href="{{callback_url}}">Join now</a>
|
|
||||||
<h3>What is Infisical?</h3>
|
|
||||||
<p>Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets
|
|
||||||
and configs.</p>
|
|
||||||
|
|
||||||
{{emailFooter}}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
<html>
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
|
||||||
<title>Secret Change Approval Request</title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<h2>Hi {{firstName}},</h2>
|
|
||||||
<h2>New secret change requests are pending review.</h2>
|
|
||||||
<br />
|
|
||||||
<p>You have a secret change request pending your review in project "{{projectName}}", in the "{{organizationName}}"
|
|
||||||
organization.</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
View the request and approve or deny it
|
|
||||||
<a href="{{approvalUrl}}">here</a>.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{{emailFooter}}
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
<html>
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
|
||||||
<title>Incident alert: secret leaked</title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<h3>Infisical has uncovered {{numberOfSecrets}} secret(s) from your recent push</h3>
|
|
||||||
<p><a href="{{siteUrl}}/secret-scanning"><strong>View leaked secrets</strong></a></p>
|
|
||||||
<p>You are receiving this notification because one or more secret leaks have been detected in a recent commit pushed
|
|
||||||
by
|
|
||||||
{{pusher_name}}
|
|
||||||
({{pusher_email}}). If these are test secrets, please add `infisical-scan:ignore` at the end of the line
|
|
||||||
containing the secret as comment in the given programming. This will prevent future notifications from being sent
|
|
||||||
out for those secret(s).</p>
|
|
||||||
|
|
||||||
<p>If these are production secrets, please rotate them immediately.</p>
|
|
||||||
|
|
||||||
<p>Once you have taken action, be sure to update the status of the risk in your
|
|
||||||
<a href="{{siteUrl}}">Infisical dashboard</a>.</p>
|
|
||||||
|
|
||||||
{{emailFooter}}
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
<html>
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
|
||||||
<title>Secret Reminder</title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<h2>Infisical</h2>
|
|
||||||
<h2>You have a new secret reminder!</h2>
|
|
||||||
<p>You have a new secret reminder from project "{{projectName}}", in {{organizationName}}</p>
|
|
||||||
{{#if reminderNote}}
|
|
||||||
<p>Here's the note included with the reminder: {{reminderNote}}</p>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{emailFooter}}
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
<html>
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
|
||||||
<title>Secret Request Completed</title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<h2>Infisical</h2>
|
|
||||||
<h2>A secret has been shared with you</h2>
|
|
||||||
|
|
||||||
{{#if name}}
|
|
||||||
<p>Secret request name: {{name}}</p>
|
|
||||||
{{/if}}
|
|
||||||
{{#if respondentUsername}}
|
|
||||||
<p>Shared by: {{respondentUsername}}</p>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
<br />
|
|
||||||
<br/>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
You can access the secret by clicking the link below.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<a href="{{secretRequestUrl}}">Access Secret</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{{emailFooter}}
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
<html>
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
|
||||||
<title>Your {{rotationType}} Rotation "{{rotationName}}" Failed to Rotate</title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<h2>Infisical</h2>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<p>{{content}}</p>
|
|
||||||
<a href="{{rotationUrl}}?search={{rotationName}}&secretPath={{secretPath}}">
|
|
||||||
View in Infisical to see the cause of failure.
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<br />
|
|
||||||
<div>
|
|
||||||
<p><strong>Name</strong>: {{rotationName}}</p>
|
|
||||||
<p><strong>Type</strong>: {{rotationType}}</p>
|
|
||||||
<p><strong>Project</strong>: {{projectName}}</p>
|
|
||||||
<p><strong>Environment</strong>: {{environment}}</p>
|
|
||||||
<p><strong>Secret Path</strong>: {{secretPath}}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{emailFooter}}
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
<html>
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
|
||||||
<title>{{syncDestination}} Sync "{{syncName}}" Failed</title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<h2>Infisical</h2>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<p>{{content}}</p>
|
|
||||||
<a href="{{syncUrl}}">
|
|
||||||
View in Infisical.
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<br />
|
|
||||||
<div>
|
|
||||||
<p><strong>Name</strong>: {{syncName}}</p>
|
|
||||||
<p><strong>Destination</strong>: {{syncDestination}}</p>
|
|
||||||
<p><strong>Project</strong>: {{projectName}}</p>
|
|
||||||
{{#if environment}}
|
|
||||||
<p><strong>Environment</strong>: {{environment}}</p>
|
|
||||||
{{/if}}
|
|
||||||
{{#if secretPath}}
|
|
||||||
<p><strong>Secret Path</strong>: {{secretPath}}</p>
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{#if failureMessage}}
|
|
||||||
<p><b>Reason: </b>{{failureMessage}}</p>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{emailFooter}}
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
|
||||||
<title>Service Token Expiring Soon</title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<h2>Service Token Expiry Notice</h2>
|
|
||||||
<p>Your service token <strong>"{{tokenName}}"</strong> will expire within 24 hours.</p>
|
|
||||||
|
|
||||||
<p>This token is currently being used on project "{{projectName}}". If this token is still needed for your workflow, please create a new one before it expires.</p>
|
|
||||||
|
|
||||||
<a href="{{url}}">Create New Token</a>
|
|
||||||
|
|
||||||
{{emailFooter}}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
<html>
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
|
||||||
<title>Code</title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<h2>Confirm your email address</h2>
|
|
||||||
<p>Your confirmation code is below — enter it in the browser window where you've started signing up for Infisical.</p>
|
|
||||||
<h1>{{code}}</h1>
|
|
||||||
<p>Questions about setting up Infisical?
|
|
||||||
{{#if isCloud}}Email us at [email protected]{{else}}Contact your administrator{{/if}}.</p>
|
|
||||||
|
|
||||||
{{emailFooter}}
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
<html>
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
|
||||||
<title>Your Infisical account has been locked</title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<h2>Unlock your Infisical account</h2>
|
|
||||||
<p>Your account has been temporarily locked due to multiple failed login attempts. </h2>
|
|
||||||
<a href="{{callback_url}}?token={{token}}">To unlock your account, follow the link here</a>
|
|
||||||
<p>If these attempts were not made by you, reset your password immediately.</p>
|
|
||||||
|
|
||||||
{{emailFooter}}
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
|
||||||
<title>Project Invitation</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h2>Join your team on Infisical</h2>
|
|
||||||
<p>You have been invited to a new Infisical project named {{workspaceName}}</p>
|
|
||||||
<a href="{{callback_url}}">Click to join</a>
|
|
||||||
<h3>What is Infisical?</h3>
|
|
||||||
<p>Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets
|
|
||||||
and configs.</p>
|
|
||||||
|
|
||||||
{{emailFooter}}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -172,8 +172,8 @@ export const superAdminServiceFactory = ({
|
|||||||
|
|
||||||
const canServerAdminAccessAfterApply =
|
const canServerAdminAccessAfterApply =
|
||||||
data.enabledLoginMethods.some((loginMethod) =>
|
data.enabledLoginMethods.some((loginMethod) =>
|
||||||
loginMethodToAuthMethod[loginMethod as LoginMethod].some(
|
loginMethodToAuthMethod[loginMethod as LoginMethod].some((authMethod) =>
|
||||||
(authMethod) => superAdminUser.authMethods?.includes(authMethod)
|
superAdminUser.authMethods?.includes(authMethod)
|
||||||
)
|
)
|
||||||
) ||
|
) ||
|
||||||
isUserSamlAccessEnabled ||
|
isUserSamlAccessEnabled ||
|
||||||
|
|||||||
@@ -25,7 +25,8 @@
|
|||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@app/*": ["./src/*"]
|
"@app/*": ["./src/*"]
|
||||||
}
|
},
|
||||||
|
"jsx": "react-jsx"
|
||||||
},
|
},
|
||||||
"include": ["src/**/*", "scripts/**/*", "e2e-test/**/*", "./.eslintrc.js", "./tsup.config.js"],
|
"include": ["src/**/*", "scripts/**/*", "e2e-test/**/*", "./.eslintrc.js", "./tsup.config.js"],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules"]
|
||||||
|
|||||||
+12
-2
@@ -2,8 +2,8 @@
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
|
||||||
import fs from "fs/promises";
|
import fs from "fs/promises";
|
||||||
import { replaceTscAliasPaths } from "tsc-alias";
|
import {replaceTscAliasPaths} from "tsc-alias";
|
||||||
import { defineConfig } from "tsup";
|
import {defineConfig} from "tsup";
|
||||||
|
|
||||||
// Instead of using tsx or tsc for building, consider using tsup.
|
// Instead of using tsx or tsc for building, consider using tsup.
|
||||||
// TSX serves as an alternative to Node.js, allowing you to build directly on the Node.js runtime.
|
// TSX serves as an alternative to Node.js, allowing you to build directly on the Node.js runtime.
|
||||||
@@ -49,8 +49,18 @@ export default defineConfig({
|
|||||||
|
|
||||||
const isFile = await fs
|
const isFile = await fs
|
||||||
.stat(`${absPath}.ts`)
|
.stat(`${absPath}.ts`)
|
||||||
|
.then((el) => el.isFile)
|
||||||
|
.catch(async (err) => {
|
||||||
|
if (err.code === "ENOTDIR") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If .ts file doesn't exist, try checking for .tsx file
|
||||||
|
return fs
|
||||||
|
.stat(`${absPath}.tsx`)
|
||||||
.then((el) => el.isFile)
|
.then((el) => el.isFile)
|
||||||
.catch((err) => err.code === "ENOTDIR");
|
.catch((err) => err.code === "ENOTDIR");
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
path: isFile ? `${args.path}.mjs` : `${args.path}/index.mjs`,
|
path: isFile ? `${args.path}.mjs` : `${args.path}/index.mjs`,
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
title: "Get Certificate Bundle"
|
||||||
|
openapi: "GET /api/v2/workspace/{slug}/bundle"
|
||||||
|
---
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
You must have the certificate `read-private-key` permission in order to call this endpoint.
|
||||||
|
</Note>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Get Certificate Private Key"
|
||||||
|
openapi: "GET /api/v2/workspace/{slug}/private-key"
|
||||||
|
---
|
||||||
@@ -73,6 +73,61 @@ Once authenticated, the Gateway establishes a secure connection with Infisical t
|
|||||||
</Warning>
|
</Warning>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
|
<Tab title="Production (Helm)">
|
||||||
|
|
||||||
|
The Gateway 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.
|
||||||
|
|
||||||
|
For production deployments on Kubernetes, install the Gateway using the Infisical Helm chart:
|
||||||
|
|
||||||
|
### Install the latest Helm Chart repository
|
||||||
|
```bash
|
||||||
|
helm repo add infisical-helm-charts 'https://dl.cloudsmith.io/public/infisical/helm-charts/helm/charts/'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update the Helm Chart repository
|
||||||
|
```bash
|
||||||
|
helm repo update
|
||||||
|
```
|
||||||
|
|
||||||
|
### Create a Kubernetes Secret with the gateway token
|
||||||
|
|
||||||
|
Create a new Kubernetes secret containing the gateway token as the `TOKEN` key. You can optionally also set the `INFISICAL_API_URL` key to your Infisical instance URL. By default, `INFISICAL_API_URL` is set to `https://app.infisical.com`.
|
||||||
|
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl create secret generic infisical-gateway-environment --from-literal=TOKEN=<your-machine-identity-access-token>
|
||||||
|
```
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
The secret name is `infisical-gateway-environment` by default. The `TOKEN` key is required, and the `INFISICAL_API_URL` key is optional.
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
### Install the Infisical Gateway Helm Chart
|
||||||
|
```bash
|
||||||
|
helm install infisical-gateway infisical-helm-charts/infisical-gateway
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check the gateway logs
|
||||||
|
After installing the gateway, you can check the logs to ensure it's running as expected.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl logs deployment/infisical-gateway
|
||||||
|
```
|
||||||
|
|
||||||
|
You should see the following output which indicates the gateway is running as expected.
|
||||||
|
```bash
|
||||||
|
$ kubectl logs deployment/infisical-gateway
|
||||||
|
INF Provided relay port 5349. Using TLS
|
||||||
|
INF Connected with relay
|
||||||
|
INF 10.0.101.112:56735
|
||||||
|
INF Starting relay connection health check
|
||||||
|
INF Gateway started successfully
|
||||||
|
INF New connection from: 10.0.1.8:34051
|
||||||
|
INF Gateway is reachable by Infisical
|
||||||
|
```
|
||||||
|
|
||||||
|
</Tab>
|
||||||
|
|
||||||
<Tab title="Development (direct)">
|
<Tab title="Development (direct)">
|
||||||
For development or testing, you can run the Gateway directly. Log in with your machine identity and start the Gateway in one command:
|
For development or testing, you can run the Gateway directly. Log in with your machine identity and start the Gateway in one command:
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -1230,13 +1230,13 @@ To address this, we added functionality to automatically redeploy your deploymen
|
|||||||
|
|
||||||
#### Enabling Automatic Redeployment
|
#### Enabling Automatic Redeployment
|
||||||
|
|
||||||
To enable auto redeployment you simply have to add the following annotation to the deployment, statefulset, or daemonset that consumes a managed secret.
|
To enable auto redeployment you simply have to add the following annotation to the Deployment, StatefulSet, or DaemonSet that consumes a managed secret.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
secrets.infisical.com/auto-reload: "true"
|
secrets.infisical.com/auto-reload: "true"
|
||||||
```
|
```
|
||||||
|
|
||||||
<Accordion title="Deployment example with auto redeploy enabled">
|
<Accordion title="Deployment example">
|
||||||
```yaml
|
```yaml
|
||||||
apiVersion: apps/v1
|
apiVersion: apps/v1
|
||||||
kind: Deployment
|
kind: Deployment
|
||||||
@@ -1266,10 +1266,82 @@ secrets.infisical.com/auto-reload: "true"
|
|||||||
- containerPort: 80
|
- containerPort: 80
|
||||||
```
|
```
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="DaemonSet example">
|
||||||
|
```yaml
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: DaemonSet
|
||||||
|
metadata:
|
||||||
|
name: log-agent
|
||||||
|
labels:
|
||||||
|
app: log-agent
|
||||||
|
annotations:
|
||||||
|
secrets.infisical.com/auto-reload: "true" # <- redeployment annotation
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: log-agent
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: log-agent
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: log-agent
|
||||||
|
image: mycompany/log-agent:latest
|
||||||
|
envFrom:
|
||||||
|
- secretRef:
|
||||||
|
name: managed-secret # <- name of the managed secret
|
||||||
|
volumeMounts:
|
||||||
|
- name: config-volume
|
||||||
|
mountPath: /etc/config
|
||||||
|
readOnly: true
|
||||||
|
volumes:
|
||||||
|
- name: config-volume
|
||||||
|
secret:
|
||||||
|
secretName: managed-secret
|
||||||
|
```
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="StatefulSet example">
|
||||||
|
```yaml
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: StatefulSet
|
||||||
|
metadata:
|
||||||
|
name: db-worker
|
||||||
|
labels:
|
||||||
|
app: db-worker
|
||||||
|
annotations:
|
||||||
|
secrets.infisical.com/auto-reload: "true" # <- redeployment annotation
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: db-worker
|
||||||
|
serviceName: "db-worker"
|
||||||
|
replicas: 2
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: db-worker
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: db-worker
|
||||||
|
image: mycompany/db-worker:stable
|
||||||
|
env:
|
||||||
|
- name: DATABASE_PASSWORD
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: managed-secret
|
||||||
|
key: DB_PASSWORD
|
||||||
|
ports:
|
||||||
|
- containerPort: 5432
|
||||||
|
```
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
<Info>
|
<Info>
|
||||||
#### How it works
|
#### How it works
|
||||||
When a secret change occurs, the operator will check to see which deployments are using the operator-managed Kubernetes secret that received the update.
|
When a managed secret is updated, the operator checks for any Deployments, DaemonSets, or StatefulSets that consume the updated secret and have the annotation
|
||||||
Then, for each deployment that has this annotation present, a rolling update will be triggered.
|
`secrets.infisical.com/auto-reload: "true"`. For each matching workload, the operator triggers a rolling restart to ensure it picks up the latest secret values.
|
||||||
</Info>
|
</Info>
|
||||||
|
|
||||||
## Using Managed ConfigMap In Your Deployment
|
## Using Managed ConfigMap In Your Deployment
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ All final reward amounts are determined at Infisical's discretion based on impac
|
|||||||
|
|
||||||
### Out of Scope
|
### Out of Scope
|
||||||
|
|
||||||
- Social engineering or phishing
|
- Social engineering or phishing (including email hyperlink injection without code execution)
|
||||||
- Rate limiting issues on non-sensitive endpoints
|
- Rate limiting issues on non-sensitive endpoints
|
||||||
- Denial-of-service attacks that require authentication and don't impact core service availability
|
- Denial-of-service attacks that require authentication and don't impact core service availability
|
||||||
- Findings based on outdated or forked code not maintained by the Infisical team
|
- Findings based on outdated or forked code not maintained by the Infisical team
|
||||||
|
|||||||
@@ -253,8 +253,9 @@ Supports conditions and permission inversion
|
|||||||
#### Subject: `certificates`
|
#### Subject: `certificates`
|
||||||
|
|
||||||
| Action | Description |
|
| Action | Description |
|
||||||
| -------- | ----------------------------- |
|
| -------------------- | ----------------------------- |
|
||||||
| `read` | View certificates |
|
| `read` | View certificates |
|
||||||
|
| `read-private-key` | Read certificate private key |
|
||||||
| `create` | Issue new certificates |
|
| `create` | Issue new certificates |
|
||||||
| `delete` | Revoke or remove certificates |
|
| `delete` | Revoke or remove certificates |
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,19 @@ Used to configure platform-specific security and operational settings
|
|||||||
Specifies the internal port on which the application listens.
|
Specifies the internal port on which the application listens.
|
||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField query="HOST" type="string" default="localhost" optional>
|
||||||
|
Specifies the network interface Infisical will bind to when accepting incoming connections.
|
||||||
|
|
||||||
|
By default, Infisical binds to `localhost`, which restricts access to connections from the same machine.
|
||||||
|
|
||||||
|
To make the application accessible externally (e.g., for self-hosted deployments), set this to `0.0.0.0`, which tells the server to listen on all network interfaces.
|
||||||
|
|
||||||
|
Example values:
|
||||||
|
- `localhost` (default, same as `127.0.0.1`)
|
||||||
|
- `0.0.0.0` (all interfaces, accessible externally)
|
||||||
|
- `192.168.1.100` (specific interface IP)
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
<ParamField query="TELEMETRY_ENABLED" type="string" default="true" optional>
|
<ParamField query="TELEMETRY_ENABLED" type="string" default="true" optional>
|
||||||
Telemetry helps us improve Infisical but if you want to disable it you may set
|
Telemetry helps us improve Infisical but if you want to disable it you may set
|
||||||
this to `false`.
|
this to `false`.
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export { useProjectPermission } from "./ProjectPermissionContext";
|
|||||||
export type { ProjectPermissionSet, TProjectPermission } from "./types";
|
export type { ProjectPermissionSet, TProjectPermission } from "./types";
|
||||||
export {
|
export {
|
||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
|
ProjectPermissionCertificateActions,
|
||||||
ProjectPermissionCmekActions,
|
ProjectPermissionCmekActions,
|
||||||
ProjectPermissionDynamicSecretActions,
|
ProjectPermissionDynamicSecretActions,
|
||||||
ProjectPermissionGroupActions,
|
ProjectPermissionGroupActions,
|
||||||
|
|||||||
@@ -7,6 +7,14 @@ export enum ProjectPermissionActions {
|
|||||||
Delete = "delete"
|
Delete = "delete"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ProjectPermissionCertificateActions {
|
||||||
|
Read = "read",
|
||||||
|
Create = "create",
|
||||||
|
Edit = "edit",
|
||||||
|
Delete = "delete",
|
||||||
|
ReadPrivateKey = "read-private-key"
|
||||||
|
}
|
||||||
|
|
||||||
export enum ProjectPermissionSecretActions {
|
export enum ProjectPermissionSecretActions {
|
||||||
DescribeAndReadValue = "read",
|
DescribeAndReadValue = "read",
|
||||||
DescribeSecret = "describeSecret",
|
DescribeSecret = "describeSecret",
|
||||||
@@ -277,7 +285,7 @@ export type ProjectPermissionSet =
|
|||||||
)
|
)
|
||||||
]
|
]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities]
|
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.Certificates]
|
| [ProjectPermissionCertificateActions, ProjectPermissionSub.Certificates]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates]
|
| [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateAuthorities]
|
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateAuthorities]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateTemplates]
|
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateTemplates]
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export {
|
|||||||
export type { TProjectPermission } from "./ProjectPermissionContext";
|
export type { TProjectPermission } from "./ProjectPermissionContext";
|
||||||
export {
|
export {
|
||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
|
ProjectPermissionCertificateActions,
|
||||||
ProjectPermissionCmekActions,
|
ProjectPermissionCmekActions,
|
||||||
ProjectPermissionDynamicSecretActions,
|
ProjectPermissionDynamicSecretActions,
|
||||||
ProjectPermissionGroupActions,
|
ProjectPermissionGroupActions,
|
||||||
|
|||||||
@@ -72,6 +72,8 @@ export const eventToNameMap: { [K in EventType]: string } = {
|
|||||||
[EventType.DELETE_CERT]: "Delete certificate",
|
[EventType.DELETE_CERT]: "Delete certificate",
|
||||||
[EventType.REVOKE_CERT]: "Revoke certificate",
|
[EventType.REVOKE_CERT]: "Revoke certificate",
|
||||||
[EventType.GET_CERT_BODY]: "Get certificate body",
|
[EventType.GET_CERT_BODY]: "Get certificate body",
|
||||||
|
[EventType.GET_CERT_PRIVATE_KEY]: "Get certificate private key",
|
||||||
|
[EventType.GET_CERT_BUNDLE]: "Get certificate bundle",
|
||||||
[EventType.CREATE_PKI_ALERT]: "Create PKI alert",
|
[EventType.CREATE_PKI_ALERT]: "Create PKI alert",
|
||||||
[EventType.GET_PKI_ALERT]: "Get PKI alert",
|
[EventType.GET_PKI_ALERT]: "Get PKI alert",
|
||||||
[EventType.UPDATE_PKI_ALERT]: "Update PKI alert",
|
[EventType.UPDATE_PKI_ALERT]: "Update PKI alert",
|
||||||
|
|||||||
@@ -78,6 +78,8 @@ export enum EventType {
|
|||||||
DELETE_CERT = "delete-cert",
|
DELETE_CERT = "delete-cert",
|
||||||
REVOKE_CERT = "revoke-cert",
|
REVOKE_CERT = "revoke-cert",
|
||||||
GET_CERT_BODY = "get-cert-body",
|
GET_CERT_BODY = "get-cert-body",
|
||||||
|
GET_CERT_PRIVATE_KEY = "get-cert-private-key",
|
||||||
|
GET_CERT_BUNDLE = "get-cert-bundle",
|
||||||
CREATE_PKI_ALERT = "create-pki-alert",
|
CREATE_PKI_ALERT = "create-pki-alert",
|
||||||
GET_PKI_ALERT = "get-pki-alert",
|
GET_PKI_ALERT = "get-pki-alert",
|
||||||
UPDATE_PKI_ALERT = "update-pki-alert",
|
UPDATE_PKI_ALERT = "update-pki-alert",
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user