Compare commits
68 Commits
fix-dns-re
...
helm-updat
Author | SHA1 | Date | |
---|---|---|---|
|
7e432a4297 | ||
|
d4e5d2c7ed | ||
|
0c2e0bb0f9 | ||
|
e2a414ffff | ||
|
0ca3c2bb68 | ||
|
083581b51a | ||
|
40e976133c | ||
|
ad2f002822 | ||
|
8842dfe5d1 | ||
|
b1eea4ae9c | ||
|
a8e0a8aca3 | ||
|
b37058d0e2 | ||
|
334a05d5f1 | ||
|
12c813928c | ||
|
521fef6fca | ||
|
8f8236c445 | ||
|
3cf5c534ff | ||
|
2b03c295f9 | ||
|
4fc7a52941 | ||
|
0d2b3adec7 | ||
|
e695203c05 | ||
|
f9d76aae5d | ||
|
1c280759d1 | ||
|
6005dce44d | ||
|
f7f7d2d528 | ||
|
57342cf2a0 | ||
|
d530604b51 | ||
|
229c7c0dcf | ||
|
6a79830e01 | ||
|
722067f86c | ||
|
86bb2659b5 | ||
|
dc59f226b6 | ||
|
cd9792822b | ||
|
9175c1dffa | ||
|
f6e802c017 | ||
|
b6e6a3c6be | ||
|
54927454bf | ||
|
1ce06891a5 | ||
|
3a8154eddc | ||
|
1e4dfd0c7c | ||
|
34b7d28e2f | ||
|
245a348517 | ||
|
e0fc582e2e | ||
|
68ef897b6a | ||
|
1b060e76de | ||
|
9f7599b2a1 | ||
|
9cbe70a6f3 | ||
|
f49fb534ab | ||
|
6eea4c8364 | ||
|
1e206ee441 | ||
|
85c1a1081e | ||
|
877485b45a | ||
|
d13e685a81 | ||
|
9849a5f136 | ||
|
26773a1444 | ||
|
a6f280197b | ||
|
346d2f213e | ||
|
9f1ac77afa | ||
|
1268bc1238 | ||
|
07e4bc8eed | ||
|
235be96ded | ||
|
30471bfcad | ||
|
eedffffc38 | ||
|
9f487ad026 | ||
|
c70b9e665e | ||
|
d460e96052 | ||
|
e475774910 | ||
|
e81c49500b |
@@ -171,6 +171,7 @@ ENV NODE_ENV production
|
||||
ENV STANDALONE_BUILD true
|
||||
ENV STANDALONE_MODE true
|
||||
ENV ChrystokiConfigurationPath=/usr/safenet/lunaclient/
|
||||
ENV NODE_OPTIONS="--max-old-space-size=1024"
|
||||
|
||||
WORKDIR /backend
|
||||
|
||||
|
@@ -168,6 +168,7 @@ ENV HTTPS_ENABLED false
|
||||
ENV NODE_ENV production
|
||||
ENV STANDALONE_BUILD true
|
||||
ENV STANDALONE_MODE true
|
||||
ENV NODE_OPTIONS="--max-old-space-size=1024"
|
||||
|
||||
WORKDIR /backend
|
||||
|
||||
|
@@ -1,4 +1,8 @@
|
||||
import RE2 from "re2";
|
||||
|
||||
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||
import { applyJitter } from "@app/lib/dates";
|
||||
import { delay as delayMs } from "@app/lib/delay";
|
||||
import { Lock } from "@app/lib/red-lock";
|
||||
|
||||
export const mockKeyStore = (): TKeyStoreFactory => {
|
||||
@@ -18,6 +22,27 @@ export const mockKeyStore = (): TKeyStoreFactory => {
|
||||
delete store[key];
|
||||
return 1;
|
||||
},
|
||||
deleteItems: async ({ pattern, batchSize = 500, delay = 1500, jitter = 200 }) => {
|
||||
const regex = new RE2(`^${pattern.replace(/[-[\]/{}()+?.\\^$|]/g, "\\$&").replace(/\*/g, ".*")}$`);
|
||||
let totalDeleted = 0;
|
||||
const keys = Object.keys(store);
|
||||
|
||||
for (let i = 0; i < keys.length; i += batchSize) {
|
||||
const batch = keys.slice(i, i + batchSize);
|
||||
|
||||
for (const key of batch) {
|
||||
if (regex.test(key)) {
|
||||
delete store[key];
|
||||
totalDeleted += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await delayMs(Math.max(0, applyJitter(delay, jitter)));
|
||||
}
|
||||
|
||||
return totalDeleted;
|
||||
},
|
||||
getItem: async (key) => {
|
||||
const value = store[key];
|
||||
if (typeof value === "string") {
|
||||
|
@@ -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(),
|
||||
updatedAt: z.date(),
|
||||
certId: z.string().uuid(),
|
||||
encryptedCertificate: zodBuffer
|
||||
encryptedCertificate: zodBuffer,
|
||||
encryptedCertificateChain: zodBuffer.nullable().optional()
|
||||
});
|
||||
|
||||
export type TCertificateBodies = z.infer<typeof CertificateBodiesSchema>;
|
||||
|
@@ -5,6 +5,8 @@
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { zodBuffer } from "@app/lib/zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const CertificateSecretsSchema = z.object({
|
||||
@@ -12,8 +14,7 @@ export const CertificateSecretsSchema = z.object({
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
certId: z.string().uuid(),
|
||||
pk: z.string(),
|
||||
sk: z.string()
|
||||
encryptedPrivateKey: zodBuffer
|
||||
});
|
||||
|
||||
export type TCertificateSecrets = z.infer<typeof CertificateSecretsSchema>;
|
||||
|
@@ -27,7 +27,7 @@ export const ProjectsSchema = z.object({
|
||||
description: z.string().nullable().optional(),
|
||||
type: z.string(),
|
||||
enforceCapitalization: z.boolean().default(false),
|
||||
hasDeleteProtection: z.boolean().default(true).nullable().optional()
|
||||
hasDeleteProtection: z.boolean().default(false).nullable().optional()
|
||||
});
|
||||
|
||||
export type TProjects = z.infer<typeof ProjectsSchema>;
|
||||
|
@@ -1,11 +1,11 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { GitAppOrgSchema, SecretScanningGitRisksSchema } from "@app/db/schemas";
|
||||
import { canUseSecretScanning } from "@app/ee/services/secret-scanning/secret-scanning-fns";
|
||||
import {
|
||||
SecretScanningResolvedStatus,
|
||||
SecretScanningRiskStatus
|
||||
} from "@app/ee/services/secret-scanning/secret-scanning-types";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { OrderByDirection } from "@app/lib/types";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
@@ -23,14 +23,14 @@ export const registerSecretScanningRouter = async (server: FastifyZodProvider) =
|
||||
body: z.object({ organizationId: z.string().trim() }),
|
||||
response: {
|
||||
200: z.object({
|
||||
sessionId: z.string()
|
||||
sessionId: z.string(),
|
||||
gitAppSlug: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const appCfg = getConfig();
|
||||
if (!appCfg.SECRET_SCANNING_ORG_WHITELIST?.includes(req.auth.orgId)) {
|
||||
if (!canUseSecretScanning(req.auth.orgId)) {
|
||||
throw new BadRequestError({
|
||||
message: "Secret scanning is temporarily unavailable."
|
||||
});
|
||||
|
@@ -224,6 +224,8 @@ export enum EventType {
|
||||
DELETE_CERT = "delete-cert",
|
||||
REVOKE_CERT = "revoke-cert",
|
||||
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",
|
||||
GET_PKI_ALERT = "get-pki-alert",
|
||||
UPDATE_PKI_ALERT = "update-pki-alert",
|
||||
@@ -1790,6 +1792,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 {
|
||||
type: EventType.CREATE_PKI_ALERT;
|
||||
metadata: {
|
||||
@@ -2824,6 +2844,8 @@ export type Event =
|
||||
| DeleteCert
|
||||
| RevokeCert
|
||||
| GetCertBody
|
||||
| GetCertPrivateKey
|
||||
| GetCertBundle
|
||||
| CreatePkiAlert
|
||||
| GetPkiAlert
|
||||
| UpdatePkiAlert
|
||||
|
@@ -17,6 +17,14 @@ export enum ProjectPermissionActions {
|
||||
Delete = "delete"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionCertificateActions {
|
||||
Read = "read",
|
||||
Create = "create",
|
||||
Edit = "edit",
|
||||
Delete = "delete",
|
||||
ReadPrivateKey = "read-private-key"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionSecretActions {
|
||||
DescribeAndReadValue = "read",
|
||||
DescribeSecret = "describeSecret",
|
||||
@@ -232,7 +240,7 @@ export type ProjectPermissionSet =
|
||||
ProjectPermissionSub.Identity | (ForcedSubject<ProjectPermissionSub.Identity> & IdentityManagementSubjectFields)
|
||||
]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Certificates]
|
||||
| [ProjectPermissionCertificateActions, ProjectPermissionSub.Certificates]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateAuthorities]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificates]
|
||||
@@ -478,7 +486,7 @@ const GeneralPermissionSchema = [
|
||||
}),
|
||||
z.object({
|
||||
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."
|
||||
)
|
||||
}),
|
||||
@@ -688,7 +696,6 @@ const buildAdminPermissionRules = () => {
|
||||
ProjectPermissionSub.AuditLogs,
|
||||
ProjectPermissionSub.IpAllowList,
|
||||
ProjectPermissionSub.CertificateAuthorities,
|
||||
ProjectPermissionSub.Certificates,
|
||||
ProjectPermissionSub.CertificateTemplates,
|
||||
ProjectPermissionSub.PkiAlerts,
|
||||
ProjectPermissionSub.PkiCollections,
|
||||
@@ -708,6 +715,17 @@ const buildAdminPermissionRules = () => {
|
||||
);
|
||||
});
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionCertificateActions.Read,
|
||||
ProjectPermissionCertificateActions.Edit,
|
||||
ProjectPermissionCertificateActions.Create,
|
||||
ProjectPermissionCertificateActions.Delete,
|
||||
ProjectPermissionCertificateActions.ReadPrivateKey
|
||||
],
|
||||
ProjectPermissionSub.Certificates
|
||||
);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionSshHostActions.Edit,
|
||||
@@ -965,10 +983,10 @@ const buildMemberPermissionRules = () => {
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionActions.Delete
|
||||
ProjectPermissionCertificateActions.Read,
|
||||
ProjectPermissionCertificateActions.Edit,
|
||||
ProjectPermissionCertificateActions.Create,
|
||||
ProjectPermissionCertificateActions.Delete
|
||||
],
|
||||
ProjectPermissionSub.Certificates
|
||||
);
|
||||
@@ -1041,7 +1059,7 @@ const buildViewerPermissionRules = () => {
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
|
||||
can(ProjectPermissionCertificateActions.Read, ProjectPermissionSub.Certificates);
|
||||
can(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificates);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateTemplates);
|
||||
|
@@ -0,0 +1,11 @@
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
|
||||
export const canUseSecretScanning = (orgId: string) => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
if (!appCfg.isCloud) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return appCfg.SECRET_SCANNING_ORG_WHITELIST?.includes(orgId);
|
||||
};
|
@@ -12,6 +12,7 @@ import { NotFoundError } from "@app/lib/errors";
|
||||
import { TGitAppDALFactory } from "./git-app-dal";
|
||||
import { TGitAppInstallSessionDALFactory } from "./git-app-install-session-dal";
|
||||
import { TSecretScanningDALFactory } from "./secret-scanning-dal";
|
||||
import { canUseSecretScanning } from "./secret-scanning-fns";
|
||||
import { TSecretScanningQueueFactory } from "./secret-scanning-queue";
|
||||
import {
|
||||
SecretScanningRiskStatus,
|
||||
@@ -47,12 +48,14 @@ export const secretScanningServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TInstallAppSessionDTO) => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.SecretScanning);
|
||||
|
||||
const sessionId = crypto.randomBytes(16).toString("hex");
|
||||
await gitAppInstallSessionDAL.upsert({ orgId, sessionId, userId: actorId });
|
||||
return { sessionId };
|
||||
return { sessionId, gitAppSlug: appCfg.SECRET_SCANNING_GIT_APP_SLUG };
|
||||
};
|
||||
|
||||
const linkInstallationToOrg = async ({
|
||||
@@ -91,7 +94,8 @@ export const secretScanningServiceFactory = ({
|
||||
const {
|
||||
data: { repositories }
|
||||
} = await octokit.apps.listReposAccessibleToInstallation();
|
||||
if (appCfg.SECRET_SCANNING_ORG_WHITELIST?.includes(actorOrgId)) {
|
||||
|
||||
if (canUseSecretScanning(actorOrgId)) {
|
||||
await Promise.all(
|
||||
repositories.map(({ id, full_name }) =>
|
||||
secretScanningQueue.startFullRepoScan({
|
||||
@@ -102,6 +106,7 @@ export const secretScanningServiceFactory = ({
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return { installatedApp };
|
||||
};
|
||||
|
||||
@@ -164,7 +169,6 @@ export const secretScanningServiceFactory = ({
|
||||
};
|
||||
|
||||
const handleRepoPushEvent = async (payload: WebhookEventMap["push"]) => {
|
||||
const appCfg = getConfig();
|
||||
const { commits, repository, installation, pusher } = payload;
|
||||
if (!commits || !repository || !installation || !pusher) {
|
||||
return;
|
||||
@@ -175,7 +179,7 @@ export const secretScanningServiceFactory = ({
|
||||
});
|
||||
if (!installationLink) return;
|
||||
|
||||
if (appCfg.SECRET_SCANNING_ORG_WHITELIST?.includes(installationLink.orgId)) {
|
||||
if (canUseSecretScanning(installationLink.orgId)) {
|
||||
await secretScanningQueue.startPushEventScan({
|
||||
commits,
|
||||
pusher: { name: pusher.name, email: pusher.email },
|
||||
|
@@ -1,6 +1,8 @@
|
||||
import { Redis } from "ioredis";
|
||||
|
||||
import { pgAdvisoryLockHashText } from "@app/lib/crypto/hashtext";
|
||||
import { applyJitter } from "@app/lib/dates";
|
||||
import { delay as delayMs } from "@app/lib/delay";
|
||||
import { Redlock, Settings } from "@app/lib/red-lock";
|
||||
|
||||
export const PgSqlLock = {
|
||||
@@ -48,6 +50,13 @@ export const KeyStoreTtls = {
|
||||
AccessTokenStatusUpdateInSeconds: 120
|
||||
};
|
||||
|
||||
type TDeleteItems = {
|
||||
pattern: string;
|
||||
batchSize?: number;
|
||||
delay?: number;
|
||||
jitter?: number;
|
||||
};
|
||||
|
||||
type TWaitTillReady = {
|
||||
key: string;
|
||||
waitingCb?: () => void;
|
||||
@@ -75,6 +84,35 @@ export const keyStoreFactory = (redisUrl: string) => {
|
||||
|
||||
const deleteItem = async (key: string) => redis.del(key);
|
||||
|
||||
const deleteItems = async ({ pattern, batchSize = 500, delay = 1500, jitter = 200 }: TDeleteItems) => {
|
||||
let cursor = "0";
|
||||
let totalDeleted = 0;
|
||||
|
||||
do {
|
||||
// Await in loop is needed so that Redis is not overwhelmed
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const [nextCursor, keys] = await redis.scan(cursor, "MATCH", pattern, "COUNT", 1000); // Count should be 1000 - 5000 for prod loads
|
||||
cursor = nextCursor;
|
||||
|
||||
for (let i = 0; i < keys.length; i += batchSize) {
|
||||
const batch = keys.slice(i, i + batchSize);
|
||||
const pipeline = redis.pipeline();
|
||||
for (const key of batch) {
|
||||
pipeline.unlink(key);
|
||||
}
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await pipeline.exec();
|
||||
totalDeleted += batch.length;
|
||||
console.log("BATCH DONE");
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await delayMs(Math.max(0, applyJitter(delay, jitter)));
|
||||
}
|
||||
} while (cursor !== "0");
|
||||
|
||||
return totalDeleted;
|
||||
};
|
||||
|
||||
const incrementBy = async (key: string, value: number) => redis.incrby(key, value);
|
||||
|
||||
const setExpiry = async (key: string, expiryInSeconds: number) => redis.expire(key, expiryInSeconds);
|
||||
@@ -94,7 +132,7 @@ export const keyStoreFactory = (redisUrl: string) => {
|
||||
// eslint-disable-next-line
|
||||
await new Promise((resolve) => {
|
||||
waitingCb?.();
|
||||
setTimeout(resolve, Math.max(0, delay + Math.floor((Math.random() * 2 - 1) * jitter)));
|
||||
setTimeout(resolve, Math.max(0, applyJitter(delay, jitter)));
|
||||
});
|
||||
attempts += 1;
|
||||
// eslint-disable-next-line
|
||||
@@ -108,6 +146,7 @@ export const keyStoreFactory = (redisUrl: string) => {
|
||||
setExpiry,
|
||||
setItemWithExpiry,
|
||||
deleteItem,
|
||||
deleteItems,
|
||||
incrementBy,
|
||||
acquireLock(resources: string[], duration: number, settings?: Partial<Settings>) {
|
||||
return redisLock.acquire(resources, duration, settings);
|
||||
|
@@ -1,3 +1,7 @@
|
||||
import RE2 from "re2";
|
||||
|
||||
import { applyJitter } from "@app/lib/dates";
|
||||
import { delay as delayMs } from "@app/lib/delay";
|
||||
import { Lock } from "@app/lib/red-lock";
|
||||
|
||||
import { TKeyStoreFactory } from "./keystore";
|
||||
@@ -19,6 +23,27 @@ export const inMemoryKeyStore = (): TKeyStoreFactory => {
|
||||
delete store[key];
|
||||
return 1;
|
||||
},
|
||||
deleteItems: async ({ pattern, batchSize = 500, delay = 1500, jitter = 200 }) => {
|
||||
const regex = new RE2(`^${pattern.replace(/[-[\]/{}()+?.\\^$|]/g, "\\$&").replace(/\*/g, ".*")}$`);
|
||||
let totalDeleted = 0;
|
||||
const keys = Object.keys(store);
|
||||
|
||||
for (let i = 0; i < keys.length; i += batchSize) {
|
||||
const batch = keys.slice(i, i + batchSize);
|
||||
|
||||
for (const key of batch) {
|
||||
if (regex.test(key)) {
|
||||
delete store[key];
|
||||
totalDeleted += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await delayMs(Math.max(0, applyJitter(delay, jitter)));
|
||||
}
|
||||
|
||||
return totalDeleted;
|
||||
},
|
||||
getItem: async (key) => {
|
||||
const value = store[key];
|
||||
if (typeof value === "string") {
|
||||
|
@@ -1619,7 +1619,8 @@ export const CERTIFICATES = {
|
||||
serialNumber: "The serial number of the certificate to get the certificate body and certificate chain for.",
|
||||
certificate: "The certificate body 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."
|
||||
}
|
||||
};
|
||||
|
||||
|
@@ -146,6 +146,7 @@ const envSchema = z
|
||||
SECRET_SCANNING_GIT_APP_ID: zpStr(z.string().optional()),
|
||||
SECRET_SCANNING_PRIVATE_KEY: zpStr(z.string().optional()),
|
||||
SECRET_SCANNING_ORG_WHITELIST: zpStr(z.string().optional()),
|
||||
SECRET_SCANNING_GIT_APP_SLUG: zpStr(z.string().default("infisical-radar")),
|
||||
// LICENSE
|
||||
LICENSE_SERVER_URL: zpStr(z.string().optional().default("https://portal.infisical.com")),
|
||||
LICENSE_SERVER_KEY: zpStr(z.string().optional()),
|
||||
|
4
backend/src/lib/delay/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export const delay = (ms: number) =>
|
||||
new Promise<void>((resolve) => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
@@ -25,6 +25,7 @@ import {
|
||||
TQueueSecretSyncSyncSecretsByIdDTO,
|
||||
TQueueSendSecretSyncActionFailedNotificationsDTO
|
||||
} from "@app/services/secret-sync/secret-sync-types";
|
||||
import { CacheType } from "@app/services/super-admin/super-admin-types";
|
||||
import { TWebhookPayloads } from "@app/services/webhook/webhook-types";
|
||||
|
||||
export enum QueueName {
|
||||
@@ -49,7 +50,8 @@ export enum QueueName {
|
||||
AccessTokenStatusUpdate = "access-token-status-update",
|
||||
ImportSecretsFromExternalSource = "import-secrets-from-external-source",
|
||||
AppConnectionSecretSync = "app-connection-secret-sync",
|
||||
SecretRotationV2 = "secret-rotation-v2"
|
||||
SecretRotationV2 = "secret-rotation-v2",
|
||||
InvalidateCache = "invalidate-cache"
|
||||
}
|
||||
|
||||
export enum QueueJobs {
|
||||
@@ -81,7 +83,8 @@ export enum QueueJobs {
|
||||
SecretSyncSendActionFailedNotifications = "secret-sync-send-action-failed-notifications",
|
||||
SecretRotationV2QueueRotations = "secret-rotation-v2-queue-rotations",
|
||||
SecretRotationV2RotateSecrets = "secret-rotation-v2-rotate-secrets",
|
||||
SecretRotationV2SendNotification = "secret-rotation-v2-send-notification"
|
||||
SecretRotationV2SendNotification = "secret-rotation-v2-send-notification",
|
||||
InvalidateCache = "invalidate-cache"
|
||||
}
|
||||
|
||||
export type TQueueJobTypes = {
|
||||
@@ -234,6 +237,14 @@ export type TQueueJobTypes = {
|
||||
name: QueueJobs.SecretRotationV2SendNotification;
|
||||
payload: TSecretRotationSendNotificationJobPayload;
|
||||
};
|
||||
[QueueName.InvalidateCache]: {
|
||||
name: QueueJobs.InvalidateCache;
|
||||
payload: {
|
||||
data: {
|
||||
type: CacheType;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export type TQueueServiceFactory = ReturnType<typeof queueServiceFactory>;
|
||||
|
@@ -100,3 +100,10 @@ export const publicSshCaLimit: RateLimitOptions = {
|
||||
max: 30, // conservative default
|
||||
keyGenerator: (req) => req.realIp
|
||||
};
|
||||
|
||||
export const invalidateCacheLimit: RateLimitOptions = {
|
||||
timeWindow: 60 * 1000,
|
||||
hook: "preValidation",
|
||||
max: 1,
|
||||
keyGenerator: (req) => req.realIp
|
||||
};
|
||||
|
8
backend/src/server/lib/caching.ts
Normal file
@@ -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 { certificateBodyDALFactory } from "@app/services/certificate/certificate-body-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 { certificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
|
||||
import { certificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
|
||||
@@ -241,6 +242,7 @@ import { projectSlackConfigDALFactory } from "@app/services/slack/project-slack-
|
||||
import { slackIntegrationDALFactory } from "@app/services/slack/slack-integration-dal";
|
||||
import { slackServiceFactory } from "@app/services/slack/slack-service";
|
||||
import { TSmtpService } from "@app/services/smtp/smtp-service";
|
||||
import { invalidateCacheQueueFactory } from "@app/services/super-admin/invalidate-cache-queue";
|
||||
import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
|
||||
import { getServerCfg, superAdminServiceFactory } from "@app/services/super-admin/super-admin-service";
|
||||
import { telemetryDALFactory } from "@app/services/telemetry/telemetry-dal";
|
||||
@@ -610,6 +612,11 @@ export const registerRoutes = async (
|
||||
queueService
|
||||
});
|
||||
|
||||
const invalidateCacheQueue = invalidateCacheQueueFactory({
|
||||
keyStore,
|
||||
queueService
|
||||
});
|
||||
|
||||
const userService = userServiceFactory({
|
||||
userDAL,
|
||||
userAliasDAL,
|
||||
@@ -721,7 +728,8 @@ export const registerRoutes = async (
|
||||
keyStore,
|
||||
licenseService,
|
||||
kmsService,
|
||||
microsoftTeamsService
|
||||
microsoftTeamsService,
|
||||
invalidateCacheQueue
|
||||
});
|
||||
|
||||
const orgAdminService = orgAdminServiceFactory({
|
||||
@@ -812,6 +820,7 @@ export const registerRoutes = async (
|
||||
|
||||
const certificateDAL = certificateDALFactory(db);
|
||||
const certificateBodyDAL = certificateBodyDALFactory(db);
|
||||
const certificateSecretDAL = certificateSecretDALFactory(db);
|
||||
|
||||
const pkiAlertDAL = pkiAlertDALFactory(db);
|
||||
const pkiCollectionDAL = pkiCollectionDALFactory(db);
|
||||
@@ -820,6 +829,7 @@ export const registerRoutes = async (
|
||||
const certificateService = certificateServiceFactory({
|
||||
certificateDAL,
|
||||
certificateBodyDAL,
|
||||
certificateSecretDAL,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
certificateAuthorityCrlDAL,
|
||||
@@ -891,6 +901,7 @@ export const registerRoutes = async (
|
||||
certificateAuthorityQueue,
|
||||
certificateDAL,
|
||||
certificateBodyDAL,
|
||||
certificateSecretDAL,
|
||||
pkiCollectionDAL,
|
||||
pkiCollectionItemDAL,
|
||||
projectDAL,
|
||||
|
@@ -4,13 +4,14 @@ import { z } from "zod";
|
||||
import { IdentitiesSchema, OrganizationsSchema, SuperAdminSchema, UsersSchema } from "@app/db/schemas";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { invalidateCacheLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||
import { verifySuperAdmin } from "@app/server/plugins/auth/superAdmin";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { RootKeyEncryptionStrategy } from "@app/services/kms/kms-types";
|
||||
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
||||
import { LoginMethod } from "@app/services/super-admin/super-admin-types";
|
||||
import { CacheType, LoginMethod } from "@app/services/super-admin/super-admin-types";
|
||||
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
||||
|
||||
export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
||||
@@ -548,4 +549,69 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/invalidate-cache",
|
||||
config: {
|
||||
rateLimit: invalidateCacheLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
type: z.nativeEnum(CacheType)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
message: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: (req, res, done) => {
|
||||
verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => {
|
||||
verifySuperAdmin(req, res, done);
|
||||
});
|
||||
},
|
||||
handler: async (req) => {
|
||||
await server.services.superAdmin.invalidateCache(req.body.type);
|
||||
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.InvalidateCache,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
properties: {
|
||||
...req.auditLogInfo
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
message: "Cache invalidation job started"
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/invalidating-cache-status",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
response: {
|
||||
200: z.object({
|
||||
invalidating: z.boolean()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: (req, res, done) => {
|
||||
verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => {
|
||||
verifySuperAdmin(req, res, done);
|
||||
});
|
||||
},
|
||||
handler: async () => {
|
||||
const invalidating = await server.services.superAdmin.checkIfInvalidatingCache();
|
||||
|
||||
return {
|
||||
invalidating
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-floating-promises */
|
||||
import { z } from "zod";
|
||||
|
||||
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 { ms } from "@app/lib/ms";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { addNoCacheHeaders } from "@app/server/lib/caching";
|
||||
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
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({
|
||||
method: "POST",
|
||||
url: "/issue-certificate",
|
||||
@@ -411,7 +518,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
response: {
|
||||
200: z.object({
|
||||
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)
|
||||
})
|
||||
}
|
||||
@@ -429,7 +536,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
...req.auditLogInfo,
|
||||
projectId: ca.projectId,
|
||||
event: {
|
||||
type: EventType.DELETE_CERT,
|
||||
type: EventType.GET_CERT_BODY,
|
||||
metadata: {
|
||||
certId: cert.id,
|
||||
cn: cert.commonName,
|
||||
|
@@ -170,7 +170,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
.optional()
|
||||
.default(InfisicalProjectTemplate.Default)
|
||||
.describe(PROJECTS.CREATE.template),
|
||||
type: z.nativeEnum(ProjectType).default(ProjectType.SecretManager)
|
||||
type: z.nativeEnum(ProjectType).default(ProjectType.SecretManager),
|
||||
shouldCreateDefaultEnvs: z.boolean().optional().default(true)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@@ -190,7 +191,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
slug: req.body.slug,
|
||||
kmsKeyId: req.body.kmsKeyId,
|
||||
template: req.body.template,
|
||||
type: req.body.type
|
||||
type: req.body.type,
|
||||
createDefaultEnvs: req.body.shouldCreateDefaultEnvs
|
||||
});
|
||||
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
@@ -272,7 +274,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
slug: slugSchema({ min: 5, max: 36 }).describe("The slug of the project to get.")
|
||||
slug: slugSchema({ max: 36 }).describe("The slug of the project to get.")
|
||||
}),
|
||||
response: {
|
||||
200: projectWithEnv
|
||||
|
@@ -6,7 +6,11 @@ import { z } from "zod";
|
||||
|
||||
import { ActionProjectType, ProjectType, TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas";
|
||||
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 { getConfig } from "@app/lib/config/env";
|
||||
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 { TCertificateAuthorityCrlDALFactory } from "../../ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
||||
import { TCertificateSecretDALFactory } from "../certificate/certificate-secret-dal";
|
||||
import {
|
||||
CertExtendedKeyUsage,
|
||||
CertExtendedKeyUsageOIDToName,
|
||||
@@ -75,6 +80,7 @@ type TCertificateAuthorityServiceFactoryDep = {
|
||||
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "getById" | "find">;
|
||||
certificateAuthorityQueue: TCertificateAuthorityQueueFactory; // TODO: Pick
|
||||
certificateDAL: Pick<TCertificateDALFactory, "transaction" | "create" | "find">;
|
||||
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "create">;
|
||||
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
|
||||
pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "findById">;
|
||||
pkiCollectionItemDAL: Pick<TPkiCollectionItemDALFactory, "create">;
|
||||
@@ -96,6 +102,7 @@ export const certificateAuthorityServiceFactory = ({
|
||||
certificateTemplateDAL,
|
||||
certificateDAL,
|
||||
certificateBodyDAL,
|
||||
certificateSecretDAL,
|
||||
pkiCollectionDAL,
|
||||
pkiCollectionItemDAL,
|
||||
projectDAL,
|
||||
@@ -1157,7 +1164,10 @@ export const certificateAuthorityServiceFactory = ({
|
||||
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.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" });
|
||||
@@ -1373,6 +1383,23 @@ export const certificateAuthorityServiceFactory = ({
|
||||
const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
|
||||
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) => {
|
||||
const cert = await certificateDAL.create(
|
||||
@@ -1396,7 +1423,16 @@ export const certificateAuthorityServiceFactory = ({
|
||||
await certificateBodyDAL.create(
|
||||
{
|
||||
certId: cert.id,
|
||||
encryptedCertificate
|
||||
encryptedCertificate,
|
||||
encryptedCertificateChain
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
await certificateSecretDAL.create(
|
||||
{
|
||||
certId: cert.id,
|
||||
encryptedPrivateKey
|
||||
},
|
||||
tx
|
||||
);
|
||||
@@ -1414,17 +1450,9 @@ export const certificateAuthorityServiceFactory = ({
|
||||
return cert;
|
||||
});
|
||||
|
||||
const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({
|
||||
caCertId: caCert.id,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
|
||||
return {
|
||||
certificate: leafCert.toString("pem"),
|
||||
certificateChain: `${issuingCaCertificate}\n${caCertChain}`.trim(),
|
||||
certificateChain: certificateChainPem,
|
||||
issuingCaCertificate,
|
||||
privateKey: skLeaf,
|
||||
serialNumber,
|
||||
@@ -1487,7 +1515,7 @@ export const certificateAuthorityServiceFactory = ({
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionCertificateActions.Create,
|
||||
ProjectPermissionSub.Certificates
|
||||
);
|
||||
}
|
||||
|
@@ -1,6 +1,11 @@
|
||||
import crypto from "node:crypto";
|
||||
|
||||
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) => {
|
||||
switch (crlReason) {
|
||||
@@ -46,3 +51,73 @@ export const constructPemChainFromCerts = (certificates: x509.X509Certificate[])
|
||||
.map((cert) => cert.toString("pem"))
|
||||
.join("\n")
|
||||
.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;
|
||||
};
|
||||
|
10
backend/src/services/certificate/certificate-secret-dal.ts
Normal file
@@ -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 { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
||||
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 { TCertificateDALFactory } from "@app/services/certificate/certificate-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 { getCaCertChain, rebuildCaCrl } from "../certificate-authority/certificate-authority-fns";
|
||||
import { revocationReasonToCrlCode } from "./certificate-fns";
|
||||
import { CertStatus, TDeleteCertDTO, TGetCertBodyDTO, TGetCertDTO, TRevokeCertDTO } from "./certificate-types";
|
||||
import { buildCertificateChain, getCertificateCredentials, revocationReasonToCrlCode } from "./certificate-fns";
|
||||
import { TCertificateSecretDALFactory } from "./certificate-secret-dal";
|
||||
import {
|
||||
CertStatus,
|
||||
TDeleteCertDTO,
|
||||
TGetCertBodyDTO,
|
||||
TGetCertBundleDTO,
|
||||
TGetCertDTO,
|
||||
TGetCertPrivateKeyDTO,
|
||||
TRevokeCertDTO
|
||||
} from "./certificate-types";
|
||||
|
||||
type TCertificateServiceFactoryDep = {
|
||||
certificateDAL: Pick<TCertificateDALFactory, "findOne" | "deleteById" | "update" | "find">;
|
||||
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne">;
|
||||
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "findOne">;
|
||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
|
||||
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findById">;
|
||||
@@ -34,6 +47,7 @@ export type TCertificateServiceFactory = ReturnType<typeof certificateServiceFac
|
||||
|
||||
export const certificateServiceFactory = ({
|
||||
certificateDAL,
|
||||
certificateSecretDAL,
|
||||
certificateBodyDAL,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
@@ -59,7 +73,10 @@ export const certificateServiceFactory = ({
|
||||
actionProjectType: ActionProjectType.CertificateManager
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionCertificateActions.Read,
|
||||
ProjectPermissionSub.Certificates
|
||||
);
|
||||
|
||||
return {
|
||||
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]
|
||||
*/
|
||||
@@ -83,7 +142,10 @@ export const certificateServiceFactory = ({
|
||||
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);
|
||||
|
||||
@@ -118,7 +180,10 @@ export const certificateServiceFactory = ({
|
||||
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");
|
||||
|
||||
@@ -165,7 +230,10 @@ export const certificateServiceFactory = ({
|
||||
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 });
|
||||
|
||||
@@ -192,19 +260,107 @@ export const certificateServiceFactory = ({
|
||||
kmsService
|
||||
});
|
||||
|
||||
const certificateChain = await buildCertificateChain({
|
||||
caCert,
|
||||
caCertChain,
|
||||
kmsId: certificateManagerKeyId,
|
||||
kmsService,
|
||||
encryptedCertificateChain: certBody.encryptedCertificateChain || undefined
|
||||
});
|
||||
|
||||
return {
|
||||
certificate: certObj.toString("pem"),
|
||||
certificateChain: `${caCert}\n${caCertChain}`.trim(),
|
||||
certificateChain,
|
||||
serialNumber: certObj.serialNumber,
|
||||
cert,
|
||||
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 {
|
||||
getCert,
|
||||
getCertPrivateKey,
|
||||
deleteCert,
|
||||
revokeCert,
|
||||
getCertBody
|
||||
getCertBody,
|
||||
getCertBundle
|
||||
};
|
||||
};
|
||||
|
@@ -2,6 +2,10 @@ import * as x509 from "@peculiar/x509";
|
||||
|
||||
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 {
|
||||
ACTIVE = "active",
|
||||
REVOKED = "revoked"
|
||||
@@ -73,3 +77,27 @@ export type TRevokeCertDTO = {
|
||||
export type TGetCertBodyDTO = {
|
||||
serialNumber: string;
|
||||
} & 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;
|
||||
};
|
||||
|
@@ -14,6 +14,7 @@ import { throwIfMissingSecretReadValueOrDescribePermission } from "@app/ee/servi
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionCertificateActions,
|
||||
ProjectPermissionSecretActions,
|
||||
ProjectPermissionSshHostActions,
|
||||
ProjectPermissionSub
|
||||
@@ -948,7 +949,10 @@ export const projectServiceFactory = ({
|
||||
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 });
|
||||
|
||||
|
49
backend/src/services/super-admin/invalidate-cache-queue.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||
|
||||
import { CacheType } from "./super-admin-types";
|
||||
|
||||
export type TInvalidateCacheQueueFactoryDep = {
|
||||
queueService: TQueueServiceFactory;
|
||||
|
||||
keyStore: Pick<TKeyStoreFactory, "deleteItems" | "setItemWithExpiry" | "deleteItem">;
|
||||
};
|
||||
|
||||
export type TInvalidateCacheQueueFactory = ReturnType<typeof invalidateCacheQueueFactory>;
|
||||
|
||||
export const invalidateCacheQueueFactory = ({ queueService, keyStore }: TInvalidateCacheQueueFactoryDep) => {
|
||||
const startInvalidate = async (dto: {
|
||||
data: {
|
||||
type: CacheType;
|
||||
};
|
||||
}) => {
|
||||
await queueService.queue(QueueName.InvalidateCache, QueueJobs.InvalidateCache, dto, {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
jobId: `invalidate-cache-${dto.data.type}`
|
||||
});
|
||||
};
|
||||
|
||||
queueService.start(QueueName.InvalidateCache, async (job) => {
|
||||
try {
|
||||
const {
|
||||
data: { type }
|
||||
} = job.data;
|
||||
|
||||
await keyStore.setItemWithExpiry("invalidating-cache", 1800, "true"); // 30 minutes max (in case the job somehow silently fails)
|
||||
|
||||
if (type === CacheType.ALL || type === CacheType.SECRETS)
|
||||
await keyStore.deleteItems({ pattern: "secret-manager:*" });
|
||||
|
||||
await keyStore.deleteItem("invalidating-cache");
|
||||
} catch (err) {
|
||||
logger.error(err, "Failed to invalidate cache");
|
||||
await keyStore.deleteItem("invalidating-cache");
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
startInvalidate
|
||||
};
|
||||
};
|
@@ -25,8 +25,10 @@ import { TOrgServiceFactory } from "../org/org-service";
|
||||
import { TUserDALFactory } from "../user/user-dal";
|
||||
import { TUserAliasDALFactory } from "../user-alias/user-alias-dal";
|
||||
import { UserAliasType } from "../user-alias/user-alias-types";
|
||||
import { TInvalidateCacheQueueFactory } from "./invalidate-cache-queue";
|
||||
import { TSuperAdminDALFactory } from "./super-admin-dal";
|
||||
import {
|
||||
CacheType,
|
||||
LoginMethod,
|
||||
TAdminBootstrapInstanceDTO,
|
||||
TAdminGetIdentitiesDTO,
|
||||
@@ -46,9 +48,10 @@ type TSuperAdminServiceFactoryDep = {
|
||||
kmsService: Pick<TKmsServiceFactory, "encryptWithRootKey" | "decryptWithRootKey" | "updateEncryptionStrategy">;
|
||||
kmsRootConfigDAL: TKmsRootConfigDALFactory;
|
||||
orgService: Pick<TOrgServiceFactory, "createOrganization">;
|
||||
keyStore: Pick<TKeyStoreFactory, "getItem" | "setItemWithExpiry" | "deleteItem">;
|
||||
keyStore: Pick<TKeyStoreFactory, "getItem" | "setItemWithExpiry" | "deleteItem" | "deleteItems">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "onPremFeatures">;
|
||||
microsoftTeamsService: Pick<TMicrosoftTeamsServiceFactory, "initializeTeamsBot">;
|
||||
invalidateCacheQueue: TInvalidateCacheQueueFactory;
|
||||
};
|
||||
|
||||
export type TSuperAdminServiceFactory = ReturnType<typeof superAdminServiceFactory>;
|
||||
@@ -64,7 +67,7 @@ export let getServerCfg: () => Promise<
|
||||
|
||||
const ADMIN_CONFIG_KEY = "infisical-admin-cfg";
|
||||
const ADMIN_CONFIG_KEY_EXP = 60; // 60s
|
||||
const ADMIN_CONFIG_DB_UUID = "00000000-0000-0000-0000-000000000000";
|
||||
export const ADMIN_CONFIG_DB_UUID = "00000000-0000-0000-0000-000000000000";
|
||||
|
||||
export const superAdminServiceFactory = ({
|
||||
serverCfgDAL,
|
||||
@@ -80,7 +83,8 @@ export const superAdminServiceFactory = ({
|
||||
identityAccessTokenDAL,
|
||||
identityTokenAuthDAL,
|
||||
identityOrgMembershipDAL,
|
||||
microsoftTeamsService
|
||||
microsoftTeamsService,
|
||||
invalidateCacheQueue
|
||||
}: TSuperAdminServiceFactoryDep) => {
|
||||
const initServerCfg = async () => {
|
||||
// TODO(akhilmhdh): bad pattern time less change this later to me itself
|
||||
@@ -631,6 +635,16 @@ export const superAdminServiceFactory = ({
|
||||
await kmsService.updateEncryptionStrategy(strategy);
|
||||
};
|
||||
|
||||
const invalidateCache = async (type: CacheType) => {
|
||||
await invalidateCacheQueue.startInvalidate({
|
||||
data: { type }
|
||||
});
|
||||
};
|
||||
|
||||
const checkIfInvalidatingCache = async () => {
|
||||
return (await keyStore.getItem("invalidating-cache")) !== null;
|
||||
};
|
||||
|
||||
return {
|
||||
initServerCfg,
|
||||
updateServerCfg,
|
||||
@@ -644,6 +658,8 @@ export const superAdminServiceFactory = ({
|
||||
getConfiguredEncryptionStrategies,
|
||||
grantServerAdminAccessToUser,
|
||||
deleteIdentitySuperAdminAccess,
|
||||
deleteUserSuperAdminAccess
|
||||
deleteUserSuperAdminAccess,
|
||||
invalidateCache,
|
||||
checkIfInvalidatingCache
|
||||
};
|
||||
};
|
||||
|
@@ -44,3 +44,8 @@ export enum LoginMethod {
|
||||
LDAP = "ldap",
|
||||
OIDC = "oidc"
|
||||
}
|
||||
|
||||
export enum CacheType {
|
||||
ALL = "all",
|
||||
SECRETS = "secrets"
|
||||
}
|
||||
|
@@ -21,7 +21,8 @@ export enum PostHogEventTypes {
|
||||
IssueSshHostUserCert = "Issue SSH Host User Certificate",
|
||||
IssueSshHostHostCert = "Issue SSH Host Host Certificate",
|
||||
SignCert = "Sign PKI Certificate",
|
||||
IssueCert = "Issue PKI Certificate"
|
||||
IssueCert = "Issue PKI Certificate",
|
||||
InvalidateCache = "Invalidate Cache"
|
||||
}
|
||||
|
||||
export type TSecretModifiedEvent = {
|
||||
@@ -203,6 +204,13 @@ export type TIssueCertificateEvent = {
|
||||
};
|
||||
};
|
||||
|
||||
export type TInvalidateCacheEvent = {
|
||||
event: PostHogEventTypes.InvalidateCache;
|
||||
properties: {
|
||||
userAgent?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type TPostHogEvent = { distinctId: string } & (
|
||||
| TSecretModifiedEvent
|
||||
| TAdminInitEvent
|
||||
@@ -221,4 +229,5 @@ export type TPostHogEvent = { distinctId: string } & (
|
||||
| TIssueSshHostHostCertEvent
|
||||
| TSignCertificateEvent
|
||||
| TIssueCertificateEvent
|
||||
| TInvalidateCacheEvent
|
||||
);
|
||||
|
8
docs/api-reference/endpoints/certificates/bundle.mdx
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
title: "Get Certificate Bundle"
|
||||
openapi: "GET /api/v1/pki/certificates/{serialNumber}/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/v1/pki/certificates/{serialNumber}/private-key"
|
||||
---
|
@@ -7,6 +7,113 @@ The Infisical Secret Scanner allows you to keep an overview and stay alert of ex
|
||||
|
||||
To further enhance security, we recommend you also use our [CLI Secret Scanner](/cli/scanning-overview#automatically-scan-changes-before-you-commit) to scan for exposed secrets prior to pushing your changes.
|
||||
|
||||
|
||||
<Accordion title="Self-hosting">
|
||||
|
||||
To setup secret scanning on your own instance of Infisical, you can follow the steps below.
|
||||
|
||||
<Steps>
|
||||
<Step title="Create a GitHub App">
|
||||
Create a new GitHub app in your GitHub organization or personal [Developer Settings](https://github.com/settings/apps).
|
||||
|
||||

|
||||
|
||||
### Configure the GitHub App
|
||||
To configure the GitHub app to work with Infisical, you'll need to modify the following settings:
|
||||
- **Homepage URL**: Required to be set. Set it to the URL of your Infisical instance. (e.g. `https://app.infisical.com`)
|
||||
- **Setup URL**: Set this to `https://<your-infisical-instance.com>/organization/secret-scanning`
|
||||
- **Webhook URL**: Set this to `https://<your-infisical-instance.com>/api/v1/secret-scanning/webhook`
|
||||
- **Webhook Secret**: Set this to a random string. This is used to verify the webhook request from Infisical. Use `openssl rand -base64 32` in your terminal to generate a random secret.
|
||||
|
||||
<Note>
|
||||
Remember to save the webhook secret as you will need it in the next step.
|
||||
</Note>
|
||||
|
||||

|
||||
|
||||
### Configure the GitHub App Permissions
|
||||
The GitHub app needs the following permissions:
|
||||
|
||||
Repository permissions:
|
||||
- `Checks`: Read and Write
|
||||
- `Contents`: Read-only
|
||||
- `Issues`: Read and Write
|
||||
- `Pull Requests`: Read and Write
|
||||
- `Metadata`: Read-only (enabled by default)
|
||||
|
||||

|
||||
|
||||
Subscribed events:
|
||||
- `Check run`
|
||||
- `Pull request`
|
||||
- `Push`
|
||||
|
||||

|
||||
|
||||
|
||||
### Create the GitHub App
|
||||
Now you can create the GitHub app by clicking on the "Create GitHub App" button.
|
||||
|
||||
<Note>
|
||||
If you want other Github users to be able to install the app, you need to tick the "Any account" option under "Where can this GitHub App be installed?"
|
||||
</Note>
|
||||
|
||||

|
||||
</Step>
|
||||
|
||||
<Step title="Retrieve the GitHub App ID">
|
||||
After clicking the "Create GitHub App" button, you will be redirected to the GitHub settings page. Here you can copy the "App ID" and save it for later when you need to configure your environment variables for your Infisical instance.
|
||||
|
||||

|
||||
</Step>
|
||||
|
||||
<Step title="Retrieve your GitHub App slug">
|
||||
The GitHub App slug is the name of the app you created in a slug friendly format. You can find the slug in the URL of the app you created.
|
||||
|
||||

|
||||
</Step>
|
||||
|
||||
<Step title="Create a new GitHub App private key">
|
||||
Create a new app private key by clicking on the "Generate a private key" button under the "Private keys" section.
|
||||
|
||||
Once you click the "Generate a private key" button, the private key will be downloaded to your computer. Save this file for later as you will need the private key when configuring Infisical.
|
||||
|
||||

|
||||
|
||||
<Note>
|
||||
Remember to save the private key as you will need it in the next step.
|
||||
</Note>
|
||||
|
||||
</Step>
|
||||
|
||||
|
||||
<Step title="Configure your Infisical instance">
|
||||
Now you can configure your Infisical instance by setting the following environment variables:
|
||||
|
||||
- `SECRET_SCANNING_GIT_APP_ID`: The App ID of your GitHub App.
|
||||
- `SECRET_SCANNING_GIT_APP_SLUG`: The slug of your GitHub App.
|
||||
- `SECRET_SCANNING_PRIVATE_KEY`: The private key of your GitHub App that you created in a previous step.
|
||||
- `SECRET_SCANNING_WEBHOOK_SECRET`: The webhook secret of your GitHub App that you created in a previous step.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
After restarting your Infisical instance, you should be able to use the secret scanning feature within your organization. Follow the steps below to add the GitHub App to your Infisical organization.
|
||||
</Accordion>
|
||||
|
||||
## Install the Infisical Radar GitHub App
|
||||
|
||||
To install the GitHub App, press the "Integrate With GitHub" button in the top right corner of your Infisical Secret Scanning dashboard.
|
||||
|
||||

|
||||
|
||||
Next, you'll be prompted to select which organization you'd like to install the app into. Select the organization you'd like to install the app into by clicking the organization in the menu.
|
||||
|
||||

|
||||
|
||||
Select the repositories you'd like to scan for secrets and press the "Install" button.
|
||||
|
||||

|
||||
|
||||
## Code Scanning
|
||||
|
||||

|
||||
|
BIN
docs/images/platform/secret-scanning/github-app-copy-app-id.png
Normal file
After Width: | Height: | Size: 126 KiB |
BIN
docs/images/platform/secret-scanning/github-app-copy-slug.png
Normal file
After Width: | Height: | Size: 135 KiB |
After Width: | Height: | Size: 83 KiB |
BIN
docs/images/platform/secret-scanning/github-configure-app.png
Normal file
After Width: | Height: | Size: 107 KiB |
After Width: | Height: | Size: 154 KiB |
BIN
docs/images/platform/secret-scanning/github-create-app.png
Normal file
After Width: | Height: | Size: 79 KiB |
BIN
docs/images/platform/secret-scanning/github-register-app.png
Normal file
After Width: | Height: | Size: 95 KiB |
BIN
docs/images/platform/secret-scanning/github-repo-permissions.png
Normal file
After Width: | Height: | Size: 240 KiB |
BIN
docs/images/platform/secret-scanning/github-select-org-2.png
Normal file
After Width: | Height: | Size: 92 KiB |
BIN
docs/images/platform/secret-scanning/github-select-org.png
Normal file
After Width: | Height: | Size: 65 KiB |
BIN
docs/images/platform/secret-scanning/github-select-repos.png
Normal file
After Width: | Height: | Size: 104 KiB |
After Width: | Height: | Size: 186 KiB |
After Width: | Height: | Size: 116 KiB |
@@ -34,7 +34,7 @@ Before applying the InfisicalPushSecret CRD, you need to create a Kubernetes sec
|
||||
metadata:
|
||||
name: infisical-push-secret-demo
|
||||
spec:
|
||||
resyncInterval: 1m
|
||||
resyncInterval: 1m # Remove this field to disable automatic reconciliation of the InfisicalPushSecret CRD.
|
||||
hostAPI: https://app.infisical.com/api
|
||||
|
||||
# Optional, defaults to no replacement.
|
||||
@@ -124,7 +124,9 @@ After applying the InfisicalPushSecret CRD, you should notice that the secrets y
|
||||
|
||||
<Accordion title="resyncInterval">
|
||||
|
||||
The `resyncInterval` is a string-formatted duration that defines the time between each resync.
|
||||
The `resyncInterval` is a string-formatted duration that defines the time between each resync. The field is optional, and will default to no automatic resync if not defined.
|
||||
|
||||
If you don't want to automatically reconcile the InfisicalPushSecret CRD on an interval, you can remove the `resyncInterval` field entirely from your InfisicalPushSecret CRD.
|
||||
|
||||
The format of the field is `[duration][unit]` where `duration` is a number and `unit` is a string representing the unit of time.
|
||||
|
||||
@@ -459,6 +461,126 @@ Using Go templates, you can format, combine, and create new key-value pairs of s
|
||||
Please refer to the [templating functions documentation](/integrations/platforms/kubernetes/overview#available-helper-functions) for more information.
|
||||
</Accordion>
|
||||
|
||||
## Using generators to push secrets
|
||||
|
||||
Generators are a feature of the Infisical secrets operator that allows you to generate secrets on-reconcile and push them to Infisical. This is useful for secret rotation purposes, and fully operator-managed secrets.
|
||||
A generator is a custom resource that is installed on the cluster that defines the logic for generating a secret.
|
||||
|
||||
Generators don't keep track of the secrets they generate, which means that on each reconciliation, a new value will be created and pushed.
|
||||
For this reason you may want to disable automatic reconciliation of the InfisicalPushSecret CRD. You can do this by removing `resyncInterval` from the InfisicalPushSecret CRD.
|
||||
|
||||
**Supported generators**:
|
||||
- `Password`: Generates a random password of string format.
|
||||
- `UUID`: Generates a random v4 UUID.
|
||||
|
||||
To use a generator, you must specify at least one generator in the `push.generators[]` field. An example of a generator usage can be seen here:
|
||||
|
||||
<Accordion title="push.generators[]">
|
||||
Define a generator in the `push.generators[]` field.
|
||||
|
||||
<Accordion title="push.generators[].destinationSecretName">
|
||||
The name of the secret that will be created in Infisical.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="push.generators[].generatorRef">
|
||||
The reference to the generator resource.
|
||||
|
||||
Valid fields:
|
||||
- `kind`: The kind of the generator resource, must match the generator kind.
|
||||
- `name`: The name of the generator resource.
|
||||
</Accordion>
|
||||
</Accordion>
|
||||
|
||||
```yaml
|
||||
push:
|
||||
secret:
|
||||
secretName: push-secret-source-secret
|
||||
secretNamespace: dev
|
||||
generators:
|
||||
- destinationSecretName: password-generator # Name of the secret that will be created in Infisical
|
||||
generatorRef:
|
||||
kind: Password|UUID # Kind of the resource, must match the generator kind.
|
||||
name: custom-generator # Name of the generator resource
|
||||
```
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Password Generator">
|
||||
|
||||
The Password generator is a custom resource that is installed on the cluster that defines the logic for generating a password.
|
||||
<Accordion title="Spec definition">
|
||||
|
||||
- `kind`: The kind of the generator resource, must match the generator kind. For the Password generator, the kind is `Password`.
|
||||
- `generator.passwordSpec`: The spec of the password generator.
|
||||
|
||||
<Accordion title="generator.passwordSpec">
|
||||
- `length`: The length of the password.
|
||||
- `digits`: The number of digits in the password.
|
||||
- `symbols`: The number of symbols in the password.
|
||||
- `symbolCharacters`: The characters to use for the symbols in the password.
|
||||
- `noUpper`: Whether to include uppercase letters in the password.
|
||||
- `allowRepeat`: Whether to allow repeating characters in the password.
|
||||
</Accordion>
|
||||
</Accordion>
|
||||
|
||||
```yaml password-cluster-generator.yaml
|
||||
apiVersion: secrets.infisical.com/v1alpha1
|
||||
kind: ClusterGenerator
|
||||
metadata:
|
||||
name: password-generator
|
||||
spec:
|
||||
kind: Password
|
||||
generator:
|
||||
passwordSpec:
|
||||
length: 10
|
||||
digits: 5
|
||||
symbols: 5
|
||||
symbolCharacters: "-_$@"
|
||||
noUpper: false
|
||||
allowRepeat: true
|
||||
```
|
||||
|
||||
Example InfisicalPushSecret CRD using the Password generator:
|
||||
```yaml infisical-push-secret-crd.yaml
|
||||
push:
|
||||
generators:
|
||||
- destinationSecretName: password-generator-test
|
||||
generatorRef:
|
||||
kind: Password
|
||||
name: password-generator
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="UUID Generator">
|
||||
The UUID generator is a custom resource that is installed on the cluster that defines the logic for generating a UUID.
|
||||
<Accordion title="Spec definition">
|
||||
- `kind`: The kind of the generator resource, must match the generator kind. For the UUID generator, the kind is `UUID`.
|
||||
- `generator.uuidSpec`: The spec of the UUID generator. For UUID's, this can be left empty.
|
||||
</Accordion>
|
||||
|
||||
```yaml uuid-cluster-generator.yaml
|
||||
apiVersion: secrets.infisical.com/v1alpha1
|
||||
kind: ClusterGenerator
|
||||
metadata:
|
||||
name: uuid-generator
|
||||
spec:
|
||||
kind: UUID
|
||||
generator:
|
||||
uuidSpec:
|
||||
```
|
||||
|
||||
Example InfisicalPushSecret CRD using the UUID generator:
|
||||
```yaml infisical-push-secret-crd.yaml
|
||||
push:
|
||||
generators:
|
||||
- destinationSecretName: uuid-generator-test
|
||||
generatorRef:
|
||||
kind: UUID
|
||||
name: uuid-generator
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
|
||||
|
||||
## Applying the InfisicalPushSecret CRD to your cluster
|
||||
|
||||
Once you have configured the `InfisicalPushSecret` CRD with the required fields, you can apply it to your cluster.
|
||||
|
@@ -41,7 +41,7 @@ All final reward amounts are determined at Infisical's discretion based on impac
|
||||
|
||||
### 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
|
||||
- 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
|
||||
@@ -57,4 +57,24 @@ We ask that researchers:
|
||||
- Use testing accounts where possible
|
||||
- Give us a reasonable window to investigate and patch before going public
|
||||
|
||||
Researchers can also spin up our [self-hosted version of Infisical](/self-hosting/overview) to test for vulnerabilities locally.
|
||||
Researchers can also spin up our [self-hosted version of Infisical](/self-hosting/overview) to test for vulnerabilities locally.
|
||||
|
||||
### Program Conduct and Enforcement
|
||||
|
||||
We value professional and collaborative interaction with security researchers. To maintain the integrity of our bug bounty program, we expect all participants to adhere to the following guidelines:
|
||||
|
||||
- Maintain professional communication in all interactions
|
||||
- Do not threaten public disclosure of vulnerabilities before we've had reasonable time to investigate and address the issue
|
||||
- Do not attempt to extort or coerce compensation through threats
|
||||
- Follow the responsible disclosure process outlined in this document
|
||||
- Do not use automated scanning tools without prior permission
|
||||
|
||||
Violations of these guidelines may result in:
|
||||
|
||||
1. **Warning**: For minor violations, we may issue a warning explaining the violation and requesting compliance with program guidelines.
|
||||
2. **Temporary Ban**: Repeated minor violations or more serious violations may result in a temporary suspension from the program.
|
||||
3. **Permanent Ban**: Severe violations such as threats, extortion attempts, or unauthorized public disclosure will result in permanent removal from the Infisical Bug Bounty Program.
|
||||
|
||||
We reserve the right to reject reports, withhold bounties, and remove participants from the program at our discretion for conduct that undermines the collaborative spirit of security research.
|
||||
|
||||
Infisical is committed to working respectfully with security researchers who follow these guidelines, and we strive to recognize and reward valuable contributions that help protect our platform and users.
|
||||
|
@@ -252,11 +252,12 @@ Supports conditions and permission inversion
|
||||
|
||||
#### Subject: `certificates`
|
||||
|
||||
| Action | Description |
|
||||
| -------- | ----------------------------- |
|
||||
| `read` | View certificates |
|
||||
| `create` | Issue new certificates |
|
||||
| `delete` | Revoke or remove certificates |
|
||||
| Action | Description |
|
||||
| -------------------- | ----------------------------- |
|
||||
| `read` | View certificates |
|
||||
| `read-private-key` | Read certificate private key |
|
||||
| `create` | Issue new certificates |
|
||||
| `delete` | Revoke or remove certificates |
|
||||
|
||||
#### Subject: `certificate-templates`
|
||||
|
||||
|
@@ -1454,6 +1454,8 @@
|
||||
"api-reference/endpoints/certificates/revoke",
|
||||
"api-reference/endpoints/certificates/delete",
|
||||
"api-reference/endpoints/certificates/cert-body",
|
||||
"api-reference/endpoints/certificates/bundle",
|
||||
"api-reference/endpoints/certificates/private-key",
|
||||
"api-reference/endpoints/certificates/issue-certificate",
|
||||
"api-reference/endpoints/certificates/sign-certificate"
|
||||
]
|
||||
|
@@ -29,6 +29,19 @@ Used to configure platform-specific security and operational settings
|
||||
Specifies the internal port on which the application listens.
|
||||
</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>
|
||||
Telemetry helps us improve Infisical but if you want to disable it you may set
|
||||
this to `false`.
|
||||
@@ -612,6 +625,26 @@ To help you sync secrets from Infisical to services such as Github and Gitlab, I
|
||||
</ParamField>
|
||||
</Accordion>
|
||||
|
||||
## Secret Scanning
|
||||
|
||||
<Accordion title="GitHub">
|
||||
<ParamField query="SECRET_SCANNING_GIT_APP_ID" type="string" default="none" optional>
|
||||
The App ID of your GitHub App.
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="SECRET_SCANNING_GIT_APP_SLUG" type="string" default="none" optional>
|
||||
The slug of your GitHub App.
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="SECRET_SCANNING_PRIVATE_KEY" type="string" default="none" optional>
|
||||
A private key for your GitHub App.
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="SECRET_SCANNING_WEBHOOK_SECRET" type="string" default="none" optional>
|
||||
The webhook secret of your GitHub App.
|
||||
</ParamField>
|
||||
</Accordion>
|
||||
|
||||
## Observability
|
||||
|
||||
You can configure Infisical to collect and expose telemetry data for analytics and monitoring.
|
||||
|
@@ -2,6 +2,7 @@ export { useProjectPermission } from "./ProjectPermissionContext";
|
||||
export type { ProjectPermissionSet, TProjectPermission } from "./types";
|
||||
export {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionCertificateActions,
|
||||
ProjectPermissionCmekActions,
|
||||
ProjectPermissionDynamicSecretActions,
|
||||
ProjectPermissionGroupActions,
|
||||
|
@@ -7,6 +7,14 @@ export enum ProjectPermissionActions {
|
||||
Delete = "delete"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionCertificateActions {
|
||||
Read = "read",
|
||||
Create = "create",
|
||||
Edit = "edit",
|
||||
Delete = "delete",
|
||||
ReadPrivateKey = "read-private-key"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionSecretActions {
|
||||
DescribeAndReadValue = "read",
|
||||
DescribeSecret = "describeSecret",
|
||||
@@ -268,7 +276,7 @@ export type ProjectPermissionSet =
|
||||
)
|
||||
]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Certificates]
|
||||
| [ProjectPermissionCertificateActions, ProjectPermissionSub.Certificates]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateAuthorities]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateTemplates]
|
||||
|
@@ -10,6 +10,7 @@ export {
|
||||
export type { TProjectPermission } from "./ProjectPermissionContext";
|
||||
export {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionCertificateActions,
|
||||
ProjectPermissionCmekActions,
|
||||
ProjectPermissionDynamicSecretActions,
|
||||
ProjectPermissionGroupActions,
|
||||
|
@@ -3,6 +3,7 @@ export {
|
||||
useAdminGrantServerAdminAccess,
|
||||
useAdminRemoveIdentitySuperAdminAccess,
|
||||
useCreateAdminUser,
|
||||
useInvalidateCache,
|
||||
useRemoveUserServerAdminAccess,
|
||||
useUpdateServerConfig,
|
||||
useUpdateServerEncryptionStrategy
|
||||
|
@@ -8,6 +8,7 @@ import { adminQueryKeys, adminStandaloneKeys } from "./queries";
|
||||
import {
|
||||
RootKeyEncryptionStrategy,
|
||||
TCreateAdminUserDTO,
|
||||
TInvalidateCacheDTO,
|
||||
TServerConfig,
|
||||
TUpdateServerConfigDTO
|
||||
} from "./types";
|
||||
@@ -126,3 +127,15 @@ export const useUpdateServerEncryptionStrategy = () => {
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useInvalidateCache = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<void, object, TInvalidateCacheDTO>({
|
||||
mutationFn: async (dto) => {
|
||||
await apiRequest.post("/api/v1/admin/invalidate-cache", dto);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: adminQueryKeys.getInvalidateCache() });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@@ -8,6 +8,7 @@ import {
|
||||
AdminGetIdentitiesFilters,
|
||||
AdminGetUsersFilters,
|
||||
AdminIntegrationsConfig,
|
||||
TGetInvalidatingCacheStatus,
|
||||
TGetServerRootKmsEncryptionDetails,
|
||||
TServerConfig
|
||||
} from "./types";
|
||||
@@ -22,8 +23,10 @@ export const adminQueryKeys = {
|
||||
getUsers: (filters: AdminGetUsersFilters) => [adminStandaloneKeys.getUsers, { filters }] as const,
|
||||
getIdentities: (filters: AdminGetIdentitiesFilters) =>
|
||||
[adminStandaloneKeys.getIdentities, { filters }] as const,
|
||||
getAdminIntegrationsConfig: () => ["admin-integrations-config"] as const,
|
||||
getServerEncryptionStrategies: () => ["server-encryption-strategies"] as const
|
||||
getAdminSlackConfig: () => ["admin-slack-config"] as const,
|
||||
getServerEncryptionStrategies: () => ["server-encryption-strategies"] as const,
|
||||
getInvalidateCache: () => ["admin-invalidate-cache"] as const,
|
||||
getAdminIntegrationsConfig: () => ["admin-integrations-config"] as const
|
||||
};
|
||||
|
||||
export const fetchServerConfig = async () => {
|
||||
@@ -118,3 +121,18 @@ export const useGetServerRootKmsEncryptionDetails = () => {
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetInvalidatingCacheStatus = (enabled = true) => {
|
||||
return useQuery({
|
||||
queryKey: adminQueryKeys.getInvalidateCache(),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<TGetInvalidatingCacheStatus>(
|
||||
"/api/v1/admin/invalidating-cache-status"
|
||||
);
|
||||
|
||||
return data.invalidating;
|
||||
},
|
||||
enabled,
|
||||
refetchInterval: (data) => (data ? 3000 : false)
|
||||
});
|
||||
};
|
||||
|
@@ -24,6 +24,7 @@ export type TServerConfig = {
|
||||
enabledLoginMethods: LoginMethod[];
|
||||
authConsentContent?: string;
|
||||
pageFrameContent?: string;
|
||||
invalidatingCache: boolean;
|
||||
};
|
||||
|
||||
export type TUpdateServerConfigDTO = {
|
||||
@@ -84,3 +85,16 @@ export enum RootKeyEncryptionStrategy {
|
||||
Software = "SOFTWARE",
|
||||
HSM = "HSM"
|
||||
}
|
||||
|
||||
export enum CacheType {
|
||||
ALL = "all",
|
||||
SECRETS = "secrets"
|
||||
}
|
||||
|
||||
export type TInvalidateCacheDTO = {
|
||||
type: CacheType;
|
||||
};
|
||||
|
||||
export type TGetInvalidatingCacheStatus = {
|
||||
invalidating: boolean;
|
||||
};
|
||||
|
@@ -72,6 +72,8 @@ export const eventToNameMap: { [K in EventType]: string } = {
|
||||
[EventType.DELETE_CERT]: "Delete certificate",
|
||||
[EventType.REVOKE_CERT]: "Revoke certificate",
|
||||
[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.GET_PKI_ALERT]: "Get PKI alert",
|
||||
[EventType.UPDATE_PKI_ALERT]: "Update PKI alert",
|
||||
|
@@ -78,6 +78,8 @@ export enum EventType {
|
||||
DELETE_CERT = "delete-cert",
|
||||
REVOKE_CERT = "revoke-cert",
|
||||
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",
|
||||
GET_PKI_ALERT = "get-pki-alert",
|
||||
UPDATE_PKI_ALERT = "update-pki-alert",
|
||||
|
@@ -620,6 +620,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 {
|
||||
type: EventType.CREATE_PKI_ALERT;
|
||||
metadata: {
|
||||
@@ -881,6 +899,8 @@ export type Event =
|
||||
| DeleteCert
|
||||
| RevokeCert
|
||||
| GetCertBody
|
||||
| GetCertPrivateKey
|
||||
| GetCertBundle
|
||||
| CreatePkiAlert
|
||||
| GetPkiAlert
|
||||
| UpdatePkiAlert
|
||||
|
@@ -6,7 +6,8 @@ import { TCertificate } from "./types";
|
||||
|
||||
export const certKeys = {
|
||||
getCertById: (serialNumber: string) => [{ serialNumber }, "cert"],
|
||||
getCertBody: (serialNumber: string) => [{ serialNumber }, "certBody"]
|
||||
getCertBody: (serialNumber: string) => [{ serialNumber }, "certBody"],
|
||||
getCertBundle: (serialNumber: string) => [{ serialNumber }, "certBundle"]
|
||||
};
|
||||
|
||||
export const useGetCert = (serialNumber: string) => {
|
||||
@@ -38,3 +39,19 @@ export const useGetCertBody = (serialNumber: string) => {
|
||||
enabled: Boolean(serialNumber)
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetCertBundle = (serialNumber: string) => {
|
||||
return useQuery({
|
||||
queryKey: certKeys.getCertBundle(serialNumber),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{
|
||||
certificate: string;
|
||||
certificateChain: string;
|
||||
serialNumber: string;
|
||||
privateKey: string;
|
||||
}>(`/api/v1/pki/certificates/${serialNumber}/bundle`);
|
||||
return data;
|
||||
},
|
||||
enabled: Boolean(serialNumber)
|
||||
});
|
||||
};
|
||||
|
@@ -10,15 +10,17 @@ import {
|
||||
} from "./types";
|
||||
|
||||
export const useCreateNewInstallationSession = () => {
|
||||
return useMutation<{ sessionId: string }, object, { organizationId: string }>({
|
||||
mutationFn: async (opt) => {
|
||||
const { data } = await apiRequest.post(
|
||||
"/api/v1/secret-scanning/create-installation-session/organization",
|
||||
opt
|
||||
);
|
||||
return data;
|
||||
return useMutation<{ sessionId: string; gitAppSlug: string }, object, { organizationId: string }>(
|
||||
{
|
||||
mutationFn: async (opt) => {
|
||||
const { data } = await apiRequest.post(
|
||||
"/api/v1/secret-scanning/create-installation-session/organization",
|
||||
opt
|
||||
);
|
||||
return data;
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
};
|
||||
|
||||
export const useUpdateRiskStatus = () => {
|
||||
|
@@ -31,6 +31,7 @@ import {
|
||||
import { IdentityPanel } from "@app/pages/admin/OverviewPage/components/IdentityPanel";
|
||||
|
||||
import { AuthPanel } from "./components/AuthPanel";
|
||||
import { CachingPanel } from "./components/CachingPanel";
|
||||
import { EncryptionPanel } from "./components/EncryptionPanel";
|
||||
import { IntegrationPanel } from "./components/IntegrationPanel";
|
||||
import { UserPanel } from "./components/UserPanel";
|
||||
@@ -42,7 +43,8 @@ enum TabSections {
|
||||
Integrations = "integrations",
|
||||
Users = "users",
|
||||
Identities = "identities",
|
||||
Kmip = "kmip"
|
||||
Kmip = "kmip",
|
||||
Caching = "caching"
|
||||
}
|
||||
|
||||
enum SignUpModes {
|
||||
@@ -164,6 +166,7 @@ export const OverviewPage = () => {
|
||||
<Tab value={TabSections.Integrations}>Integrations</Tab>
|
||||
<Tab value={TabSections.Users}>User Identities</Tab>
|
||||
<Tab value={TabSections.Identities}>Machine Identities</Tab>
|
||||
<Tab value={TabSections.Caching}>Caching</Tab>
|
||||
</div>
|
||||
</TabList>
|
||||
<TabPanel value={TabSections.Settings}>
|
||||
@@ -408,6 +411,9 @@ export const OverviewPage = () => {
|
||||
<TabPanel value={TabSections.Identities}>
|
||||
<IdentityPanel />
|
||||
</TabPanel>
|
||||
<TabPanel value={TabSections.Caching}>
|
||||
<CachingPanel />
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
</div>
|
||||
)}
|
||||
|
@@ -0,0 +1,101 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { faRotate } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Badge, Button, DeleteActionModal } from "@app/components/v2";
|
||||
import { useUser } from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useInvalidateCache } from "@app/hooks/api";
|
||||
import { useGetInvalidatingCacheStatus } from "@app/hooks/api/admin/queries";
|
||||
import { CacheType } from "@app/hooks/api/admin/types";
|
||||
|
||||
export const CachingPanel = () => {
|
||||
const { mutateAsync: invalidateCache } = useInvalidateCache();
|
||||
const { user } = useUser();
|
||||
|
||||
const [type, setType] = useState<CacheType | null>(null);
|
||||
const [shouldPoll, setShouldPoll] = useState(false);
|
||||
|
||||
const {
|
||||
data: invalidationStatus,
|
||||
isFetching,
|
||||
refetch
|
||||
} = useGetInvalidatingCacheStatus(shouldPoll);
|
||||
const isInvalidating = Boolean(shouldPoll && (isFetching || invalidationStatus));
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"invalidateCache"
|
||||
] as const);
|
||||
|
||||
const handleInvalidateCacheSubmit = async () => {
|
||||
if (!type || isInvalidating) return;
|
||||
|
||||
try {
|
||||
await invalidateCache({ type });
|
||||
createNotification({ text: `Began invalidating ${type} cache`, type: "success" });
|
||||
setShouldPoll(true);
|
||||
handlePopUpClose("invalidateCache");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({ text: `Failed to invalidate ${type} cache`, type: "error" });
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isInvalidating) return;
|
||||
|
||||
if (shouldPoll) {
|
||||
setShouldPoll(false);
|
||||
createNotification({ text: "Successfully invalidated cache", type: "success" });
|
||||
}
|
||||
}, [isInvalidating, shouldPoll]);
|
||||
|
||||
useEffect(() => {
|
||||
refetch().then((v) => setShouldPoll(v.data || false));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-6 flex flex-wrap items-end justify-between gap-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="flex flex-col">
|
||||
<div className="mb-2 flex items-center gap-3">
|
||||
<span className="text-xl font-semibold text-mineshaft-100">Secrets Cache</span>
|
||||
{isInvalidating && (
|
||||
<Badge
|
||||
variant="danger"
|
||||
className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap"
|
||||
>
|
||||
<FontAwesomeIcon icon={faRotate} className="animate-spin" />
|
||||
Invalidating Cache
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<span className="max-w-xl text-sm text-mineshaft-400">
|
||||
The encrypted secrets cache encompasses all secrets stored within the system and
|
||||
provides a temporary, secure storage location for frequently accessed credentials.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
colorSchema="danger"
|
||||
onClick={() => {
|
||||
setType(CacheType.SECRETS);
|
||||
handlePopUpOpen("invalidateCache");
|
||||
}}
|
||||
isDisabled={!user.superAdmin || isInvalidating}
|
||||
>
|
||||
Invalidate Secrets Cache
|
||||
</Button>
|
||||
</div>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.invalidateCache.isOpen}
|
||||
title={`Are you sure you want to invalidate ${type} cache?`}
|
||||
subTitle="This action is permanent and irreversible. The cache invalidation process may take several minutes to complete."
|
||||
onChange={(isOpen) => handlePopUpToggle("invalidateCache", isOpen)}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={handleInvalidateCacheSubmit}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
@@ -3,7 +3,12 @@ import { useTranslation } from "react-i18next";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { PageHeader } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionCertificateActions,
|
||||
ProjectPermissionSub,
|
||||
useProjectPermission
|
||||
} from "@app/context";
|
||||
|
||||
import { PkiCollectionSection } from "../AlertingPage/components";
|
||||
import { CertificatesSection } from "./components";
|
||||
@@ -17,7 +22,7 @@ export const CertificatesPage = () => {
|
||||
ProjectPermissionSub.PkiCollections
|
||||
);
|
||||
const canAccessCerts = permission.can(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionCertificateActions.Read,
|
||||
ProjectPermissionSub.Certificates
|
||||
);
|
||||
|
||||
@@ -40,7 +45,7 @@ export const CertificatesPage = () => {
|
||||
)}
|
||||
<ProjectPermissionCan
|
||||
renderGuardBanner
|
||||
I={ProjectPermissionActions.Read}
|
||||
I={ProjectPermissionCertificateActions.Read}
|
||||
a={ProjectPermissionSub.Certificates}
|
||||
>
|
||||
<CertificatesSection />
|
||||
|
@@ -1,5 +1,11 @@
|
||||
import { Modal, ModalContent } from "@app/components/v2";
|
||||
import {
|
||||
ProjectPermissionCertificateActions,
|
||||
ProjectPermissionSub,
|
||||
useProjectPermission
|
||||
} from "@app/context";
|
||||
import { useGetCertBody } from "@app/hooks/api";
|
||||
import { useGetCertBundle } from "@app/hooks/api/certificates/queries";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
import { CertificateContent } from "./CertificateContent";
|
||||
@@ -10,10 +16,29 @@ type Props = {
|
||||
};
|
||||
|
||||
export const CertificateCertModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const { data } = useGetCertBody(
|
||||
(popUp?.certificateCert?.data as { serialNumber: string })?.serialNumber || ""
|
||||
const { permission } = useProjectPermission();
|
||||
|
||||
const serialNumber =
|
||||
(popUp?.certificateCert?.data as { serialNumber: string })?.serialNumber || "";
|
||||
|
||||
const canReadPrivateKey = permission.can(
|
||||
ProjectPermissionCertificateActions.ReadPrivateKey,
|
||||
ProjectPermissionSub.Certificates
|
||||
);
|
||||
|
||||
// useGetCertBundle fails unless user has the correct permissions
|
||||
const { data: bundleData } = useGetCertBundle(serialNumber);
|
||||
const { data: bodyData } = useGetCertBody(serialNumber);
|
||||
|
||||
const data:
|
||||
| {
|
||||
certificate: string;
|
||||
certificateChain: string;
|
||||
serialNumber: string;
|
||||
privateKey?: string;
|
||||
}
|
||||
| undefined = canReadPrivateKey ? bundleData : bodyData;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={popUp?.certificateCert?.isOpen}
|
||||
@@ -27,6 +52,7 @@ export const CertificateCertModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
serialNumber={data.serialNumber}
|
||||
certificate={data.certificate}
|
||||
certificateChain={data.certificateChain}
|
||||
privateKey={data.privateKey}
|
||||
/>
|
||||
) : (
|
||||
<div />
|
||||
|
@@ -4,7 +4,11 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { Button, DeleteActionModal } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import {
|
||||
ProjectPermissionCertificateActions,
|
||||
ProjectPermissionSub,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { useDeleteCert } from "@app/hooks/api";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
@@ -50,7 +54,7 @@ export const CertificatesSection = () => {
|
||||
<div className="mb-4 flex justify-between">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">Certificates</p>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
I={ProjectPermissionCertificateActions.Create}
|
||||
a={ProjectPermissionSub.Certificates}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
|
@@ -30,7 +30,11 @@ import {
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import {
|
||||
ProjectPermissionCertificateActions,
|
||||
ProjectPermissionSub,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { useListWorkspaceCertificates } from "@app/hooks/api";
|
||||
import { CertStatus } from "@app/hooks/api/certificates/enums";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
@@ -110,7 +114,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="p-1">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Read}
|
||||
I={ProjectPermissionCertificateActions.Read}
|
||||
a={ProjectPermissionSub.Certificates}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
@@ -131,7 +135,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Read}
|
||||
I={ProjectPermissionCertificateActions.Read}
|
||||
a={ProjectPermissionSub.Certificates}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
@@ -152,7 +156,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
I={ProjectPermissionCertificateActions.Delete}
|
||||
a={ProjectPermissionSub.Certificates}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
@@ -173,7 +177,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
I={ProjectPermissionCertificateActions.Delete}
|
||||
a={ProjectPermissionSub.Certificates}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
|
@@ -108,7 +108,7 @@ export const SecretScanningPage = withPermission(
|
||||
|
||||
const generateNewIntegrationSession = async () => {
|
||||
const session = await createNewIntegrationSession({ organizationId });
|
||||
window.location.href = `https://github.com/apps/infisical-radar/installations/new?state=${session.sessionId}`;
|
||||
window.location.href = `https://github.com/apps/${session.gitAppSlug}/installations/new?state=${session.sessionId}`;
|
||||
};
|
||||
|
||||
return (
|
||||
|
@@ -10,10 +10,6 @@ import { AxiosError } from "axios";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
decryptAssymmetric,
|
||||
encryptSymmetric
|
||||
} from "@app/components/utilities/cryptography/crypto";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
@@ -28,7 +24,7 @@ import {
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { useCreateServiceToken, useGetUserWsKey } from "@app/hooks/api";
|
||||
import { useCreateServiceToken } from "@app/hooks/api";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const apiTokenExpiry = [
|
||||
@@ -97,7 +93,6 @@ export const AddServiceTokenModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const [newToken, setToken] = useState("");
|
||||
const [isTokenCopied, setIsTokenCopied] = useToggle(false);
|
||||
|
||||
const { data: latestFileKey } = useGetUserWsKey(currentWorkspace?.id ?? "");
|
||||
const createServiceToken = useCreateServiceToken();
|
||||
const hasServiceToken = Boolean(newToken);
|
||||
|
||||
@@ -118,26 +113,13 @@ export const AddServiceTokenModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const onFormSubmit = async ({ name, scopes, expiresIn, permissions }: FormData) => {
|
||||
try {
|
||||
if (!currentWorkspace?.id) return;
|
||||
if (!latestFileKey) return;
|
||||
|
||||
const key = decryptAssymmetric({
|
||||
ciphertext: latestFileKey.encryptedKey,
|
||||
nonce: latestFileKey.nonce,
|
||||
publicKey: latestFileKey.sender.publicKey,
|
||||
privateKey: localStorage.getItem("PRIVATE_KEY") as string
|
||||
});
|
||||
|
||||
const randomBytes = crypto.randomBytes(16).toString("hex");
|
||||
|
||||
const { ciphertext, iv, tag } = encryptSymmetric({
|
||||
plaintext: key,
|
||||
key: randomBytes
|
||||
});
|
||||
|
||||
const { serviceToken } = await createServiceToken.mutateAsync({
|
||||
encryptedKey: ciphertext,
|
||||
iv,
|
||||
tag,
|
||||
encryptedKey: "",
|
||||
iv: "",
|
||||
tag: "",
|
||||
scopes,
|
||||
expiresIn: Number(expiresIn),
|
||||
name,
|
||||
|
@@ -114,7 +114,7 @@ export const GeneralPermissionPolicies = <T extends keyof NonNullable<TFormSchem
|
||||
<div
|
||||
key={el.id}
|
||||
className={twMerge(
|
||||
"relative bg-mineshaft-800 p-5 first:rounded-t-md last:rounded-b-md",
|
||||
"relative bg-mineshaft-800 p-5 pr-10 first:rounded-t-md last:rounded-b-md",
|
||||
dragOverItem === rootIndex ? "border-2 border-blue-400" : "",
|
||||
draggedItem === rootIndex ? "opacity-50" : ""
|
||||
)}
|
||||
@@ -179,7 +179,7 @@ export const GeneralPermissionPolicies = <T extends keyof NonNullable<TFormSchem
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex text-gray-300">
|
||||
<div className="flex gap-4 text-gray-300">
|
||||
<div className="w-1/4">Actions</div>
|
||||
<div className="flex flex-grow flex-wrap justify-start gap-8">
|
||||
{actions.map(({ label, value }, index) => {
|
||||
|
@@ -6,6 +6,7 @@ import { z } from "zod";
|
||||
import { Tooltip } from "@app/components/v2";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionCertificateActions,
|
||||
ProjectPermissionCmekActions,
|
||||
ProjectPermissionSub
|
||||
} from "@app/context";
|
||||
@@ -32,6 +33,14 @@ const GeneralPolicyActionSchema = z.object({
|
||||
create: z.boolean().optional()
|
||||
});
|
||||
|
||||
const CertificatePolicyActionSchema = z.object({
|
||||
[ProjectPermissionCertificateActions.Create]: z.boolean().optional(),
|
||||
[ProjectPermissionCertificateActions.Delete]: z.boolean().optional(),
|
||||
[ProjectPermissionCertificateActions.Edit]: z.boolean().optional(),
|
||||
[ProjectPermissionCertificateActions.Read]: z.boolean().optional(),
|
||||
[ProjectPermissionCertificateActions.ReadPrivateKey]: z.boolean().optional()
|
||||
});
|
||||
|
||||
const SecretPolicyActionSchema = z.object({
|
||||
[ProjectPermissionSecretActions.DescribeAndReadValue]: z.boolean().optional(), // existing read, gives both describe and read value
|
||||
[ProjectPermissionSecretActions.DescribeSecret]: z.boolean().optional(),
|
||||
@@ -219,7 +228,7 @@ export const projectRoleFormSchema = z.object({
|
||||
[ProjectPermissionSub.AuditLogs]: GeneralPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.IpAllowList]: GeneralPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.CertificateAuthorities]: GeneralPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.Certificates]: GeneralPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.Certificates]: CertificatePolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.PkiAlerts]: GeneralPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.PkiCollections]: GeneralPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.CertificateTemplates]: GeneralPolicyActionSchema.array().default([]),
|
||||
@@ -371,7 +380,6 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
||||
ProjectPermissionSub.AuditLogs,
|
||||
ProjectPermissionSub.IpAllowList,
|
||||
ProjectPermissionSub.CertificateAuthorities,
|
||||
ProjectPermissionSub.Certificates,
|
||||
ProjectPermissionSub.PkiAlerts,
|
||||
ProjectPermissionSub.PkiCollections,
|
||||
ProjectPermissionSub.CertificateTemplates,
|
||||
@@ -507,6 +515,25 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (subject === ProjectPermissionSub.Certificates) {
|
||||
const canRead = action.includes(ProjectPermissionCertificateActions.Read);
|
||||
const canEdit = action.includes(ProjectPermissionCertificateActions.Edit);
|
||||
const canDelete = action.includes(ProjectPermissionCertificateActions.Delete);
|
||||
const canCreate = action.includes(ProjectPermissionCertificateActions.Create);
|
||||
const canReadPrivateKey = action.includes(ProjectPermissionCertificateActions.ReadPrivateKey);
|
||||
|
||||
if (!formVal[subject]) formVal[subject] = [{}];
|
||||
|
||||
// from above statement we are sure it won't be undefined
|
||||
if (canRead) formVal[subject]![0].read = true;
|
||||
if (canEdit) formVal[subject]![0].edit = true;
|
||||
if (canCreate) formVal[subject]![0].create = true;
|
||||
if (canDelete) formVal[subject]![0].delete = true;
|
||||
if (canReadPrivateKey)
|
||||
formVal[subject]![0][ProjectPermissionCertificateActions.ReadPrivateKey] = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (subject === ProjectPermissionSub.Project) {
|
||||
const canEdit = action.includes(ProjectPermissionActions.Edit);
|
||||
const canDelete = action.includes(ProjectPermissionActions.Delete);
|
||||
@@ -1014,10 +1041,11 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
|
||||
[ProjectPermissionSub.Certificates]: {
|
||||
title: "Certificates",
|
||||
actions: [
|
||||
{ label: "Read", value: "read" },
|
||||
{ label: "Create", value: "create" },
|
||||
{ label: "Modify", value: "edit" },
|
||||
{ label: "Remove", value: "delete" }
|
||||
{ label: "Read", value: ProjectPermissionCertificateActions.Read },
|
||||
{ label: "Read Private Key", value: ProjectPermissionCertificateActions.ReadPrivateKey },
|
||||
{ label: "Create", value: ProjectPermissionCertificateActions.Create },
|
||||
{ label: "Modify", value: ProjectPermissionCertificateActions.Edit },
|
||||
{ label: "Remove", value: ProjectPermissionCertificateActions.Delete }
|
||||
]
|
||||
},
|
||||
[ProjectPermissionSub.CertificateTemplates]: {
|
||||
|
@@ -13,9 +13,9 @@ type: application
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||
version: v0.9.1
|
||||
version: v0.9.2
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||
# It is recommended to use it with quotes.
|
||||
appVersion: "v0.9.1"
|
||||
appVersion: "v0.9.2"
|
||||
|
@@ -0,0 +1,97 @@
|
||||
{{- if .Values.installCRDs }}
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
name: clustergenerators.secrets.infisical.com
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.10.0
|
||||
labels:
|
||||
{{- include "secrets-operator.labels" . | nindent 4 }}
|
||||
spec:
|
||||
group: secrets.infisical.com
|
||||
names:
|
||||
kind: ClusterGenerator
|
||||
listKind: ClusterGeneratorList
|
||||
plural: clustergenerators
|
||||
singular: clustergenerator
|
||||
scope: Cluster
|
||||
versions:
|
||||
- name: v1alpha1
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
description: ClusterGenerator represents a cluster-wide generator
|
||||
properties:
|
||||
apiVersion:
|
||||
description: 'APIVersion defines the versioned schema of this representation
|
||||
of an object. Servers should convert recognized schemas to the latest
|
||||
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
|
||||
type: string
|
||||
kind:
|
||||
description: 'Kind is a string value representing the REST resource this
|
||||
object represents. Servers may infer this from the endpoint the client
|
||||
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
spec:
|
||||
properties:
|
||||
generator:
|
||||
description: Generator the spec for this generator, must match the kind.
|
||||
properties:
|
||||
passwordSpec:
|
||||
description: PasswordSpec controls the behavior of the password
|
||||
generator.
|
||||
properties:
|
||||
allowRepeat:
|
||||
default: false
|
||||
description: set allowRepeat to true to allow repeating characters.
|
||||
type: boolean
|
||||
digits:
|
||||
description: digits specifies the number of digits in the generated
|
||||
password. If omitted it defaults to 25% of the length of the
|
||||
password
|
||||
type: integer
|
||||
length:
|
||||
default: 24
|
||||
description: Length of the password to be generated. Defaults
|
||||
to 24
|
||||
type: integer
|
||||
noUpper:
|
||||
default: false
|
||||
description: Set noUpper to disable uppercase characters
|
||||
type: boolean
|
||||
symbolCharacters:
|
||||
description: symbolCharacters specifies the special characters
|
||||
that should be used in the generated password.
|
||||
type: string
|
||||
symbols:
|
||||
description: symbols specifies the number of symbol characters
|
||||
in the generated password. If omitted it defaults to 25% of
|
||||
the length of the password
|
||||
type: integer
|
||||
type: object
|
||||
uuidSpec:
|
||||
description: UUIDSpec controls the behavior of the uuid generator.
|
||||
type: object
|
||||
type: object
|
||||
kind:
|
||||
description: Kind the kind of this generator.
|
||||
enum:
|
||||
- Password
|
||||
- UUID
|
||||
type: string
|
||||
required:
|
||||
- kind
|
||||
type: object
|
||||
type: object
|
||||
served: true
|
||||
storage: true
|
||||
subresources:
|
||||
status: {}
|
||||
status:
|
||||
acceptedNames:
|
||||
kind: ""
|
||||
plural: ""
|
||||
conditions: []
|
||||
storedVersions: []
|
||||
{{- end }}
|
@@ -143,6 +143,34 @@ spec:
|
||||
type: string
|
||||
push:
|
||||
properties:
|
||||
generators:
|
||||
items:
|
||||
properties:
|
||||
destinationSecretName:
|
||||
type: string
|
||||
generatorRef:
|
||||
properties:
|
||||
kind:
|
||||
allOf:
|
||||
- enum:
|
||||
- Password
|
||||
- UUID
|
||||
- enum:
|
||||
- Password
|
||||
- UUID
|
||||
description: Specify the Kind of the generator resource
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
required:
|
||||
- kind
|
||||
- name
|
||||
type: object
|
||||
required:
|
||||
- destinationSecretName
|
||||
- generatorRef
|
||||
type: object
|
||||
type: array
|
||||
secret:
|
||||
properties:
|
||||
secretName:
|
||||
@@ -168,8 +196,6 @@ spec:
|
||||
- secretName
|
||||
- secretNamespace
|
||||
type: object
|
||||
required:
|
||||
- secret
|
||||
type: object
|
||||
resyncInterval:
|
||||
type: string
|
||||
@@ -199,7 +225,6 @@ spec:
|
||||
required:
|
||||
- destination
|
||||
- push
|
||||
- resyncInterval
|
||||
type: object
|
||||
status:
|
||||
description: InfisicalPushSecretStatus defines the observed state of InfisicalPushSecret
|
||||
|
@@ -81,6 +81,18 @@ rules:
|
||||
- tokenreviews
|
||||
verbs:
|
||||
- create
|
||||
- apiGroups:
|
||||
- secrets.infisical.com
|
||||
resources:
|
||||
- clustergenerators
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- get
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- secrets.infisical.com
|
||||
resources:
|
||||
|
@@ -32,7 +32,7 @@ controllerManager:
|
||||
- ALL
|
||||
image:
|
||||
repository: infisical/kubernetes-operator
|
||||
tag: v0.9.1
|
||||
tag: v0.9.2
|
||||
resources:
|
||||
limits:
|
||||
cpu: 500m
|
||||
|
152
k8-operator/api/v1alpha1/generators.go
Normal file
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
Copyright 2022.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// GeneratorKind represents a kind of generator.
|
||||
// +kubebuilder:validation:Enum=Password;UUID
|
||||
type GeneratorKind string
|
||||
|
||||
const (
|
||||
GeneratorKindPassword GeneratorKind = "Password"
|
||||
GeneratorKindUUID GeneratorKind = "UUID"
|
||||
)
|
||||
|
||||
type ClusterGeneratorSpec struct {
|
||||
// Kind the kind of this generator.
|
||||
Kind GeneratorKind `json:"kind"`
|
||||
|
||||
// Generator the spec for this generator, must match the kind.
|
||||
Generator GeneratorSpec `json:"generator,omitempty"`
|
||||
}
|
||||
|
||||
type GeneratorSpec struct {
|
||||
// +kubebuilder:validation:Optional
|
||||
PasswordSpec *PasswordSpec `json:"passwordSpec,omitempty"`
|
||||
// +kubebuilder:validation:Optional
|
||||
UUIDSpec *UUIDSpec `json:"uuidSpec,omitempty"`
|
||||
}
|
||||
|
||||
// ClusterGenerator represents a cluster-wide generator
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:storageversion
|
||||
// +kubebuilder:subresource:status
|
||||
// +kubebuilder:resource:scope=Cluster
|
||||
type ClusterGenerator struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec ClusterGeneratorSpec `json:"spec,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
|
||||
// ClusterGeneratorList contains a list of ClusterGenerator resources.
|
||||
type ClusterGeneratorList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
Items []ClusterGenerator `json:"items"`
|
||||
}
|
||||
|
||||
// ! UUID Generator
|
||||
|
||||
// UUIDSpec controls the behavior of the uuid generator.
|
||||
type UUIDSpec struct{}
|
||||
|
||||
// UUID generates a version 4 UUID (e56657e3-764f-11ef-a397-65231a88c216).
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:subresource:status
|
||||
type UUID struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec UUIDSpec `json:"spec,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
|
||||
// UUIDList contains a list of UUID resources.
|
||||
type UUIDList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
Items []UUID `json:"items"`
|
||||
}
|
||||
|
||||
// ! Password Generator
|
||||
|
||||
// PasswordSpec controls the behavior of the password generator.
|
||||
type PasswordSpec struct {
|
||||
// Length of the password to be generated.
|
||||
// Defaults to 24
|
||||
// +kubebuilder:validation:Optional
|
||||
// +kubebuilder:default=24
|
||||
Length int `json:"length"`
|
||||
|
||||
// digits specifies the number of digits in the generated
|
||||
// password. If omitted it defaults to 25% of the length of the password
|
||||
Digits *int `json:"digits,omitempty"`
|
||||
|
||||
// symbols specifies the number of symbol characters in the generated
|
||||
// password. If omitted it defaults to 25% of the length of the password
|
||||
Symbols *int `json:"symbols,omitempty"`
|
||||
|
||||
// symbolCharacters specifies the special characters that should be used
|
||||
// in the generated password.
|
||||
SymbolCharacters *string `json:"symbolCharacters,omitempty"`
|
||||
|
||||
// Set noUpper to disable uppercase characters
|
||||
// +kubebuilder:validation:Optional
|
||||
// +kubebuilder:default=false
|
||||
NoUpper bool `json:"noUpper"`
|
||||
|
||||
// set allowRepeat to true to allow repeating characters.
|
||||
// +kubebuilder:validation:Optional
|
||||
// +kubebuilder:default=false
|
||||
AllowRepeat bool `json:"allowRepeat"`
|
||||
}
|
||||
|
||||
// Password generates a random password based on the
|
||||
// configuration parameters in spec.
|
||||
// You can specify the length, characterset and other attributes.
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:storageversion
|
||||
// +kubebuilder:subresource:status
|
||||
// +kubebuilder:resource:scope=Namespaced
|
||||
type Password struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec PasswordSpec `json:"spec,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
|
||||
// PasswordList contains a list of Password resources.
|
||||
type PasswordList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
Items []Password `json:"items"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
SchemeBuilder.Register(&Password{}, &PasswordList{})
|
||||
SchemeBuilder.Register(&UUID{}, &UUIDList{})
|
||||
SchemeBuilder.Register(&ClusterGenerator{}, &ClusterGeneratorList{})
|
||||
}
|
@@ -29,9 +29,28 @@ type InfisicalPushSecretSecretSource struct {
|
||||
Template *SecretTemplate `json:"template,omitempty"`
|
||||
}
|
||||
|
||||
type SecretPush struct {
|
||||
type GeneratorRef struct {
|
||||
// Specify the Kind of the generator resource
|
||||
// +kubebuilder:validation:Enum=Password;UUID
|
||||
// +kubebuilder:validation:Required
|
||||
Secret InfisicalPushSecretSecretSource `json:"secret"`
|
||||
Kind GeneratorKind `json:"kind"`
|
||||
|
||||
// +kubebuilder:validation:Required
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type SecretPushGenerator struct {
|
||||
// +kubebuilder:validation:Required
|
||||
DestinationSecretName string `json:"destinationSecretName"`
|
||||
// +kubebuilder:validation:Required
|
||||
GeneratorRef GeneratorRef `json:"generatorRef"`
|
||||
}
|
||||
|
||||
type SecretPush struct {
|
||||
// +kubebuilder:validation:Optional
|
||||
Secret *InfisicalPushSecretSecretSource `json:"secret,omitempty"`
|
||||
// +kubebuilder:validation:Optional
|
||||
Generators []SecretPushGenerator `json:"generators,omitempty"`
|
||||
}
|
||||
|
||||
// InfisicalPushSecretSpec defines the desired state of InfisicalPushSecret
|
||||
@@ -52,7 +71,8 @@ type InfisicalPushSecretSpec struct {
|
||||
// +kubebuilder:validation:Required
|
||||
Push SecretPush `json:"push"`
|
||||
|
||||
ResyncInterval string `json:"resyncInterval"`
|
||||
// +kubebuilder:validation:Optional
|
||||
ResyncInterval *string `json:"resyncInterval,omitempty"`
|
||||
|
||||
// Infisical host to pull secrets from
|
||||
// +kubebuilder:validation:Optional
|
||||
|
@@ -96,6 +96,80 @@ func (in *CaReference) DeepCopy() *CaReference {
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ClusterGenerator) DeepCopyInto(out *ClusterGenerator) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
in.Spec.DeepCopyInto(&out.Spec)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterGenerator.
|
||||
func (in *ClusterGenerator) DeepCopy() *ClusterGenerator {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ClusterGenerator)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *ClusterGenerator) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ClusterGeneratorList) DeepCopyInto(out *ClusterGeneratorList) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]ClusterGenerator, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterGeneratorList.
|
||||
func (in *ClusterGeneratorList) DeepCopy() *ClusterGeneratorList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ClusterGeneratorList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *ClusterGeneratorList) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ClusterGeneratorSpec) DeepCopyInto(out *ClusterGeneratorSpec) {
|
||||
*out = *in
|
||||
in.Generator.DeepCopyInto(&out.Generator)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterGeneratorSpec.
|
||||
func (in *ClusterGeneratorSpec) DeepCopy() *ClusterGeneratorSpec {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ClusterGeneratorSpec)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *DynamicSecretDetails) DeepCopyInto(out *DynamicSecretDetails) {
|
||||
*out = *in
|
||||
@@ -143,6 +217,46 @@ func (in *GcpIamAuthDetails) DeepCopy() *GcpIamAuthDetails {
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *GeneratorRef) DeepCopyInto(out *GeneratorRef) {
|
||||
*out = *in
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GeneratorRef.
|
||||
func (in *GeneratorRef) DeepCopy() *GeneratorRef {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(GeneratorRef)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *GeneratorSpec) DeepCopyInto(out *GeneratorSpec) {
|
||||
*out = *in
|
||||
if in.PasswordSpec != nil {
|
||||
in, out := &in.PasswordSpec, &out.PasswordSpec
|
||||
*out = new(PasswordSpec)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.UUIDSpec != nil {
|
||||
in, out := &in.UUIDSpec, &out.UUIDSpec
|
||||
*out = new(UUIDSpec)
|
||||
**out = **in
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GeneratorSpec.
|
||||
func (in *GeneratorSpec) DeepCopy() *GeneratorSpec {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(GeneratorSpec)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *GenericAwsIamAuth) DeepCopyInto(out *GenericAwsIamAuth) {
|
||||
*out = *in
|
||||
@@ -483,6 +597,11 @@ func (in *InfisicalPushSecretSpec) DeepCopyInto(out *InfisicalPushSecretSpec) {
|
||||
out.Destination = in.Destination
|
||||
in.Authentication.DeepCopyInto(&out.Authentication)
|
||||
in.Push.DeepCopyInto(&out.Push)
|
||||
if in.ResyncInterval != nil {
|
||||
in, out := &in.ResyncInterval, &out.ResyncInterval
|
||||
*out = new(string)
|
||||
**out = **in
|
||||
}
|
||||
out.TLS = in.TLS
|
||||
}
|
||||
|
||||
@@ -746,10 +865,107 @@ func (in *ManagedKubeSecretConfig) DeepCopy() *ManagedKubeSecretConfig {
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Password) DeepCopyInto(out *Password) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
in.Spec.DeepCopyInto(&out.Spec)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Password.
|
||||
func (in *Password) DeepCopy() *Password {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Password)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *Password) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *PasswordList) DeepCopyInto(out *PasswordList) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]Password, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PasswordList.
|
||||
func (in *PasswordList) DeepCopy() *PasswordList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(PasswordList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *PasswordList) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *PasswordSpec) DeepCopyInto(out *PasswordSpec) {
|
||||
*out = *in
|
||||
if in.Digits != nil {
|
||||
in, out := &in.Digits, &out.Digits
|
||||
*out = new(int)
|
||||
**out = **in
|
||||
}
|
||||
if in.Symbols != nil {
|
||||
in, out := &in.Symbols, &out.Symbols
|
||||
*out = new(int)
|
||||
**out = **in
|
||||
}
|
||||
if in.SymbolCharacters != nil {
|
||||
in, out := &in.SymbolCharacters, &out.SymbolCharacters
|
||||
*out = new(string)
|
||||
**out = **in
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PasswordSpec.
|
||||
func (in *PasswordSpec) DeepCopy() *PasswordSpec {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(PasswordSpec)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *SecretPush) DeepCopyInto(out *SecretPush) {
|
||||
*out = *in
|
||||
in.Secret.DeepCopyInto(&out.Secret)
|
||||
if in.Secret != nil {
|
||||
in, out := &in.Secret, &out.Secret
|
||||
*out = new(InfisicalPushSecretSecretSource)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.Generators != nil {
|
||||
in, out := &in.Generators, &out.Generators
|
||||
*out = make([]SecretPushGenerator, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretPush.
|
||||
@@ -762,6 +978,22 @@ func (in *SecretPush) DeepCopy() *SecretPush {
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *SecretPushGenerator) DeepCopyInto(out *SecretPushGenerator) {
|
||||
*out = *in
|
||||
out.GeneratorRef = in.GeneratorRef
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretPushGenerator.
|
||||
func (in *SecretPushGenerator) DeepCopy() *SecretPushGenerator {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(SecretPushGenerator)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *SecretScopeInWorkspace) DeepCopyInto(out *SecretScopeInWorkspace) {
|
||||
*out = *in
|
||||
@@ -848,6 +1080,79 @@ func (in *TLSConfig) DeepCopy() *TLSConfig {
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *UUID) DeepCopyInto(out *UUID) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
out.Spec = in.Spec
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UUID.
|
||||
func (in *UUID) DeepCopy() *UUID {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(UUID)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *UUID) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *UUIDList) DeepCopyInto(out *UUIDList) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]UUID, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UUIDList.
|
||||
func (in *UUIDList) DeepCopy() *UUIDList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(UUIDList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *UUIDList) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *UUIDSpec) DeepCopyInto(out *UUIDSpec) {
|
||||
*out = *in
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UUIDSpec.
|
||||
func (in *UUIDSpec) DeepCopy() *UUIDSpec {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(UUIDSpec)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *UniversalAuthDetails) DeepCopyInto(out *UniversalAuthDetails) {
|
||||
*out = *in
|
||||
|
@@ -0,0 +1,90 @@
|
||||
---
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.10.0
|
||||
creationTimestamp: null
|
||||
name: clustergenerators.secrets.infisical.com
|
||||
spec:
|
||||
group: secrets.infisical.com
|
||||
names:
|
||||
kind: ClusterGenerator
|
||||
listKind: ClusterGeneratorList
|
||||
plural: clustergenerators
|
||||
singular: clustergenerator
|
||||
scope: Cluster
|
||||
versions:
|
||||
- name: v1alpha1
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
description: ClusterGenerator represents a cluster-wide generator
|
||||
properties:
|
||||
apiVersion:
|
||||
description: 'APIVersion defines the versioned schema of this representation
|
||||
of an object. Servers should convert recognized schemas to the latest
|
||||
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
|
||||
type: string
|
||||
kind:
|
||||
description: 'Kind is a string value representing the REST resource this
|
||||
object represents. Servers may infer this from the endpoint the client
|
||||
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
spec:
|
||||
properties:
|
||||
generator:
|
||||
description: Generator the spec for this generator, must match the
|
||||
kind.
|
||||
properties:
|
||||
passwordSpec:
|
||||
description: PasswordSpec controls the behavior of the password
|
||||
generator.
|
||||
properties:
|
||||
allowRepeat:
|
||||
default: false
|
||||
description: set allowRepeat to true to allow repeating characters.
|
||||
type: boolean
|
||||
digits:
|
||||
description: digits specifies the number of digits in the
|
||||
generated password. If omitted it defaults to 25% of the
|
||||
length of the password
|
||||
type: integer
|
||||
length:
|
||||
default: 24
|
||||
description: Length of the password to be generated. Defaults
|
||||
to 24
|
||||
type: integer
|
||||
noUpper:
|
||||
default: false
|
||||
description: Set noUpper to disable uppercase characters
|
||||
type: boolean
|
||||
symbolCharacters:
|
||||
description: symbolCharacters specifies the special characters
|
||||
that should be used in the generated password.
|
||||
type: string
|
||||
symbols:
|
||||
description: symbols specifies the number of symbol characters
|
||||
in the generated password. If omitted it defaults to 25%
|
||||
of the length of the password
|
||||
type: integer
|
||||
type: object
|
||||
uuidSpec:
|
||||
description: UUIDSpec controls the behavior of the uuid generator.
|
||||
type: object
|
||||
type: object
|
||||
kind:
|
||||
description: Kind the kind of this generator.
|
||||
enum:
|
||||
- Password
|
||||
- UUID
|
||||
type: string
|
||||
required:
|
||||
- kind
|
||||
type: object
|
||||
type: object
|
||||
served: true
|
||||
storage: true
|
||||
subresources:
|
||||
status: {}
|
@@ -142,6 +142,34 @@ spec:
|
||||
type: string
|
||||
push:
|
||||
properties:
|
||||
generators:
|
||||
items:
|
||||
properties:
|
||||
destinationSecretName:
|
||||
type: string
|
||||
generatorRef:
|
||||
properties:
|
||||
kind:
|
||||
allOf:
|
||||
- enum:
|
||||
- Password
|
||||
- UUID
|
||||
- enum:
|
||||
- Password
|
||||
- UUID
|
||||
description: Specify the Kind of the generator resource
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
required:
|
||||
- kind
|
||||
- name
|
||||
type: object
|
||||
required:
|
||||
- destinationSecretName
|
||||
- generatorRef
|
||||
type: object
|
||||
type: array
|
||||
secret:
|
||||
properties:
|
||||
secretName:
|
||||
@@ -168,8 +196,6 @@ spec:
|
||||
- secretName
|
||||
- secretNamespace
|
||||
type: object
|
||||
required:
|
||||
- secret
|
||||
type: object
|
||||
resyncInterval:
|
||||
type: string
|
||||
@@ -200,7 +226,6 @@ spec:
|
||||
required:
|
||||
- destination
|
||||
- push
|
||||
- resyncInterval
|
||||
type: object
|
||||
status:
|
||||
description: InfisicalPushSecretStatus defines the observed state of InfisicalPushSecret
|
||||
|
@@ -0,0 +1,69 @@
|
||||
---
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.10.0
|
||||
creationTimestamp: null
|
||||
name: passwords.secrets.infisical.com
|
||||
spec:
|
||||
group: secrets.infisical.com
|
||||
names:
|
||||
kind: Password
|
||||
listKind: PasswordList
|
||||
plural: passwords
|
||||
singular: password
|
||||
scope: Namespaced
|
||||
versions:
|
||||
- name: v1alpha1
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
description: Password generates a random password based on the configuration
|
||||
parameters in spec. You can specify the length, characterset and other attributes.
|
||||
properties:
|
||||
apiVersion:
|
||||
description: 'APIVersion defines the versioned schema of this representation
|
||||
of an object. Servers should convert recognized schemas to the latest
|
||||
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
|
||||
type: string
|
||||
kind:
|
||||
description: 'Kind is a string value representing the REST resource this
|
||||
object represents. Servers may infer this from the endpoint the client
|
||||
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
spec:
|
||||
description: PasswordSpec controls the behavior of the password generator.
|
||||
properties:
|
||||
allowRepeat:
|
||||
default: false
|
||||
description: set allowRepeat to true to allow repeating characters.
|
||||
type: boolean
|
||||
digits:
|
||||
description: digits specifies the number of digits in the generated
|
||||
password. If omitted it defaults to 25% of the length of the password
|
||||
type: integer
|
||||
length:
|
||||
default: 24
|
||||
description: Length of the password to be generated. Defaults to 24
|
||||
type: integer
|
||||
noUpper:
|
||||
default: false
|
||||
description: Set noUpper to disable uppercase characters
|
||||
type: boolean
|
||||
symbolCharacters:
|
||||
description: symbolCharacters specifies the special characters that
|
||||
should be used in the generated password.
|
||||
type: string
|
||||
symbols:
|
||||
description: symbols specifies the number of symbol characters in
|
||||
the generated password. If omitted it defaults to 25% of the length
|
||||
of the password
|
||||
type: integer
|
||||
type: object
|
||||
type: object
|
||||
served: true
|
||||
storage: true
|
||||
subresources:
|
||||
status: {}
|
@@ -0,0 +1,42 @@
|
||||
---
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.10.0
|
||||
creationTimestamp: null
|
||||
name: uuids.secrets.infisical.com
|
||||
spec:
|
||||
group: secrets.infisical.com
|
||||
names:
|
||||
kind: UUID
|
||||
listKind: UUIDList
|
||||
plural: uuids
|
||||
singular: uuid
|
||||
scope: Namespaced
|
||||
versions:
|
||||
- name: v1alpha1
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
description: UUID generates a version 4 UUID (e56657e3-764f-11ef-a397-65231a88c216).
|
||||
properties:
|
||||
apiVersion:
|
||||
description: 'APIVersion defines the versioned schema of this representation
|
||||
of an object. Servers should convert recognized schemas to the latest
|
||||
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
|
||||
type: string
|
||||
kind:
|
||||
description: 'Kind is a string value representing the REST resource this
|
||||
object represents. Servers may infer this from the endpoint the client
|
||||
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
spec:
|
||||
description: UUIDSpec controls the behavior of the uuid generator.
|
||||
type: object
|
||||
type: object
|
||||
served: true
|
||||
storage: true
|
||||
subresources:
|
||||
status: {}
|
@@ -5,6 +5,7 @@ resources:
|
||||
- bases/secrets.infisical.com_infisicalsecrets.yaml
|
||||
- bases/secrets.infisical.com_infisicalpushsecrets.yaml
|
||||
- bases/secrets.infisical.com_infisicaldynamicsecrets.yaml
|
||||
- bases/secrets.infisical.com_clustergenerators.yaml
|
||||
#+kubebuilder:scaffold:crdkustomizeresource
|
||||
|
||||
patchesStrategicMerge:
|
||||
|
@@ -74,6 +74,18 @@ rules:
|
||||
- tokenreviews
|
||||
verbs:
|
||||
- create
|
||||
- apiGroups:
|
||||
- secrets.infisical.com
|
||||
resources:
|
||||
- clustergenerators
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- get
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- secrets.infisical.com
|
||||
resources:
|
||||
|
@@ -0,0 +1,14 @@
|
||||
apiVersion: secrets.infisical.com/v1alpha1
|
||||
kind: ClusterGenerator
|
||||
metadata:
|
||||
name: password-generator
|
||||
spec:
|
||||
kind: Password
|
||||
generator:
|
||||
passwordSpec:
|
||||
length: 10
|
||||
digits: 5
|
||||
symbols: 5
|
||||
symbolCharacters: "-_$@"
|
||||
noUpper: false
|
||||
allowRepeat: true
|
@@ -393,7 +393,7 @@ func (r *InfisicalDynamicSecretReconciler) ReconcileInfisicalDynamicSecret(ctx c
|
||||
|
||||
// Max TTL
|
||||
if infisicalDynamicSecret.Status.MaxTTL != "" {
|
||||
maxTTLDuration, err := util.ConvertIntervalToDuration(infisicalDynamicSecret.Status.MaxTTL)
|
||||
maxTTLDuration, err := util.ConvertIntervalToDuration(&infisicalDynamicSecret.Status.MaxTTL)
|
||||
if err != nil {
|
||||
return defaultNextReconcile, fmt.Errorf("unable to parse MaxTTL duration: %w", err)
|
||||
}
|
||||
|
@@ -51,7 +51,7 @@ func (r *InfisicalPushSecretReconciler) GetLogger(req ctrl.Request) logr.Logger
|
||||
//+kubebuilder:rbac:groups="",resources=pods,verbs=get;list
|
||||
//+kubebuilder:rbac:groups="authentication.k8s.io",resources=tokenreviews,verbs=create
|
||||
//+kubebuilder:rbac:groups="",resources=serviceaccounts/token,verbs=create
|
||||
|
||||
// +kubebuilder:rbac:groups=secrets.infisical.com,resources=clustergenerators,verbs=get;list;watch;create;update;patch;delete
|
||||
// Reconcile is part of the main kubernetes reconciliation loop which aims to
|
||||
// move the current state of the cluster closer to the desired state.
|
||||
// For more details, check Reconcile and its Result here:
|
||||
@@ -108,23 +108,30 @@ func (r *InfisicalPushSecretReconciler) Reconcile(ctx context.Context, req ctrl.
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
if infisicalPushSecretCRD.Spec.ResyncInterval != "" {
|
||||
if infisicalPushSecretCRD.Spec.Push.Secret == nil && infisicalPushSecretCRD.Spec.Push.Generators == nil {
|
||||
logger.Info("No secret or generators found, skipping reconciliation. Please define ")
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
duration, err := util.ConvertIntervalToDuration(infisicalPushSecretCRD.Spec.ResyncInterval)
|
||||
duration, err := util.ConvertIntervalToDuration(infisicalPushSecretCRD.Spec.ResyncInterval)
|
||||
|
||||
if err != nil {
|
||||
if err != nil {
|
||||
// if resyncInterval is nil, we don't want to reconcile automatically
|
||||
if infisicalPushSecretCRD.Spec.ResyncInterval != nil {
|
||||
logger.Error(err, fmt.Sprintf("unable to convert resync interval to duration. Will requeue after [requeueTime=%v]", requeueTime))
|
||||
return ctrl.Result{
|
||||
RequeueAfter: requeueTime,
|
||||
}, nil
|
||||
} else {
|
||||
logger.Error(err, "unable to convert resync interval to duration")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
}
|
||||
|
||||
requeueTime = duration
|
||||
requeueTime = duration
|
||||
|
||||
if requeueTime != 0 {
|
||||
logger.Info(fmt.Sprintf("Manual re-sync interval set. Interval: %v", requeueTime))
|
||||
|
||||
} else {
|
||||
logger.Info(fmt.Sprintf("Re-sync interval set. Interval: %v", requeueTime))
|
||||
}
|
||||
|
||||
// Check if the resource is already marked for deletion
|
||||
@@ -137,10 +144,15 @@ func (r *InfisicalPushSecretReconciler) Reconcile(ctx context.Context, req ctrl.
|
||||
// Get modified/default config
|
||||
infisicalConfig, err := controllerhelpers.GetInfisicalConfigMap(ctx, r.Client)
|
||||
if err != nil {
|
||||
logger.Error(err, fmt.Sprintf("unable to fetch infisical-config. Will requeue after [requeueTime=%v]", requeueTime))
|
||||
return ctrl.Result{
|
||||
RequeueAfter: requeueTime,
|
||||
}, nil
|
||||
if requeueTime != 0 {
|
||||
logger.Error(err, fmt.Sprintf("unable to fetch infisical-config. Will requeue after [requeueTime=%v]", requeueTime))
|
||||
return ctrl.Result{
|
||||
RequeueAfter: requeueTime,
|
||||
}, nil
|
||||
} else {
|
||||
logger.Error(err, "unable to fetch infisical-config")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
}
|
||||
|
||||
if infisicalPushSecretCRD.Spec.HostAPI == "" {
|
||||
@@ -152,10 +164,15 @@ func (r *InfisicalPushSecretReconciler) Reconcile(ctx context.Context, req ctrl.
|
||||
if infisicalPushSecretCRD.Spec.TLS.CaRef.SecretName != "" {
|
||||
api.API_CA_CERTIFICATE, err = r.getInfisicalCaCertificateFromKubeSecret(ctx, infisicalPushSecretCRD)
|
||||
if err != nil {
|
||||
logger.Error(err, fmt.Sprintf("unable to fetch CA certificate. Will requeue after [requeueTime=%v]", requeueTime))
|
||||
return ctrl.Result{
|
||||
RequeueAfter: requeueTime,
|
||||
}, nil
|
||||
if requeueTime != 0 {
|
||||
logger.Error(err, fmt.Sprintf("unable to fetch CA certificate. Will requeue after [requeueTime=%v]", requeueTime))
|
||||
return ctrl.Result{
|
||||
RequeueAfter: requeueTime,
|
||||
}, nil
|
||||
} else {
|
||||
logger.Error(err, "unable to fetch CA certificate")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info("Using custom CA certificate...")
|
||||
@@ -167,17 +184,27 @@ func (r *InfisicalPushSecretReconciler) Reconcile(ctx context.Context, req ctrl.
|
||||
r.SetReconcileStatusCondition(ctx, &infisicalPushSecretCRD, err)
|
||||
|
||||
if err != nil {
|
||||
logger.Error(err, fmt.Sprintf("unable to reconcile Infisical Push Secret. Will requeue after [requeueTime=%v]", requeueTime))
|
||||
return ctrl.Result{
|
||||
RequeueAfter: requeueTime,
|
||||
}, nil
|
||||
if requeueTime != 0 {
|
||||
logger.Error(err, fmt.Sprintf("unable to reconcile Infisical Push Secret. Will requeue after [requeueTime=%v]", requeueTime))
|
||||
return ctrl.Result{
|
||||
RequeueAfter: requeueTime,
|
||||
}, nil
|
||||
} else {
|
||||
logger.Error(err, "unable to reconcile Infisical Push Secret")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
}
|
||||
|
||||
// Sync again after the specified time
|
||||
logger.Info(fmt.Sprintf("Operator will requeue after [%v]", requeueTime))
|
||||
return ctrl.Result{
|
||||
RequeueAfter: requeueTime,
|
||||
}, nil
|
||||
if requeueTime != 0 {
|
||||
logger.Info(fmt.Sprintf("Operator will requeue after [%v]", requeueTime))
|
||||
return ctrl.Result{
|
||||
RequeueAfter: requeueTime,
|
||||
}, nil
|
||||
} else {
|
||||
logger.Info("Operator will reconcile on next spec change")
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (r *InfisicalPushSecretReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
@@ -228,27 +255,67 @@ func (r *InfisicalPushSecretReconciler) SetupWithManager(mgr ctrl.Manager) error
|
||||
)).
|
||||
Watches(
|
||||
&source.Kind{Type: &corev1.Secret{}},
|
||||
handler.EnqueueRequestsFromMapFunc(func(o client.Object) []reconcile.Request {
|
||||
ctx := context.Background()
|
||||
pushSecrets := &secretsv1alpha1.InfisicalPushSecretList{}
|
||||
if err := r.List(ctx, pushSecrets); err != nil {
|
||||
return []reconcile.Request{}
|
||||
}
|
||||
|
||||
requests := []reconcile.Request{}
|
||||
for _, pushSecret := range pushSecrets.Items {
|
||||
if pushSecret.Spec.Push.Secret.SecretName == o.GetName() &&
|
||||
pushSecret.Spec.Push.Secret.SecretNamespace == o.GetNamespace() {
|
||||
requests = append(requests, reconcile.Request{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: pushSecret.GetName(),
|
||||
Namespace: pushSecret.GetNamespace(),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
return requests
|
||||
}),
|
||||
handler.EnqueueRequestsFromMapFunc(r.findPushSecretsForSecret),
|
||||
).
|
||||
Watches(
|
||||
&source.Kind{Type: &secretsv1alpha1.ClusterGenerator{}},
|
||||
handler.EnqueueRequestsFromMapFunc(r.findPushSecretsForClusterGenerator),
|
||||
).
|
||||
Complete(r)
|
||||
}
|
||||
|
||||
func (r *InfisicalPushSecretReconciler) findPushSecretsForClusterGenerator(o client.Object) []reconcile.Request {
|
||||
ctx := context.Background()
|
||||
pushSecrets := &secretsv1alpha1.InfisicalPushSecretList{}
|
||||
if err := r.List(ctx, pushSecrets); err != nil {
|
||||
return []reconcile.Request{}
|
||||
}
|
||||
|
||||
clusterGenerator, ok := o.(*secretsv1alpha1.ClusterGenerator)
|
||||
if !ok {
|
||||
return []reconcile.Request{}
|
||||
}
|
||||
|
||||
requests := []reconcile.Request{}
|
||||
for _, pushSecret := range pushSecrets.Items {
|
||||
if pushSecret.Spec.Push.Generators != nil {
|
||||
for _, generator := range pushSecret.Spec.Push.Generators {
|
||||
if generator.GeneratorRef.Name == clusterGenerator.GetName() {
|
||||
requests = append(requests, reconcile.Request{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: pushSecret.GetName(),
|
||||
Namespace: pushSecret.GetNamespace(),
|
||||
},
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return requests
|
||||
}
|
||||
|
||||
func (r *InfisicalPushSecretReconciler) findPushSecretsForSecret(o client.Object) []reconcile.Request {
|
||||
ctx := context.Background()
|
||||
pushSecrets := &secretsv1alpha1.InfisicalPushSecretList{}
|
||||
if err := r.List(ctx, pushSecrets); err != nil {
|
||||
return []reconcile.Request{}
|
||||
}
|
||||
|
||||
requests := []reconcile.Request{}
|
||||
for _, pushSecret := range pushSecrets.Items {
|
||||
if pushSecret.Spec.Push.Secret != nil &&
|
||||
pushSecret.Spec.Push.Secret.SecretName == o.GetName() &&
|
||||
pushSecret.Spec.Push.Secret.SecretNamespace == o.GetNamespace() {
|
||||
requests = append(requests, reconcile.Request{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: pushSecret.GetName(),
|
||||
Namespace: pushSecret.GetNamespace(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return requests
|
||||
}
|
||||
|
@@ -19,6 +19,7 @@ import (
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
generatorUtil "github.com/Infisical/infisical/k8-operator/packages/generator"
|
||||
infisicalSdk "github.com/infisical/go-sdk"
|
||||
k8Errors "k8s.io/apimachinery/pkg/api/errors"
|
||||
)
|
||||
@@ -106,6 +107,52 @@ func (r *InfisicalPushSecretReconciler) updateResourceVariables(infisicalPushSec
|
||||
infisicalPushSecretResourceVariablesMap[string(infisicalPushSecret.UID)] = resourceVariables
|
||||
}
|
||||
|
||||
func (r *InfisicalPushSecretReconciler) processGenerators(ctx context.Context, infisicalPushSecret v1alpha1.InfisicalPushSecret) (map[string]string, error) {
|
||||
|
||||
processedSecrets := make(map[string]string)
|
||||
|
||||
if len(infisicalPushSecret.Spec.Push.Generators) == 0 {
|
||||
return processedSecrets, nil
|
||||
}
|
||||
|
||||
for _, generator := range infisicalPushSecret.Spec.Push.Generators {
|
||||
generatorRef := generator.GeneratorRef
|
||||
|
||||
clusterGenerator := &v1alpha1.ClusterGenerator{}
|
||||
err := r.Client.Get(ctx, types.NamespacedName{Name: generatorRef.Name}, clusterGenerator)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get ClusterGenerator resource [err=%s]", err)
|
||||
}
|
||||
if generatorRef.Kind == v1alpha1.GeneratorKindPassword {
|
||||
// get the custom ClusterGenerator resource from the cluster
|
||||
|
||||
if clusterGenerator.Spec.Generator.PasswordSpec == nil {
|
||||
return nil, fmt.Errorf("password spec is not defined in the ClusterGenerator resource")
|
||||
}
|
||||
|
||||
password, err := generatorUtil.GeneratorPassword(*clusterGenerator.Spec.Generator.PasswordSpec)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to generate password [err=%s]", err)
|
||||
}
|
||||
|
||||
processedSecrets[generator.DestinationSecretName] = password
|
||||
}
|
||||
|
||||
if generatorRef.Kind == v1alpha1.GeneratorKindUUID {
|
||||
|
||||
uuid, err := generatorUtil.GeneratorUUID()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to generate UUID [err=%s]", err)
|
||||
}
|
||||
|
||||
processedSecrets[generator.DestinationSecretName] = uuid
|
||||
}
|
||||
}
|
||||
|
||||
return processedSecrets, nil
|
||||
|
||||
}
|
||||
|
||||
func (r *InfisicalPushSecretReconciler) processTemplatedSecrets(infisicalPushSecret v1alpha1.InfisicalPushSecret, kubePushSecret *corev1.Secret, destination v1alpha1.InfisicalPushSecretDestination) (map[string]string, error) {
|
||||
|
||||
processedSecrets := make(map[string]string)
|
||||
@@ -172,18 +219,31 @@ func (r *InfisicalPushSecretReconciler) ReconcileInfisicalPushSecret(ctx context
|
||||
})
|
||||
}
|
||||
|
||||
kubePushSecret, err := util.GetKubeSecretByNamespacedName(ctx, r.Client, types.NamespacedName{
|
||||
Namespace: infisicalPushSecret.Spec.Push.Secret.SecretNamespace,
|
||||
Name: infisicalPushSecret.Spec.Push.Secret.SecretName,
|
||||
})
|
||||
processedSecrets := make(map[string]string)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to fetch kube secret [err=%s]", err)
|
||||
if infisicalPushSecret.Spec.Push.Secret != nil {
|
||||
kubePushSecret, err := util.GetKubeSecretByNamespacedName(ctx, r.Client, types.NamespacedName{
|
||||
Namespace: infisicalPushSecret.Spec.Push.Secret.SecretNamespace,
|
||||
Name: infisicalPushSecret.Spec.Push.Secret.SecretName,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to fetch kube secret [err=%s]", err)
|
||||
}
|
||||
|
||||
processedSecrets, err = r.processTemplatedSecrets(infisicalPushSecret, kubePushSecret, infisicalPushSecret.Spec.Destination)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to process templated secrets [err=%s]", err)
|
||||
}
|
||||
}
|
||||
|
||||
processedSecrets, err := r.processTemplatedSecrets(infisicalPushSecret, kubePushSecret, infisicalPushSecret.Spec.Destination)
|
||||
generatorSecrets, err := r.processGenerators(ctx, infisicalPushSecret)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to process templated secrets [err=%s]", err)
|
||||
return fmt.Errorf("unable to process generators [err=%s]", err)
|
||||
}
|
||||
|
||||
for key, value := range generatorSecrets {
|
||||
processedSecrets[key] = value
|
||||
}
|
||||
|
||||
destination := infisicalPushSecret.Spec.Destination
|
||||
|
@@ -4,10 +4,12 @@ go 1.21
|
||||
|
||||
require (
|
||||
github.com/Masterminds/sprig/v3 v3.3.0
|
||||
github.com/aws/smithy-go v1.20.3
|
||||
github.com/infisical/go-sdk v0.4.4
|
||||
github.com/lestrrat-go/jwx/v2 v2.1.4
|
||||
github.com/onsi/ginkgo/v2 v2.6.0
|
||||
github.com/onsi/gomega v1.24.1
|
||||
github.com/sethvargo/go-password v0.3.1
|
||||
k8s.io/apimachinery v0.26.1
|
||||
k8s.io/client-go v0.26.1
|
||||
sigs.k8s.io/controller-runtime v0.14.4
|
||||
@@ -34,7 +36,6 @@ require (
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.22.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.30.1 // indirect
|
||||
github.com/aws/smithy-go v1.20.3 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
@@ -85,7 +86,7 @@ require (
|
||||
github.com/google/gnostic v0.5.7-v3refs // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/google/gofuzz v1.1.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/imdario/mergo v0.3.12 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
|
@@ -338,6 +338,8 @@ github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZV
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
|
||||
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||
github.com/sethvargo/go-password v0.3.1 h1:WqrLTjo7X6AcVYfC6R7GtSyuUQR9hGyAj/f1PYQZCJU=
|
||||
github.com/sethvargo/go-password v0.3.1/go.mod h1:rXofC1zT54N7R8K/h1WDUdkf9BOx5OptoxrMBcrXzvs=
|
||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
|
1
k8-operator/packages/generator/generator.go
Normal file
@@ -0,0 +1 @@
|
||||
package generator
|
76
k8-operator/packages/generator/password.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"github.com/Infisical/infisical/k8-operator/api/v1alpha1"
|
||||
"github.com/sethvargo/go-password/password"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultLength = 24
|
||||
defaultSymbolChars = "~!@#$%^&*()_+`-={}|[]\\:\"<>?,./"
|
||||
digitFactor = 0.25
|
||||
symbolFactor = 0.25
|
||||
)
|
||||
|
||||
func generateSafePassword(
|
||||
passLen int,
|
||||
symbols int,
|
||||
symbolCharacters string,
|
||||
digits int,
|
||||
noUpper bool,
|
||||
allowRepeat bool,
|
||||
) (string, error) {
|
||||
gen, err := password.NewGenerator(&password.GeneratorInput{
|
||||
Symbols: symbolCharacters,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return gen.Generate(
|
||||
passLen,
|
||||
digits,
|
||||
symbols,
|
||||
noUpper,
|
||||
allowRepeat,
|
||||
)
|
||||
}
|
||||
|
||||
func GeneratorPassword(spec v1alpha1.PasswordSpec) (string, error) {
|
||||
|
||||
symbolCharacters := defaultSymbolChars
|
||||
|
||||
if spec.SymbolCharacters != nil && *spec.SymbolCharacters != "" {
|
||||
symbolCharacters = *spec.SymbolCharacters
|
||||
}
|
||||
|
||||
passwordLength := defaultLength
|
||||
|
||||
if spec.Length != 0 {
|
||||
passwordLength = spec.Length
|
||||
}
|
||||
|
||||
digits := int(float32(passwordLength) * digitFactor)
|
||||
if spec.Digits != nil {
|
||||
digits = *spec.Digits
|
||||
}
|
||||
|
||||
symbols := int(float32(passwordLength) * symbolFactor)
|
||||
if spec.Symbols != nil {
|
||||
symbols = *spec.Symbols
|
||||
}
|
||||
|
||||
pass, err := generateSafePassword(
|
||||
passwordLength,
|
||||
symbols,
|
||||
symbolCharacters,
|
||||
digits,
|
||||
spec.NoUpper,
|
||||
spec.AllowRepeat,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return pass, nil
|
||||
}
|
10
k8-operator/packages/generator/uuid.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func GeneratorUUID() (string, error) {
|
||||
uuid := uuid.New().String()
|
||||
return uuid, nil
|
||||
}
|