Compare commits

...

50 Commits

Author SHA1 Message Date
d4728e31c1 Keep Domains on writeInitalConfig for CLI login 2025-03-24 09:50:22 -03:00
f9a5b46365 Merge pull request #3293 from Infisical/feat/addRecursiveSearchToFoldersGetEndpoint
Add recursive flag to folders get endpoint to retrieve all nested folders
2025-03-24 08:15:30 -03:00
d65deab0af Merge pull request #3295 from akhilmhdh/fix/cli-gateway
Gateway minor fixes
2025-03-23 22:57:53 +05:30
=
61591742e4 feat: error propagation for quic client on backend 2025-03-23 22:28:19 +05:30
=
54b13a9daa feat: resolved small gateway issues and added gateway uninstall command 2025-03-23 22:24:41 +05:30
4adf0aa1e2 Merge pull request #3292 from akhilmhdh/feat/k8-auth
K8s reviewer jwt optional and self client method
2025-03-21 21:04:17 -04:00
3d3ee746cf improve docs 2025-03-21 20:37:41 -04:00
07e4358d00 Merge pull request #3294 from Infisical/daniel/airflow-docs
docs: added apache airflow link
2025-03-22 03:55:54 +04:00
962dd5d919 docs: added apache airflow link 2025-03-22 03:51:39 +04:00
52bd1afb0a Move booleanSchema to sanitizedSchema - fix default value 2025-03-21 18:35:32 -03:00
d918dd8967 Move booleanSchema to sanitizedSchema 2025-03-21 18:29:55 -03:00
e2e0f6a346 Add recursive flag to folders get endpoint to retrieve all nested folders 2025-03-21 18:08:22 -03:00
=
326cb99732 feat: updated docs 2025-03-22 01:07:35 +05:30
=
341b63c61c feat: updated frontend to make reviewer jwt optional 2025-03-22 01:07:28 +05:30
=
81b026865c feat: updated backend for identity auth with reviewer optional 2025-03-22 01:06:58 +05:30
f50c72c033 Merge pull request #3291 from Infisical/fix/removeDragonsFromInviteUserViews
Remove dragons from /signupinvite and /requestnewinvite
2025-03-21 16:11:00 -03:00
e1046e2d56 Remove dragons from /signupinvite and /requestnewinvite 2025-03-21 16:00:43 -03:00
ed3fa8add1 Merge pull request #3264 from Infisical/feat/addSecretImportsToOverview
Show when folders have imports in secret overview page
2025-03-21 15:30:03 -03:00
d123283849 Merge branch 'main' into feat/addSecretImportsToOverview 2025-03-21 15:10:37 -03:00
d7fd44b845 Fix secret imports issue on isSecretPresentInEnv validation 2025-03-21 15:06:17 -03:00
3ffee049ee Merge pull request #3268 from Infisical/feat/addCustomDomainToWindmillIntegration
Add Windmill custom api url domain
2025-03-21 14:19:39 -03:00
524462d7bc Merge pull request #3276 from akhilmhdh/feat/folder-sql-improvement
Indexed and optimized folder queries
2025-03-21 09:55:10 -04:00
351e573fea Merge pull request #3288 from Infisical/0xArshdeep-patch-1
Update terraform.mdx
2025-03-20 16:10:00 -07:00
f1bc26e2e5 Update terraform.mdx 2025-03-20 16:09:01 -07:00
8aeb607f6e Merge pull request #3287 from akhilmhdh/fix/patch-4
Fix/patch 4
2025-03-20 17:32:32 -04:00
=
e530b7a788 feat: reduced to two 2025-03-21 02:59:44 +05:30
=
bf61090b5a feat: added cache clear and refresh with session limit 2025-03-21 02:58:59 +05:30
106b068a51 Merge pull request #3286 from akhilmhdh/fix/patch-4
feat: removed refresh
2025-03-20 17:16:01 -04:00
=
6f0a97a2fa feat: removed refresh 2025-03-21 02:43:10 +05:30
5d604be091 Merge pull request #3285 from akhilmhdh/fix/patch-4
feat: return fastify res and making it async
2025-03-20 17:01:03 -04:00
=
905cf47d90 feat: return fastify res and making it async 2025-03-21 02:26:33 +05:30
2c40d316f4 Merge pull request #3284 from Infisical/misc/add-flag-for-disabling-worker-queue
misc: add flag for disabling workers
2025-03-21 03:49:57 +08:00
32521523c1 misc: add flag for disabling workers 2025-03-21 03:46:17 +08:00
3a2e8939b1 Merge pull request #3282 from Infisical/misc/add-event-loop-stats
misc: add event loop stats
2025-03-21 01:54:12 +08:00
a6d9c74054 Merge pull request #3272 from akhilmhdh/feat/metadata-audit-log
Permission metadata data in audit log
2025-03-20 13:47:52 -04:00
82520a7f0a Check local urls for cloud instances on windmill custom domain input 2025-03-19 15:54:07 -03:00
af236ba892 Avoid throwing forbidden on non accessible resources and return an empty response for those 2025-03-19 15:30:05 -03:00
=
c4b7d4618d feat: updated ui 2025-03-19 20:33:31 +05:30
=
003f2b003d feat: indexed and optimized folder queries 2025-03-19 19:46:05 +05:30
=
747b5ec68d feat: updated doc 2025-03-19 15:00:36 +05:30
=
ed0dc324a3 feat: updated audit log ui 2025-03-19 13:28:26 +05:30
=
1c13ed54af feat: updated audit log to permission metadataw 2025-03-19 13:28:26 +05:30
8abfea0409 Fix getAppsWindmill url field type 2025-03-18 19:14:49 -03:00
ce4adccc80 Add Windmill custom api url domain to connection details page 2025-03-18 19:07:11 -03:00
dcd3b5df56 Add Windmill custom api url domain 2025-03-18 19:01:52 -03:00
f6425480ca Merge branch 'main' into feat/addSecretImportsToOverview 2025-03-18 16:45:47 -03:00
a3e9392a2f Fix totalCount missing import count 2025-03-18 16:34:31 -03:00
633a2ae985 Rework of secret imports on overview page 2025-03-18 16:26:15 -03:00
e67a8f9c05 Add filter imports and minor improvements to allSecretImports logic 2025-03-17 22:23:31 -03:00
ad110f490c Show when folders have imports in secret overview page 2025-03-17 19:13:06 -03:00
60 changed files with 1150 additions and 576 deletions

View File

@ -106,6 +106,7 @@ declare module "@fastify/request-context" {
claims: Record<string, string>; claims: Record<string, string>;
}; };
}; };
identityPermissionMetadata?: Record<string, unknown>; // filled by permission service
} }
} }

View File

@ -0,0 +1,23 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const doesParentColumExist = await knex.schema.hasColumn(TableName.SecretFolder, "parentId");
const doesNameColumnExist = await knex.schema.hasColumn(TableName.SecretFolder, "name");
if (doesParentColumExist && doesNameColumnExist) {
await knex.schema.alterTable(TableName.SecretFolder, (t) => {
t.index(["parentId", "name"]);
});
}
}
export async function down(knex: Knex): Promise<void> {
const doesParentColumExist = await knex.schema.hasColumn(TableName.SecretFolder, "parentId");
const doesNameColumnExist = await knex.schema.hasColumn(TableName.SecretFolder, "name");
if (doesParentColumExist && doesNameColumnExist) {
await knex.schema.alterTable(TableName.SecretFolder, (t) => {
t.dropIndex(["parentId", "name"]);
});
}
}

View File

@ -0,0 +1,19 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasReviewerJwtCol = await knex.schema.hasColumn(
TableName.IdentityKubernetesAuth,
"encryptedKubernetesTokenReviewerJwt"
);
if (hasReviewerJwtCol) {
await knex.schema.alterTable(TableName.IdentityKubernetesAuth, (t) => {
t.binary("encryptedKubernetesTokenReviewerJwt").nullable().alter();
});
}
}
export async function down(): Promise<void> {
// we can't make it back to non nullable, it will fail
}

View File

@ -28,7 +28,7 @@ export const IdentityKubernetesAuthsSchema = z.object({
allowedNamespaces: z.string(), allowedNamespaces: z.string(),
allowedNames: z.string(), allowedNames: z.string(),
allowedAudience: z.string(), allowedAudience: z.string(),
encryptedKubernetesTokenReviewerJwt: zodBuffer, encryptedKubernetesTokenReviewerJwt: zodBuffer.nullable().optional(),
encryptedKubernetesCaCertificate: zodBuffer.nullable().optional() encryptedKubernetesCaCertificate: zodBuffer.nullable().optional()
}); });

View File

@ -1,8 +1,10 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { requestContext } from "@fastify/request-context";
import { ActionProjectType } from "@app/db/schemas"; import { ActionProjectType } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { ActorType } from "@app/services/auth/auth-type";
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission"; import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPermissionServiceFactory } from "../permission/permission-service"; import { TPermissionServiceFactory } from "../permission/permission-service";
@ -81,8 +83,12 @@ export const auditLogServiceFactory = ({
if (!data.projectId && !data.orgId) if (!data.projectId && !data.orgId)
throw new BadRequestError({ message: "Must specify either project id or org id" }); throw new BadRequestError({ message: "Must specify either project id or org id" });
} }
const el = { ...data };
return auditLogQueue.pushToLog(data); if (el.actor.type === ActorType.USER || el.actor.type === ActorType.IDENTITY) {
const permissionMetadata = requestContext.get("identityPermissionMetadata");
el.actor.metadata.permission = permissionMetadata;
}
return auditLogQueue.pushToLog(el);
}; };
return { return {

View File

@ -290,6 +290,7 @@ interface UserActorMetadata {
userId: string; userId: string;
email?: string | null; email?: string | null;
username: string; username: string;
permission?: Record<string, unknown>;
} }
interface ServiceActorMetadata { interface ServiceActorMetadata {
@ -300,6 +301,7 @@ interface ServiceActorMetadata {
interface IdentityActorMetadata { interface IdentityActorMetadata {
identityId: string; identityId: string;
name: string; name: string;
permission?: Record<string, unknown>;
} }
interface ScimClientActorMetadata {} interface ScimClientActorMetadata {}

View File

@ -244,22 +244,20 @@ export const permissionServiceFactory = ({
const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges)); const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges));
const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false }); const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false });
const metadataKeyValuePair = escapeHandlebarsMissingDict( const unescapedMetadata = objectify(
objectify(
userProjectPermission.metadata, userProjectPermission.metadata,
(i) => i.key, (i) => i.key,
(i) => i.value (i) => i.value
),
"identity.metadata"
); );
const templateValue = { const metadataKeyValuePair = escapeHandlebarsMissingDict(unescapedMetadata, "identity.metadata");
requestContext.set("identityPermissionMetadata", { metadata: unescapedMetadata });
const interpolateRules = templatedRules(
{
identity: {
id: userProjectPermission.userId, id: userProjectPermission.userId,
username: userProjectPermission.username, username: userProjectPermission.username,
metadata: metadataKeyValuePair metadata: metadataKeyValuePair
}; }
const interpolateRules = templatedRules(
{
identity: templateValue
}, },
{ data: false } { data: false }
); );
@ -331,15 +329,16 @@ export const permissionServiceFactory = ({
? escapeHandlebarsMissingDict(unescapedIdentityAuthInfo as never, "identity.auth") ? escapeHandlebarsMissingDict(unescapedIdentityAuthInfo as never, "identity.auth")
: {}; : {};
const metadataKeyValuePair = escapeHandlebarsMissingDict(unescapedMetadata, "identity.metadata"); const metadataKeyValuePair = escapeHandlebarsMissingDict(unescapedMetadata, "identity.metadata");
const templateValue = {
requestContext.set("identityPermissionMetadata", { metadata: unescapedMetadata, auth: unescapedIdentityAuthInfo });
const interpolateRules = templatedRules(
{
identity: {
id: identityProjectPermission.identityId, id: identityProjectPermission.identityId,
username: identityProjectPermission.username, username: identityProjectPermission.username,
metadata: metadataKeyValuePair, metadata: metadataKeyValuePair,
auth: identityAuthInfo auth: identityAuthInfo
}; }
const interpolateRules = templatedRules(
{
identity: templateValue
}, },
{ data: false } { data: false }
); );
@ -440,14 +439,13 @@ export const permissionServiceFactory = ({
), ),
"identity.metadata" "identity.metadata"
); );
const templateValue = { const interpolateRules = templatedRules(
{
identity: {
id: userProjectPermission.userId, id: userProjectPermission.userId,
username: userProjectPermission.username, username: userProjectPermission.username,
metadata: metadataKeyValuePair metadata: metadataKeyValuePair
}; }
const interpolateRules = templatedRules(
{
identity: templateValue
}, },
{ data: false } { data: false }
); );
@ -487,14 +485,13 @@ export const permissionServiceFactory = ({
), ),
"identity.metadata" "identity.metadata"
); );
const templateValue = { const interpolateRules = templatedRules(
{
identity: {
id: identityProjectPermission.identityId, id: identityProjectPermission.identityId,
username: identityProjectPermission.username, username: identityProjectPermission.username,
metadata: metadataKeyValuePair metadata: metadataKeyValuePair
}; }
const interpolateRules = templatedRules(
{
identity: templateValue
}, },
{ data: false } { data: false }
); );

View File

@ -244,7 +244,7 @@ export const KUBERNETES_AUTH = {
kubernetesHost: "The host string, host:port pair, or URL to the base of the Kubernetes API server.", kubernetesHost: "The host string, host:port pair, or URL to the base of the Kubernetes API server.",
caCert: "The PEM-encoded CA cert for the Kubernetes API server.", caCert: "The PEM-encoded CA cert for the Kubernetes API server.",
tokenReviewerJwt: tokenReviewerJwt:
"The long-lived service account JWT token for Infisical to access the TokenReview API to validate other service account JWT tokens submitted by applications/pods.", "Optional JWT token for accessing Kubernetes TokenReview API. If provided, this long-lived token will be used to validate service account tokens during authentication. If omitted, the client's own JWT will be used instead, which requires the client to have the system:auth-delegator ClusterRole binding.",
allowedNamespaces: allowedNamespaces:
"The comma-separated list of trusted namespaces that service accounts must belong to authenticate with Infisical.", "The comma-separated list of trusted namespaces that service accounts must belong to authenticate with Infisical.",
allowedNames: "The comma-separated list of trusted service account names that can authenticate with Infisical.", allowedNames: "The comma-separated list of trusted service account names that can authenticate with Infisical.",
@ -260,7 +260,7 @@ export const KUBERNETES_AUTH = {
kubernetesHost: "The new host string, host:port pair, or URL to the base of the Kubernetes API server.", kubernetesHost: "The new host string, host:port pair, or URL to the base of the Kubernetes API server.",
caCert: "The new PEM-encoded CA cert for the Kubernetes API server.", caCert: "The new PEM-encoded CA cert for the Kubernetes API server.",
tokenReviewerJwt: tokenReviewerJwt:
"The new long-lived service account JWT token for Infisical to access the TokenReview API to validate other service account JWT tokens submitted by applications/pods.", "Optional JWT token for accessing Kubernetes TokenReview API. If provided, this long-lived token will be used to validate service account tokens during authentication. If omitted, the client's own JWT will be used instead, which requires the client to have the system:auth-delegator ClusterRole binding.",
allowedNamespaces: allowedNamespaces:
"The new comma-separated list of trusted namespaces that service accounts must belong to authenticate with Infisical.", "The new comma-separated list of trusted namespaces that service accounts must belong to authenticate with Infisical.",
allowedNames: "The new comma-separated list of trusted service account names that can authenticate with Infisical.", allowedNames: "The new comma-separated list of trusted service account names that can authenticate with Infisical.",
@ -631,7 +631,8 @@ export const FOLDERS = {
workspaceId: "The ID of the project to list folders from.", workspaceId: "The ID of the project to list folders from.",
environment: "The slug of the environment to list folders from.", environment: "The slug of the environment to list folders from.",
path: "The path to list folders from.", path: "The path to list folders from.",
directory: "The directory to list folders from. (Deprecated in favor of path)" directory: "The directory to list folders from. (Deprecated in favor of path)",
recursive: "Whether or not to fetch all folders from the specified base path, and all of its subdirectories."
}, },
GET_BY_ID: { GET_BY_ID: {
folderId: "The ID of the folder to get details." folderId: "The ID of the folder to get details."
@ -815,7 +816,8 @@ export const DASHBOARD = {
search: "The text string to filter secret keys and folder names by.", search: "The text string to filter secret keys and folder names by.",
includeSecrets: "Whether to include project secrets in the response.", includeSecrets: "Whether to include project secrets in the response.",
includeFolders: "Whether to include project folders in the response.", includeFolders: "Whether to include project folders in the response.",
includeDynamicSecrets: "Whether to include dynamic project secrets in the response." includeDynamicSecrets: "Whether to include dynamic project secrets in the response.",
includeImports: "Whether to include project secret imports in the response."
}, },
SECRET_DETAILS_LIST: { SECRET_DETAILS_LIST: {
projectId: "The ID of the project to list secrets/folders from.", projectId: "The ID of the project to list secrets/folders from.",

View File

@ -56,6 +56,7 @@ const envSchema = z
// TODO(akhilmhdh): will be changed to one // TODO(akhilmhdh): will be changed to one
ENCRYPTION_KEY: zpStr(z.string().optional()), ENCRYPTION_KEY: zpStr(z.string().optional()),
ROOT_ENCRYPTION_KEY: zpStr(z.string().optional()), ROOT_ENCRYPTION_KEY: zpStr(z.string().optional()),
QUEUE_WORKERS_ENABLED: zodStrBool.default("true"),
HTTPS_ENABLED: zodStrBool, HTTPS_ENABLED: zodStrBool,
// smtp options // smtp options
SMTP_HOST: zpStr(z.string().optional()), SMTP_HOST: zpStr(z.string().optional()),

View File

@ -93,6 +93,7 @@ export const pingGatewayAndVerify = async ({
let lastError: Error | null = null; let lastError: Error | null = null;
const quicClient = await createQuicConnection(relayHost, relayPort, tlsOptions, identityId, orgId).catch((err) => { const quicClient = await createQuicConnection(relayHost, relayPort, tlsOptions, identityId, orgId).catch((err) => {
throw new BadRequestError({ throw new BadRequestError({
message: (err as Error)?.message,
error: err as Error error: err as Error
}); });
}); });

View File

@ -272,10 +272,13 @@ export const queueServiceFactory = (
connection connection
}); });
const appCfg = getConfig();
if (appCfg.QUEUE_WORKERS_ENABLED) {
workerContainer[name] = new Worker<TQueueJobTypes[T]["payload"], void, TQueueJobTypes[T]["name"]>(name, jobFn, { workerContainer[name] = new Worker<TQueueJobTypes[T]["payload"], void, TQueueJobTypes[T]["name"]>(name, jobFn, {
...queueSettings, ...queueSettings,
connection connection
}); });
}
}; };
const startPg = async <T extends QueueName>( const startPg = async <T extends QueueName>(
@ -307,6 +310,11 @@ export const queueServiceFactory = (
event: U, event: U,
listener: WorkerListener<TQueueJobTypes[T]["payload"], void, TQueueJobTypes[T]["name"]>[U] listener: WorkerListener<TQueueJobTypes[T]["payload"], void, TQueueJobTypes[T]["name"]>[U]
) => { ) => {
const appCfg = getConfig();
if (!appCfg.QUEUE_WORKERS_ENABLED) {
return;
}
const worker = workerContainer[name]; const worker = workerContainer[name];
worker.on(event, listener); worker.on(event, listener);
}; };

View File

@ -65,7 +65,7 @@ export const registerSecretScannerGhApp = async (server: FastifyZodProvider) =>
payload: JSON.stringify(req.body), payload: JSON.stringify(req.body),
signature: signatureSHA256 signature: signatureSHA256
}); });
void res.send("ok"); return res.send("ok");
} }
}); });
} }

View File

@ -34,7 +34,7 @@ export const registerServeUI = async (
TELEMETRY_CAPTURING_ENABLED: appCfg.TELEMETRY_ENABLED TELEMETRY_CAPTURING_ENABLED: appCfg.TELEMETRY_ENABLED
}; };
const js = `window.__INFISICAL_RUNTIME_ENV__ = Object.freeze(${JSON.stringify(config)});`; const js = `window.__INFISICAL_RUNTIME_ENV__ = Object.freeze(${JSON.stringify(config)});`;
void res.send(js); return res.send(js);
} }
}); });
@ -57,7 +57,7 @@ export const registerServeUI = async (
reply.callNotFound(); reply.callNotFound();
return; return;
} }
void reply.sendFile("index.html"); return reply.sendFile("index.html");
} }
}); });
} }

View File

@ -70,6 +70,19 @@ export const DefaultResponseErrorsSchema = {
}) })
}; };
export const booleanSchema = z
.union([z.boolean(), z.string().trim()])
.transform((value) => {
if (typeof value === "string") {
// ie if not empty, 0 or false, return true
return Boolean(value) && Number(value) !== 0 && value.toLowerCase() !== "false";
}
return value;
})
.optional()
.default(true);
export const sapPubSchema = SecretApprovalPoliciesSchema.merge( export const sapPubSchema = SecretApprovalPoliciesSchema.merge(
z.object({ z.object({
environment: z.object({ environment: z.object({

View File

@ -16,7 +16,12 @@ import { secretsLimit } from "@app/server/config/rateLimiter";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { getUserAgentType } from "@app/server/plugins/audit-log"; import { getUserAgentType } from "@app/server/plugins/audit-log";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { SanitizedDynamicSecretSchema, SanitizedTagSchema, secretRawSchema } from "@app/server/routes/sanitizedSchemas"; import {
booleanSchema,
SanitizedDynamicSecretSchema,
SanitizedTagSchema,
secretRawSchema
} from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema"; import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema";
import { SecretsOrderBy } from "@app/services/secret/secret-types"; import { SecretsOrderBy } from "@app/services/secret/secret-types";
@ -24,20 +29,6 @@ import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
const MAX_DEEP_SEARCH_LIMIT = 500; // arbitrary limit to prevent excessive results const MAX_DEEP_SEARCH_LIMIT = 500; // arbitrary limit to prevent excessive results
// handle querystring boolean values
const booleanSchema = z
.union([z.boolean(), z.string().trim()])
.transform((value) => {
if (typeof value === "string") {
// ie if not empty, 0 or false, return true
return Boolean(value) && Number(value) !== 0 && value.toLowerCase() !== "false";
}
return value;
})
.optional()
.default(true);
const parseSecretPathSearch = (search?: string) => { const parseSecretPathSearch = (search?: string) => {
if (!search) if (!search)
return { return {
@ -109,6 +100,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
search: z.string().trim().describe(DASHBOARD.SECRET_OVERVIEW_LIST.search).optional(), search: z.string().trim().describe(DASHBOARD.SECRET_OVERVIEW_LIST.search).optional(),
includeSecrets: booleanSchema.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeSecrets), includeSecrets: booleanSchema.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeSecrets),
includeFolders: booleanSchema.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeFolders), includeFolders: booleanSchema.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeFolders),
includeImports: booleanSchema.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeImports),
includeDynamicSecrets: booleanSchema.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeDynamicSecrets) includeDynamicSecrets: booleanSchema.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeDynamicSecrets)
}), }),
response: { response: {
@ -124,9 +116,17 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
}) })
.array() .array()
.optional(), .optional(),
imports: SecretImportsSchema.omit({ importEnv: true })
.extend({
importEnv: z.object({ name: z.string(), slug: z.string(), id: z.string() }),
environment: z.string()
})
.array()
.optional(),
totalFolderCount: z.number().optional(), totalFolderCount: z.number().optional(),
totalDynamicSecretCount: z.number().optional(), totalDynamicSecretCount: z.number().optional(),
totalSecretCount: z.number().optional(), totalSecretCount: z.number().optional(),
totalImportCount: z.number().optional(),
totalCount: z.number() totalCount: z.number()
}) })
} }
@ -143,6 +143,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
orderDirection, orderDirection,
includeFolders, includeFolders,
includeSecrets, includeSecrets,
includeImports,
includeDynamicSecrets includeDynamicSecrets
} = req.query; } = req.query;
@ -159,6 +160,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
let remainingLimit = limit; let remainingLimit = limit;
let adjustedOffset = offset; let adjustedOffset = offset;
let imports: Awaited<ReturnType<typeof server.services.secretImport.getImportsMultiEnv>> | undefined;
let folders: Awaited<ReturnType<typeof server.services.folder.getFoldersMultiEnv>> | undefined; let folders: Awaited<ReturnType<typeof server.services.folder.getFoldersMultiEnv>> | undefined;
let secrets: Awaited<ReturnType<typeof server.services.secret.getSecretsRawMultiEnv>> | undefined; let secrets: Awaited<ReturnType<typeof server.services.secret.getSecretsRawMultiEnv>> | undefined;
let dynamicSecrets: let dynamicSecrets:
@ -168,6 +170,53 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
let totalFolderCount: number | undefined; let totalFolderCount: number | undefined;
let totalDynamicSecretCount: number | undefined; let totalDynamicSecretCount: number | undefined;
let totalSecretCount: number | undefined; let totalSecretCount: number | undefined;
let totalImportCount: number | undefined;
if (includeImports) {
totalImportCount = await server.services.secretImport.getProjectImportMultiEnvCount({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId,
environments,
path: secretPath,
search
});
if (remainingLimit > 0 && totalImportCount > adjustedOffset) {
imports = await server.services.secretImport.getImportsMultiEnv({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId,
environments,
path: secretPath,
search,
limit: remainingLimit,
offset: adjustedOffset
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: req.query.projectId,
event: {
type: EventType.GET_SECRET_IMPORTS,
metadata: {
environment: environments.join(","),
folderId: imports?.[0]?.folderId,
numberOfImports: imports.length
}
}
});
remainingLimit -= imports.length;
adjustedOffset = 0;
} else {
adjustedOffset = Math.max(0, adjustedOffset - totalImportCount);
}
}
if (includeFolders) { if (includeFolders) {
// this is the unique count, ie duplicate folders across envs only count as 1 // this is the unique count, ie duplicate folders across envs only count as 1
@ -345,10 +394,13 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
folders, folders,
dynamicSecrets, dynamicSecrets,
secrets, secrets,
imports,
totalFolderCount, totalFolderCount,
totalDynamicSecretCount, totalDynamicSecretCount,
totalImportCount,
totalSecretCount, totalSecretCount,
totalCount: (totalFolderCount ?? 0) + (totalDynamicSecretCount ?? 0) + (totalSecretCount ?? 0) totalCount:
(totalFolderCount ?? 0) + (totalDynamicSecretCount ?? 0) + (totalSecretCount ?? 0) + (totalImportCount ?? 0)
}; };
} }
}); });

View File

@ -24,7 +24,7 @@ const IdentityKubernetesAuthResponseSchema = IdentityKubernetesAuthsSchema.pick(
allowedAudience: true allowedAudience: true
}).extend({ }).extend({
caCert: z.string(), caCert: z.string(),
tokenReviewerJwt: z.string() tokenReviewerJwt: z.string().optional().nullable()
}); });
export const registerIdentityKubernetesRouter = async (server: FastifyZodProvider) => { export const registerIdentityKubernetesRouter = async (server: FastifyZodProvider) => {
@ -98,7 +98,7 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
.object({ .object({
kubernetesHost: z.string().trim().min(1).describe(KUBERNETES_AUTH.ATTACH.kubernetesHost), kubernetesHost: z.string().trim().min(1).describe(KUBERNETES_AUTH.ATTACH.kubernetesHost),
caCert: z.string().trim().default("").describe(KUBERNETES_AUTH.ATTACH.caCert), caCert: z.string().trim().default("").describe(KUBERNETES_AUTH.ATTACH.caCert),
tokenReviewerJwt: z.string().trim().min(1).describe(KUBERNETES_AUTH.ATTACH.tokenReviewerJwt), tokenReviewerJwt: z.string().trim().optional().describe(KUBERNETES_AUTH.ATTACH.tokenReviewerJwt),
allowedNamespaces: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedNamespaces), // TODO: validation allowedNamespaces: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedNamespaces), // TODO: validation
allowedNames: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedNames), allowedNames: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedNames),
allowedAudience: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedAudience), allowedAudience: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedAudience),
@ -195,7 +195,7 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
.object({ .object({
kubernetesHost: z.string().trim().min(1).optional().describe(KUBERNETES_AUTH.UPDATE.kubernetesHost), kubernetesHost: z.string().trim().min(1).optional().describe(KUBERNETES_AUTH.UPDATE.kubernetesHost),
caCert: z.string().trim().optional().describe(KUBERNETES_AUTH.UPDATE.caCert), caCert: z.string().trim().optional().describe(KUBERNETES_AUTH.UPDATE.caCert),
tokenReviewerJwt: z.string().trim().min(1).optional().describe(KUBERNETES_AUTH.UPDATE.tokenReviewerJwt), tokenReviewerJwt: z.string().trim().nullable().optional().describe(KUBERNETES_AUTH.UPDATE.tokenReviewerJwt),
allowedNamespaces: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedNamespaces), // TODO: validation allowedNamespaces: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedNamespaces), // TODO: validation
allowedNames: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedNames), allowedNames: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedNames),
allowedAudience: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedAudience), allowedAudience: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedAudience),

View File

@ -9,6 +9,8 @@ import { readLimit, secretsLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
import { booleanSchema } from "../sanitizedSchemas";
export const registerSecretFolderRouter = async (server: FastifyZodProvider) => { export const registerSecretFolderRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
url: "/", url: "/",
@ -347,11 +349,14 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
.default("/") .default("/")
.transform(prefixWithSlash) .transform(prefixWithSlash)
.transform(removeTrailingSlash) .transform(removeTrailingSlash)
.describe(FOLDERS.LIST.directory) .describe(FOLDERS.LIST.directory),
recursive: booleanSchema.default(false).describe(FOLDERS.LIST.recursive)
}), }),
response: { response: {
200: z.object({ 200: z.object({
folders: SecretFoldersSchema.array() folders: SecretFoldersSchema.extend({
relativePath: z.string().optional()
}).array()
}) })
} }
}, },

View File

@ -84,6 +84,9 @@ export const identityKubernetesAuthServiceFactory = ({
tokenReviewerJwt = decryptor({ tokenReviewerJwt = decryptor({
cipherTextBlob: identityKubernetesAuth.encryptedKubernetesTokenReviewerJwt cipherTextBlob: identityKubernetesAuth.encryptedKubernetesTokenReviewerJwt
}).toString(); }).toString();
} else {
// if no token reviewer is provided means the incoming token has to act as reviewer
tokenReviewerJwt = serviceAccountJwt;
} }
const { data } = await axios const { data } = await axios
@ -291,7 +294,9 @@ export const identityKubernetesAuthServiceFactory = ({
accessTokenTTL, accessTokenTTL,
accessTokenNumUsesLimit, accessTokenNumUsesLimit,
accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps), accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps),
encryptedKubernetesTokenReviewerJwt: encryptor({ plainText: Buffer.from(tokenReviewerJwt) }).cipherTextBlob, encryptedKubernetesTokenReviewerJwt: tokenReviewerJwt
? encryptor({ plainText: Buffer.from(tokenReviewerJwt) }).cipherTextBlob
: null,
encryptedKubernetesCaCertificate: encryptor({ plainText: Buffer.from(caCert) }).cipherTextBlob encryptedKubernetesCaCertificate: encryptor({ plainText: Buffer.from(caCert) }).cipherTextBlob
}, },
tx tx
@ -387,10 +392,12 @@ export const identityKubernetesAuthServiceFactory = ({
updateQuery.encryptedKubernetesCaCertificate = encryptor({ plainText: Buffer.from(caCert) }).cipherTextBlob; updateQuery.encryptedKubernetesCaCertificate = encryptor({ plainText: Buffer.from(caCert) }).cipherTextBlob;
} }
if (tokenReviewerJwt !== undefined) { if (tokenReviewerJwt) {
updateQuery.encryptedKubernetesTokenReviewerJwt = encryptor({ updateQuery.encryptedKubernetesTokenReviewerJwt = encryptor({
plainText: Buffer.from(tokenReviewerJwt) plainText: Buffer.from(tokenReviewerJwt)
}).cipherTextBlob; }).cipherTextBlob;
} else if (tokenReviewerJwt === null) {
updateQuery.encryptedKubernetesTokenReviewerJwt = null;
} }
const updatedKubernetesAuth = await identityKubernetesAuthDAL.updateById(identityKubernetesAuth.id, updateQuery); const updatedKubernetesAuth = await identityKubernetesAuthDAL.updateById(identityKubernetesAuth.id, updateQuery);

View File

@ -9,7 +9,7 @@ export type TAttachKubernetesAuthDTO = {
identityId: string; identityId: string;
kubernetesHost: string; kubernetesHost: string;
caCert: string; caCert: string;
tokenReviewerJwt: string; tokenReviewerJwt?: string;
allowedNamespaces: string; allowedNamespaces: string;
allowedNames: string; allowedNames: string;
allowedAudience: string; allowedAudience: string;
@ -24,7 +24,7 @@ export type TUpdateKubernetesAuthDTO = {
identityId: string; identityId: string;
kubernetesHost?: string; kubernetesHost?: string;
caCert?: string; caCert?: string;
tokenReviewerJwt?: string; tokenReviewerJwt?: string | null;
allowedNamespaces?: string; allowedNamespaces?: string;
allowedNames?: string; allowedNames?: string;
allowedAudience?: string; allowedAudience?: string;

View File

@ -69,9 +69,15 @@ export const identityUaServiceFactory = ({
isClientSecretRevoked: false isClientSecretRevoked: false
}); });
const validClientSecretInfo = clientSecrtInfo.find(({ clientSecretHash }) => let validClientSecretInfo: (typeof clientSecrtInfo)[0] | null = null;
bcrypt.compareSync(clientSecret, clientSecretHash) for await (const info of clientSecrtInfo) {
); const isMatch = await bcrypt.compare(clientSecret, info.clientSecretHash);
if (isMatch) {
validClientSecretInfo = info;
break;
}
}
if (!validClientSecretInfo) throw new UnauthorizedError({ message: "Invalid credentials" }); if (!validClientSecretInfo) throw new UnauthorizedError({ message: "Invalid credentials" });
const { clientSecretTTL, clientSecretNumUses, clientSecretNumUsesLimit } = validClientSecretInfo; const { clientSecretTTL, clientSecretNumUses, clientSecretNumUsesLimit } = validClientSecretInfo;
@ -104,7 +110,7 @@ export const identityUaServiceFactory = ({
} }
const identityAccessToken = await identityUaDAL.transaction(async (tx) => { const identityAccessToken = await identityUaDAL.transaction(async (tx) => {
const uaClientSecretDoc = await identityUaClientSecretDAL.incrementUsage(validClientSecretInfo.id, tx); const uaClientSecretDoc = await identityUaClientSecretDAL.incrementUsage(validClientSecretInfo!.id, tx);
const newToken = await identityAccessTokenDAL.create( const newToken = await identityAccessTokenDAL.create(
{ {
identityId: identityUa.identityId, identityId: identityUa.identityId,

View File

@ -923,16 +923,14 @@ const getAppsCodefresh = async ({ accessToken }: { accessToken: string }) => {
/** /**
* Return list of projects for Windmill integration * Return list of projects for Windmill integration
*/ */
const getAppsWindmill = async ({ accessToken }: { accessToken: string }) => { const getAppsWindmill = async ({ accessToken, url }: { accessToken: string; url?: string | null }) => {
const { data } = await request.get<{ id: string; name: string }[]>( const apiUrl = url ? `${url}/api` : IntegrationUrls.WINDMILL_API_URL;
`${IntegrationUrls.WINDMILL_API_URL}/workspaces/list`, const { data } = await request.get<{ id: string; name: string }[]>(`${apiUrl}/workspaces/list`, {
{
headers: { headers: {
Authorization: `Bearer ${accessToken}`, Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json" "Accept-Encoding": "application/json"
} }
} });
);
// check for write access of secrets in windmill workspaces // check for write access of secrets in windmill workspaces
const writeAccessCheck = data.map(async (app) => { const writeAccessCheck = data.map(async (app) => {
@ -941,7 +939,7 @@ const getAppsWindmill = async ({ accessToken }: { accessToken: string }) => {
const folderPath = "f/folder/variable"; const folderPath = "f/folder/variable";
const { data: writeUser } = await request.post<object>( const { data: writeUser } = await request.post<object>(
`${IntegrationUrls.WINDMILL_API_URL}/w/${app.id}/variables/create`, `${apiUrl}/w/${app.id}/variables/create`,
{ {
path: userPath, path: userPath,
value: "variable", value: "variable",
@ -957,7 +955,7 @@ const getAppsWindmill = async ({ accessToken }: { accessToken: string }) => {
); );
const { data: writeFolder } = await request.post<object>( const { data: writeFolder } = await request.post<object>(
`${IntegrationUrls.WINDMILL_API_URL}/w/${app.id}/variables/create`, `${apiUrl}/w/${app.id}/variables/create`,
{ {
path: folderPath, path: folderPath,
value: "variable", value: "variable",
@ -974,14 +972,14 @@ const getAppsWindmill = async ({ accessToken }: { accessToken: string }) => {
// is write access is allowed then delete the created secrets from workspace // is write access is allowed then delete the created secrets from workspace
if (writeUser && writeFolder) { if (writeUser && writeFolder) {
await request.delete(`${IntegrationUrls.WINDMILL_API_URL}/w/${app.id}/variables/delete/${userPath}`, { await request.delete(`${apiUrl}/w/${app.id}/variables/delete/${userPath}`, {
headers: { headers: {
Authorization: `Bearer ${accessToken}`, Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json" "Accept-Encoding": "application/json"
} }
}); });
await request.delete(`${IntegrationUrls.WINDMILL_API_URL}/w/${app.id}/variables/delete/${folderPath}`, { await request.delete(`${apiUrl}/w/${app.id}/variables/delete/${folderPath}`, {
headers: { headers: {
Authorization: `Bearer ${accessToken}`, Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json" "Accept-Encoding": "application/json"
@ -1316,7 +1314,8 @@ export const getApps = async ({
case Integrations.WINDMILL: case Integrations.WINDMILL:
return getAppsWindmill({ return getAppsWindmill({
accessToken accessToken,
url
}); });
case Integrations.DIGITAL_OCEAN_APP_PLATFORM: case Integrations.DIGITAL_OCEAN_APP_PLATFORM:

View File

@ -4127,10 +4127,10 @@ const syncSecretsWindmill = async ({
is_secret: boolean; is_secret: boolean;
description?: string; description?: string;
} }
const apiUrl = integration.url ? `${integration.url}/api` : IntegrationUrls.WINDMILL_API_URL;
// get secrets stored in windmill workspace // get secrets stored in windmill workspace
const res = ( const res = (
await request.get<WindmillSecret[]>(`${IntegrationUrls.WINDMILL_API_URL}/w/${integration.appId}/variables/list`, { await request.get<WindmillSecret[]>(`${apiUrl}/w/${integration.appId}/variables/list`, {
headers: { headers: {
Authorization: `Bearer ${accessToken}`, Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json" "Accept-Encoding": "application/json"
@ -4146,7 +4146,6 @@ const syncSecretsWindmill = async ({
// eslint-disable-next-line // eslint-disable-next-line
const pattern = new RegExp("^(u/|f/)[a-zA-Z0-9_-]+/([a-zA-Z0-9_-]+/)*[a-zA-Z0-9_-]*[^/]$"); const pattern = new RegExp("^(u/|f/)[a-zA-Z0-9_-]+/([a-zA-Z0-9_-]+/)*[a-zA-Z0-9_-]*[^/]$");
for await (const key of Object.keys(secrets)) { for await (const key of Object.keys(secrets)) {
if ((key.startsWith("u/") || key.startsWith("f/")) && pattern.test(key)) { if ((key.startsWith("u/") || key.startsWith("f/")) && pattern.test(key)) {
if (!(key in res)) { if (!(key in res)) {
@ -4154,7 +4153,7 @@ const syncSecretsWindmill = async ({
// -> create secret // -> create secret
await request.post( await request.post(
`${IntegrationUrls.WINDMILL_API_URL}/w/${integration.appId}/variables/create`, `${apiUrl}/w/${integration.appId}/variables/create`,
{ {
path: key, path: key,
value: secrets[key].value, value: secrets[key].value,
@ -4171,7 +4170,7 @@ const syncSecretsWindmill = async ({
} else { } else {
// -> update secret // -> update secret
await request.post( await request.post(
`${IntegrationUrls.WINDMILL_API_URL}/w/${integration.appId}/variables/update/${res[key].path}`, `${apiUrl}/w/${integration.appId}/variables/update/${res[key].path}`,
{ {
path: key, path: key,
value: secrets[key].value, value: secrets[key].value,
@ -4192,16 +4191,13 @@ const syncSecretsWindmill = async ({
for await (const key of Object.keys(res)) { for await (const key of Object.keys(res)) {
if (!(key in secrets)) { if (!(key in secrets)) {
// -> delete secret // -> delete secret
await request.delete( await request.delete(`${apiUrl}/w/${integration.appId}/variables/delete/${res[key].path}`, {
`${IntegrationUrls.WINDMILL_API_URL}/w/${integration.appId}/variables/delete/${res[key].path}`,
{
headers: { headers: {
Authorization: `Bearer ${accessToken}`, Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json", "Content-Type": "application/json",
"Accept-Encoding": "application/json" "Accept-Encoding": "application/json"
} }
} });
);
} }
} }
}; };

View File

@ -1,7 +1,7 @@
import { Knex } from "knex"; import { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { TableName, TProjectEnvironments, TSecretFolders, TSecretFoldersUpdate } from "@app/db/schemas"; import { TableName, TSecretFolders, TSecretFoldersUpdate } from "@app/db/schemas";
import { BadRequestError, DatabaseError } from "@app/lib/errors"; import { BadRequestError, DatabaseError } from "@app/lib/errors";
import { groupBy, removeTrailingSlash } from "@app/lib/fn"; import { groupBy, removeTrailingSlash } from "@app/lib/fn";
import { ormify, selectAllTableCols } from "@app/lib/knex"; import { ormify, selectAllTableCols } from "@app/lib/knex";
@ -41,12 +41,12 @@ const sqlFindMultipleFolderByEnvPathQuery = (db: Knex, query: Array<{ envId: str
void baseQb void baseQb
.select({ .select({
depth: 1, depth: 1,
// latestFolderVerId: db.raw("NULL::uuid"),
path: db.raw("'/'") path: db.raw("'/'")
}) })
.from(TableName.SecretFolder) .from(TableName.SecretFolder)
.where({ .where({
parentId: null parentId: null,
name: "root"
}) })
.whereIn( .whereIn(
"envId", "envId",
@ -69,9 +69,7 @@ const sqlFindMultipleFolderByEnvPathQuery = (db: Knex, query: Array<{ envId: str
.where((wb) => .where((wb) =>
formatedQuery.map(({ secretPath }) => formatedQuery.map(({ secretPath }) =>
wb.orWhereRaw( wb.orWhereRaw(
`depth = array_position(ARRAY[${secretPath.map(() => "?").join(",")}]::varchar[], ${ `secret_folders.name = (ARRAY[${secretPath.map(() => "?").join(",")}]::varchar[])[depth]`,
TableName.SecretFolder
}.name,depth)`,
[...secretPath] [...secretPath]
) )
) )
@ -107,7 +105,6 @@ const sqlFindFolderByPathQuery = (db: Knex, projectId: string, environments: str
void baseQb void baseQb
.select({ .select({
depth: 1, depth: 1,
// latestFolderVerId: db.raw("NULL::uuid"),
path: db.raw("'/'") path: db.raw("'/'")
}) })
.from(TableName.SecretFolder) .from(TableName.SecretFolder)
@ -117,6 +114,11 @@ const sqlFindFolderByPathQuery = (db: Knex, projectId: string, environments: str
parentId: null parentId: null
}) })
.whereIn(`${TableName.Environment}.slug`, environments) .whereIn(`${TableName.Environment}.slug`, environments)
.select(
db.ref("slug").withSchema(TableName.Environment).as("envSlug"),
db.ref("name").withSchema(TableName.Environment).as("envName"),
db.ref("projectId").withSchema(TableName.Environment)
)
.select(selectAllTableCols(TableName.SecretFolder)) .select(selectAllTableCols(TableName.SecretFolder))
.union( .union(
(qb) => (qb) =>
@ -128,21 +130,20 @@ const sqlFindFolderByPathQuery = (db: Knex, projectId: string, environments: str
depth: db.raw("parent.depth + 1"), depth: db.raw("parent.depth + 1"),
path: db.raw( path: db.raw(
"CONCAT((CASE WHEN parent.path = '/' THEN '' ELSE parent.path END),'/', secret_folders.name)" "CONCAT((CASE WHEN parent.path = '/' THEN '' ELSE parent.path END),'/', secret_folders.name)"
) ),
envSlug: db.ref("envSlug").withSchema("parent"),
envName: db.ref("envName").withSchema("parent"),
projectId: db.ref("projectId").withSchema("parent")
}) })
.select(selectAllTableCols(TableName.SecretFolder)) .select(selectAllTableCols(TableName.SecretFolder))
.whereRaw( .whereRaw(`secret_folders.name = (ARRAY[${pathSegments.map(() => "?").join(",")}]::varchar[])[depth]`, [
`depth = array_position(ARRAY[${pathSegments ...pathSegments
.map(() => "?") ])
.join(",")}]::varchar[], secret_folders.name,depth)`,
[...pathSegments]
)
.from(TableName.SecretFolder) .from(TableName.SecretFolder)
.join("parent", "parent.id", `${TableName.SecretFolder}.parentId`) .join("parent", "parent.id", `${TableName.SecretFolder}.parentId`)
); );
}) })
.from<TSecretFolders & { depth: number; path: string }>("parent") .from<TSecretFolders & { depth: number; path: string }>("parent")
.leftJoin<TProjectEnvironments>(TableName.Environment, `${TableName.Environment}.id`, "parent.envId")
.select< .select<
(TSecretFolders & { (TSecretFolders & {
depth: number; depth: number;
@ -152,13 +153,7 @@ const sqlFindFolderByPathQuery = (db: Knex, projectId: string, environments: str
envName: string; envName: string;
projectId: string; projectId: string;
})[] })[]
>( >(selectAllTableCols("parent" as TableName.SecretFolder));
selectAllTableCols("parent" as TableName.SecretFolder),
db.ref("id").withSchema(TableName.Environment).as("envId"),
db.ref("slug").withSchema(TableName.Environment).as("envSlug"),
db.ref("name").withSchema(TableName.Environment).as("envName"),
db.ref("projectId").withSchema(TableName.Environment)
);
}; };
const sqlFindSecretPathByFolderId = (db: Knex, projectId: string, folderIds: string[]) => const sqlFindSecretPathByFolderId = (db: Knex, projectId: string, folderIds: string[]) =>
@ -220,19 +215,12 @@ export const secretFolderDALFactory = (db: TDbClient) => {
throw new BadRequestError({ throw new BadRequestError({
message: "Invalid secret path. Only alphanumeric characters, dashes, and underscores are allowed." message: "Invalid secret path. Only alphanumeric characters, dashes, and underscores are allowed."
}); });
const formatedPath = removeTrailingSlash(path);
try { try {
const folder = await sqlFindFolderByPathQuery( const query = sqlFindFolderByPathQuery(tx || db.replicaNode(), projectId, [environment], formatedPath)
tx || db.replicaNode(), .where("path", formatedPath)
projectId,
[environment],
removeTrailingSlash(path)
)
.orderBy("depth", "desc")
.first(); .first();
if (folder && folder.path !== removeTrailingSlash(path)) { const folder = await query;
return;
}
if (!folder) return; if (!folder) return;
const { envId: id, envName: name, envSlug: slug, ...el } = folder; const { envId: id, envName: name, envSlug: slug, ...el } = folder;
return { ...el, envId: id, environment: { id, name, slug } }; return { ...el, envId: id, environment: { id, name, slug } };
@ -250,22 +238,13 @@ export const secretFolderDALFactory = (db: TDbClient) => {
}); });
try { try {
const pathDepth = removeTrailingSlash(path).split("/").filter(Boolean).length + 1; const formatedPath = removeTrailingSlash(path);
const folders = await sqlFindFolderByPathQuery( const folders = await sqlFindFolderByPathQuery(
tx || db.replicaNode(), tx || db.replicaNode(),
projectId, projectId,
environments, environments,
removeTrailingSlash(path) formatedPath
) ).where("path", removeTrailingSlash(path));
.orderBy("depth", "desc")
.where("depth", pathDepth);
const firstFolder = folders[0];
if (firstFolder && firstFolder.path !== removeTrailingSlash(path)) {
return [];
}
return folders.map((folder) => { return folders.map((folder) => {
const { envId: id, envName: name, envSlug: slug, ...el } = folder; const { envId: id, envName: name, envSlug: slug, ...el } = folder;
@ -323,7 +302,6 @@ export const secretFolderDALFactory = (db: TDbClient) => {
const findSecretPathByFolderIds = async (projectId: string, folderIds: string[], tx?: Knex) => { const findSecretPathByFolderIds = async (projectId: string, folderIds: string[], tx?: Knex) => {
try { try {
const folders = await sqlFindSecretPathByFolderId(tx || db.replicaNode(), projectId, folderIds); const folders = await sqlFindSecretPathByFolderId(tx || db.replicaNode(), projectId, folderIds);
// travelling all the way from leaf node to root contains real path // travelling all the way from leaf node to root contains real path
const rootFolders = groupBy( const rootFolders = groupBy(
folders.filter(({ parentId }) => parentId === null), folders.filter(({ parentId }) => parentId === null),

View File

@ -401,7 +401,8 @@ export const secretFolderServiceFactory = ({
orderBy, orderBy,
orderDirection, orderDirection,
limit, limit,
offset offset,
recursive
}: TGetFolderDTO) => { }: TGetFolderDTO) => {
// folder list is allowed to be read by anyone // folder list is allowed to be read by anyone
// permission to check does user has access // permission to check does user has access
@ -420,6 +421,17 @@ export const secretFolderServiceFactory = ({
const parentFolder = await folderDAL.findBySecretPath(projectId, environment, secretPath); const parentFolder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!parentFolder) return []; if (!parentFolder) return [];
if (recursive) {
const recursiveFolders = await folderDAL.findByEnvsDeep({ parentIds: [parentFolder.id] });
// remove the parent folder
return recursiveFolders
.filter((folder) => folder.id !== parentFolder.id)
.map((folder) => ({
...folder,
relativePath: folder.path
}));
}
const folders = await folderDAL.find( const folders = await folderDAL.find(
{ {
envId: env.id, envId: env.id,

View File

@ -45,6 +45,7 @@ export type TGetFolderDTO = {
orderDirection?: OrderByDirection; orderDirection?: OrderByDirection;
limit?: number; limit?: number;
offset?: number; offset?: number;
recursive?: boolean;
} & TProjectPermission; } & TProjectPermission;
export type TGetFolderByIdDTO = { export type TGetFolderByIdDTO = {

View File

@ -469,6 +469,58 @@ export const secretImportServiceFactory = ({
return count; return count;
}; };
const getProjectImportMultiEnvCount = async ({
path: secretPath,
environments,
projectId,
actor,
actorId,
actorAuthMethod,
actorOrgId,
search
}: Omit<TGetSecretImportsDTO, "environment"> & { environments: string[] }) => {
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
const filteredEnvironments = [];
for (const environment of environments) {
if (
permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.SecretImports, { environment, secretPath })
)
) {
filteredEnvironments.push(environment);
}
}
if (filteredEnvironments.length === 0) {
return 0;
}
for (const environment of filteredEnvironments) {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.SecretImports, { environment, secretPath })
);
}
const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, secretPath);
if (!folders?.length)
throw new NotFoundError({
message: `Folder with path '${secretPath}' not found on environments with slugs '${environments.join(", ")}'`
});
const counts = await Promise.all(
folders.map((folder) => secretImportDAL.getProjectImportCount({ folderId: folder.id, search }))
);
return counts.reduce((sum, count) => sum + count, 0);
};
const getImports = async ({ const getImports = async ({
path: secretPath, path: secretPath,
environment, environment,
@ -688,6 +740,59 @@ export const secretImportServiceFactory = ({
})); }));
}; };
const getImportsMultiEnv = async ({
path: secretPath,
environments,
projectId,
actor,
actorId,
actorAuthMethod,
actorOrgId,
search,
limit,
offset
}: Omit<TGetSecretImportsDTO, "environment"> & { environments: string[] }) => {
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
const filteredEnvironments = [];
for (const environment of environments) {
if (
permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.SecretImports, { environment, secretPath })
)
) {
filteredEnvironments.push(environment);
}
}
if (filteredEnvironments.length === 0) {
return [];
}
const folders = await folderDAL.findBySecretPathMultiEnv(projectId, filteredEnvironments, secretPath);
if (!folders?.length)
throw new NotFoundError({
message: `Folder with path '${secretPath}' not found on environments with slugs '${environments.join(", ")}'`
});
const secImportsArrays = await Promise.all(
folders.map(async (folder) => {
const imports = await secretImportDAL.find({ folderId: folder.id, search, limit, offset });
return imports.map((importItem) => ({
...importItem,
environment: folder.environment.slug
}));
})
);
return secImportsArrays.flat();
};
return { return {
createImport, createImport,
updateImport, updateImport,
@ -698,6 +803,8 @@ export const secretImportServiceFactory = ({
getRawSecretsFromImports, getRawSecretsFromImports,
resyncSecretImportReplication, resyncSecretImportReplication,
getProjectImportCount, getProjectImportCount,
fnSecretsFromImports fnSecretsFromImports,
getProjectImportMultiEnvCount,
getImportsMultiEnv
}; };
}; };

View File

@ -148,6 +148,28 @@ var gatewayInstallCmd = &cobra.Command{
}, },
} }
var gatewayUninstallCmd = &cobra.Command{
Use: "uninstall",
Short: "Uninstall and remove systemd service for the gateway (requires sudo)",
Long: "Uninstall and remove systemd service for the gateway. Must be run with sudo on Linux.",
Example: "sudo infisical gateway uninstall",
DisableFlagsInUseLine: true,
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
if runtime.GOOS != "linux" {
util.HandleError(fmt.Errorf("systemd service installation is only supported on Linux"))
}
if os.Geteuid() != 0 {
util.HandleError(fmt.Errorf("systemd service installation requires root/sudo privileges"))
}
if err := gateway.UninstallGatewaySystemdService(); err != nil {
util.HandleError(err, "Failed to uninstall systemd service")
}
},
}
var gatewayRelayCmd = &cobra.Command{ var gatewayRelayCmd = &cobra.Command{
Example: `infisical gateway relay`, Example: `infisical gateway relay`,
Short: "Used to run infisical gateway relay", Short: "Used to run infisical gateway relay",
@ -183,6 +205,7 @@ func init() {
gatewayRelayCmd.Flags().String("config", "", "Relay config yaml file path") gatewayRelayCmd.Flags().String("config", "", "Relay config yaml file path")
gatewayCmd.AddCommand(gatewayInstallCmd) gatewayCmd.AddCommand(gatewayInstallCmd)
gatewayCmd.AddCommand(gatewayUninstallCmd)
gatewayCmd.AddCommand(gatewayRelayCmd) gatewayCmd.AddCommand(gatewayRelayCmd)
rootCmd.AddCommand(gatewayCmd) rootCmd.AddCommand(gatewayCmd)
} }

View File

@ -89,7 +89,7 @@ func (g *Gateway) ConnectWithRelay() error {
turnClientCfg.Conn = turn.NewSTUNConn(conn) turnClientCfg.Conn = turn.NewSTUNConn(conn)
} else { } else {
log.Info().Msgf("Provided relay port %s. Using non TLS connection.", relayPort) log.Info().Msgf("Provided relay port %s. Using non TLS connection.", relayPort)
conn, err := net.ListenPacket("udp4", turnAddr.String()) conn, err := net.ListenPacket("udp4", "0.0.0.0:0")
if err != nil { if err != nil {
return fmt.Errorf("Failed to connect with relay server: %w", err) return fmt.Errorf("Failed to connect with relay server: %w", err)
} }
@ -342,7 +342,9 @@ func (g *Gateway) registerRelayIsActive(ctx context.Context, errCh chan error) e
case <-ticker.C: case <-ticker.C:
log.Debug().Msg("Performing relay connection health check") log.Debug().Msg("Performing relay connection health check")
err := g.createPermissionForStaticIps(g.config.InfisicalStaticIp) err := g.createPermissionForStaticIps(g.config.InfisicalStaticIp)
if err != nil && !strings.Contains(err.Error(), "tls:") { // try again error message from server happens to avoid congestion
// https://github.com/pion/turn/blob/master/internal/client/udp_conn.go#L382
if err != nil && !strings.Contains(err.Error(), "try again") {
failures++ failures++
log.Warn().Err(err).Int("failures", failures).Msg("Failed to refresh TURN permissions") log.Warn().Err(err).Int("failures", failures).Msg("Failed to refresh TURN permissions")
if failures >= maxFailures { if failures >= maxFailures {
@ -351,6 +353,7 @@ func (g *Gateway) registerRelayIsActive(ctx context.Context, errCh chan error) e
} }
continue continue
} }
failures = 0 // reset
} }
} }
}() }()

View File

@ -15,7 +15,8 @@ Description=Infisical Gateway Service
After=network.target After=network.target
[Service] [Service]
Type=simple Type=notify
NotifyAccess=all
EnvironmentFile=/etc/infisical/gateway.conf EnvironmentFile=/etc/infisical/gateway.conf
ExecStart=infisical gateway ExecStart=infisical gateway
Restart=on-failure Restart=on-failure
@ -50,8 +51,6 @@ func InstallGatewaySystemdService(token string, domain string) error {
configContent := fmt.Sprintf("INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN=%s\n", token) configContent := fmt.Sprintf("INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN=%s\n", token)
if domain != "" { if domain != "" {
configContent += fmt.Sprintf("INFISICAL_API_URL=%s\n", domain) configContent += fmt.Sprintf("INFISICAL_API_URL=%s\n", domain)
} else {
configContent += "INFISICAL_API_URL=\n"
} }
configPath := filepath.Join(configDir, "gateway.conf") configPath := filepath.Join(configDir, "gateway.conf")
@ -60,11 +59,6 @@ func InstallGatewaySystemdService(token string, domain string) error {
} }
servicePath := "/etc/systemd/system/infisical-gateway.service" servicePath := "/etc/systemd/system/infisical-gateway.service"
if _, err := os.Stat(servicePath); err == nil {
log.Info().Msg("Systemd service file already exists")
return nil
}
if err := os.WriteFile(servicePath, []byte(systemdServiceTemplate), 0644); err != nil { if err := os.WriteFile(servicePath, []byte(systemdServiceTemplate), 0644); err != nil {
return fmt.Errorf("failed to write systemd service file: %v", err) return fmt.Errorf("failed to write systemd service file: %v", err)
} }
@ -80,3 +74,48 @@ func InstallGatewaySystemdService(token string, domain string) error {
return nil return nil
} }
func UninstallGatewaySystemdService() error {
if runtime.GOOS != "linux" {
log.Info().Msg("Skipping systemd service uninstallation - not on Linux")
return nil
}
if os.Geteuid() != 0 {
log.Info().Msg("Skipping systemd service uninstallation - not running as root/sudo")
return nil
}
// Stop the service if it's running
stopCmd := exec.Command("systemctl", "stop", "infisical-gateway")
if err := stopCmd.Run(); err != nil {
log.Warn().Msgf("Failed to stop service: %v", err)
}
// Disable the service
disableCmd := exec.Command("systemctl", "disable", "infisical-gateway")
if err := disableCmd.Run(); err != nil {
log.Warn().Msgf("Failed to disable service: %v", err)
}
// Remove the service file
servicePath := "/etc/systemd/system/infisical-gateway.service"
if err := os.Remove(servicePath); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to remove systemd service file: %v", err)
}
// Remove the configuration file
configPath := "/etc/infisical/gateway.conf"
if err := os.Remove(configPath); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to remove config file: %v", err)
}
// Reload systemd to apply changes
reloadCmd := exec.Command("systemctl", "daemon-reload")
if err := reloadCmd.Run(); err != nil {
return fmt.Errorf("failed to reload systemd: %v", err)
}
log.Info().Msg("Successfully uninstalled Infisical Gateway systemd service")
return nil
}

View File

@ -56,6 +56,7 @@ func WriteInitalConfig(userCredentials *models.UserCredentials) error {
LoggedInUsers: existingConfigFile.LoggedInUsers, LoggedInUsers: existingConfigFile.LoggedInUsers,
VaultBackendType: existingConfigFile.VaultBackendType, VaultBackendType: existingConfigFile.VaultBackendType,
VaultBackendPassphrase: existingConfigFile.VaultBackendPassphrase, VaultBackendPassphrase: existingConfigFile.VaultBackendPassphrase,
Domains: existingConfigFile.Domains,
} }
configFileMarshalled, err := json.Marshal(configFile) configFileMarshalled, err := json.Marshal(configFile)

View File

@ -245,8 +245,9 @@ func getCurrentBranch() (string, error) {
} }
func AppendAPIEndpoint(address string) string { func AppendAPIEndpoint(address string) string {
// if it's empty return as it is
// Ensure the address does not already end with "/api" // Ensure the address does not already end with "/api"
if strings.HasSuffix(address, "/api") { if address == "" || strings.HasSuffix(address, "/api") {
return address return address
} }

View File

@ -9,20 +9,76 @@ description: "Track evert event action performed within Infisical projects."
If you're using Infisical Cloud, then it is available under the **Pro**, If you're using Infisical Cloud, then it is available under the **Pro**,
and **Enterprise Tier** with varying retention periods. If you're self-hosting Infisical, and **Enterprise Tier** with varying retention periods. If you're self-hosting Infisical,
then you should contact sales@infisical.com to purchase an enterprise license to use it. then you should contact sales@infisical.com to purchase an enterprise license to use it.
</Info> </Info>
Infisical provides audit logs for security and compliance teams to monitor information access. Infisical provides audit logs for security and compliance teams to monitor information access.
With the Audit Log functionality, teams can: With the Audit Log functionality, teams can:
- **Track** 40+ different events; - **Track** 40+ different events;
- **Filter** audit logs by event, actor, source, date or any combination of these filters; - **Filter** audit logs by event, actor, source, date or any combination of these filters;
- **Inspect** extensive metadata in the event of any suspicious activity or incident review. - **Inspect** extensive metadata in the event of any suspicious activity or incident review.
![Audit logs](../../images/platform/audit-logs/audit-logs-table.png) ![Audit logs](../../images/platform/audit-logs/audit-logs-table.png)
## Audit Log Structure
Each log contains the following data: Each log contains the following data:
- **Event**: The underlying action such as create, list, read, update, or delete secret(s). | Field | Type | Description | Purpose |
- **Actor**: The entity responsible for performing or causing the event; this can be a user or service. | ------------------------- | -------- | --------------------------------------------------------- | ------------------------------------------------------------- |
- **Timestamp**: The date and time at which point the event occurred. | **event** | Object | Contains details about the action performed | Captures what happened |
- **Source** (User agent + IP): The software (user agent) and network address (IP) from which the event was initiated. | event.type | String | The specific action that occurred (e.g., "create-secret") | Identifies the exact operation |
- **Metadata**: Additional data to provide context for each event. For example, this could be the path at which a secret was fetched from etc. | event.metadata | Object | Context-specific details about the event | Provides detailed information relevant to the specific action |
| **actor** | Object | Information about who performed the action | Identifies the responsible entity |
| actor.type | String | Category of actor (user, service, identity, etc.) | Distinguishes between human and non-human actors |
| actor.metadata | Object | Details about the specific actor | Provides identity information |
| actor.metadata.userId | String | Unique identifier for user actors | Links to specific user account |
| actor.metadata.email | String | Email address for user actors | Email of the executing user |
| actor.metadata.username | String | Username for user actors | Username of the executing user |
| actor.metadata.serviceId | String | Identifier for service actors | ID of specific service token |
| actor.metadata.identityId | String | Identifier for identity actors | ID to specific identity |
| actor.metadata.permission | Object | Permission context for the action | Shows permission template data when action was performed |
| **orgId** | String | Organization identifier | Indicates which organization the action occurred in |
| **projectId** | String | Project identifier | Indicates which project the action affected |
| **ipAddress** | String | Source IP address | Shows where the request originated from |
| **userAgent** | String | Client application information | Identifies browser or application used |
| **userAgentType** | String | Category of client (web, CLI, SDK, etc.) | Classifies the access method |
| **timestamp** | DateTime | When the action occurred | Records the exact time of the event |
<Accordion title="Example Payload">
```json
{
"id": "[UUID]",
"ipAddress": "[IP_ADDRESS]",
"userAgent": "[USER_AGENT_STRING]",
"userAgentType": "web",
"expiresAt": "[TIMESTAMP]",
"createdAt": "[TIMESTAMP]",
"updatedAt": "[TIMESTAMP]",
"orgId": "[ORGANIZATION_UUID]",
"projectId": "[PROJECT_UUID]",
"projectName": "[PROJECT_NAME]",
"event": {
"type": "get-secrets",
"metadata": {
"secretPath": "[PATH]",
"environment": "[ENVIRONMENT_NAME]",
"numberOfSecrets": [NUMBER]
}
},
"actor": {
"type": "user",
"metadata": {
"email": "[EMAIL]",
"userId": "[USER_UUID]",
"username": "[USERNAME]",
"permission": {
"metadata": {},
"auth": {}
}
}
}
}
```
</Accordion>

View File

@ -37,7 +37,8 @@ then Infisical returns a short-lived access token that can be used to make authe
To be more specific: To be more specific:
1. The application deployed on Kubernetes retrieves its [service account credential](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#opt-out-of-api-credential-automounting) that is a JWT token at the `/var/run/secrets/kubernetes.io/serviceaccount/token` pod path. 1. The application deployed on Kubernetes retrieves its [service account credential](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#opt-out-of-api-credential-automounting) that is a JWT token at the `/var/run/secrets/kubernetes.io/serviceaccount/token` pod path.
2. The application sends the JWT token to Infisical at the `/api/v1/auth/kubernetes-auth/login` endpoint after which Infisical forwards the JWT token to the Kubernetes API Server at the [TokenReview API](https://kubernetes.io/docs/reference/kubernetes-api/authentication-resources/token-review-v1/) for verification and to obtain the service account information associated with the JWT token. Infisical is able to authenticate and interact with the TokenReview API by using a long-lived service account JWT token itself (referred to onward as the token reviewer JWT token). 2. The application sends the JWT token to Infisical at the `/api/v1/auth/kubernetes-auth/login` endpoint after which Infisical forwards the JWT token to the Kubernetes API Server at the TokenReview API for verification and to obtain the service account information associated with the JWT token.
Infisical is able to authenticate and interact with the TokenReview API by using either the long lived JWT token set while configuring this authentication method or by using the incoming token itself. The JWT token mentioned in this context is referred as the token reviewer JWT token.
3. Infisical checks the service account properties against set criteria such **Allowed Service Account Names** and **Allowed Namespaces**. 3. Infisical checks the service account properties against set criteria such **Allowed Service Account Names** and **Allowed Namespaces**.
4. If all is well, Infisical returns a short-lived access token that the application can use to make authenticated requests to the Infisical API. 4. If all is well, Infisical returns a short-lived access token that the application can use to make authenticated requests to the Infisical API.
@ -53,6 +54,12 @@ In the following steps, we explore how to create and use identities for your app
<Steps> <Steps>
<Step title="Obtaining the token reviewer JWT for Infisical"> <Step title="Obtaining the token reviewer JWT for Infisical">
<Tabs>
<Tab title="Option 1: Reviewer JWT Token">
<Note>
**When to use this option**: Choose this approach when you want centralized authentication management. Only one service account needs special permissions, and your application service accounts remain unchanged.
</Note>
1.1. Start by creating a service account in your Kubernetes cluster that will be used by Infisical to authenticate with the Kubernetes API Server. 1.1. Start by creating a service account in your Kubernetes cluster that will be used by Infisical to authenticate with the Kubernetes API Server.
```yaml infisical-service-account.yaml ```yaml infisical-service-account.yaml
@ -61,7 +68,6 @@ In the following steps, we explore how to create and use identities for your app
metadata: metadata:
name: infisical-auth name: infisical-auth
namespace: default namespace: default
``` ```
``` ```
@ -121,7 +127,40 @@ In the following steps, we explore how to create and use identities for your app
Keep this JWT token handy as you will need it for the **Token Reviewer JWT** field when configuring the Kubernetes Auth authentication method for the identity in step 2. Keep this JWT token handy as you will need it for the **Token Reviewer JWT** field when configuring the Kubernetes Auth authentication method for the identity in step 2.
</Step> </Tab>
<Tab title="Option 2: Client JWT as Reviewer JWT Token">
<Note>
**When to use this option**: Choose this approach to eliminate long-lived tokens. This option simplifies Infisical configuration but requires each application service account to have elevated permissions.
</Note>
The self-validation method eliminates the need for a separate long-lived reviewer JWT by using the same token for both authentication and validation. Instead of creating a dedicated reviewer service account, you'll grant the necessary permissions to each application service account.
For each service account that needs to authenticate with Infisical, add the `system:auth-delegator` role:
```yaml client-role-binding.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: infisical-client-binding-[your-app-name]
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: system:auth-delegator
subjects:
- kind: ServiceAccount
name: [your-app-service-account]
namespace: [your-app-namespace]
```
```
kubectl apply -f client-role-binding.yaml
```
When configuring Kubernetes Auth in Infisical, leave the **Token Reviewer JWT** field empty. Infisical will use the client's own token for validation.
</Tab>
</Tabs>
</Step>
<Step title="Creating an identity"> <Step title="Creating an identity">
To create an identity, head to your Organization Settings > Access Control > Machine Identities and press **Create identity**. To create an identity, head to your Organization Settings > Access Control > Machine Identities and press **Create identity**.
@ -151,7 +190,8 @@ In the following steps, we explore how to create and use identities for your app
Here's some more guidance on each field: Here's some more guidance on each field:
- Kubernetes Host / Base Kubernetes API URL: The host string, host:port pair, or URL to the base of the Kubernetes API server. This can usually be obtained by running `kubectl cluster-info`. - Kubernetes Host / Base Kubernetes API URL: The host string, host:port pair, or URL to the base of the Kubernetes API server. This can usually be obtained by running `kubectl cluster-info`.
- Token Reviewer JWT: A long-lived service account JWT token for Infisical to access the [TokenReview API](https://kubernetes.io/docs/reference/kubernetes-api/authentication-resources/token-review-v1/) to validate other service account JWT tokens submitted by applications/pods. This is the JWT token obtained from step 1.5. - Token Reviewer JWT: A long-lived service account JWT token for Infisical to access the [TokenReview API](https://kubernetes.io/docs/reference/kubernetes-api/authentication-resources/token-review-v1/) to validate other service account JWT tokens submitted by applications/pods. This is the JWT token obtained from step 1.5(Reviewer Tab). If omitted, the client's own JWT will be used instead, which requires the client to have the `system:auth-delegator` ClusterRole binding.
This is shown in step 1, option 2.
- Allowed Service Account Names: A comma-separated list of trusted service account names that are allowed to authenticate with Infisical. - Allowed Service Account Names: A comma-separated list of trusted service account names that are allowed to authenticate with Infisical.
- Allowed Namespaces: A comma-separated list of trusted namespaces that service accounts must belong to authenticate with Infisical. - Allowed Namespaces: A comma-separated list of trusted namespaces that service accounts must belong to authenticate with Infisical.
- Allowed Audience: An optional audience claim that the service account JWT token must have to authenticate with Infisical. - Allowed Audience: An optional audience claim that the service account JWT token must have to authenticate with Infisical.
@ -182,6 +222,7 @@ In the following steps, we explore how to create and use identities for your app
For information on how to configure sevice accounts for pods, refer to the guide [here](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/). For information on how to configure sevice accounts for pods, refer to the guide [here](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/).
We provide a code example below of how you might retrieve the JWT token and use it to authenticate with Infisical to gain access to the [Infisical API](/api-reference/overview/introduction). We provide a code example below of how you might retrieve the JWT token and use it to authenticate with Infisical to gain access to the [Infisical API](/api-reference/overview/introduction).
<Accordion <Accordion
title="Sample code for inside an application" title="Sample code for inside an application"
> >
@ -238,14 +279,15 @@ In the following steps, we explore how to create and use identities for your app
<Accordion title="Why is the Infisical API rejecting my access token?"> <Accordion title="Why is the Infisical API rejecting my access token?">
There are a few reasons for why this might happen: There are a few reasons for why this might happen:
- The access token has expired. - The access token has expired.
- The identity is insufficently permissioned to interact with the resources you wish to access. - The identity is insufficently permissioned to interact with the resources you wish to access.
- The client access token is being used from an untrusted IP. - The client access token is being used from an untrusted IP.
</Accordion> </Accordion>
<Accordion title="What is access token renewal and TTL/Max TTL?"> <Accordion title="What is access token renewal and TTL/Max TTL?">
A identity access token can have a time-to-live (TTL) or incremental lifetime after which it expires. A identity access token can have a time-to-live (TTL) or incremental lifetime after which it expires.
In certain cases, you may want to extend the lifespan of an access token; to do so, you must set a max TTL parameter. In certain cases, you may want to extend the lifespan of an access token; to do so, you must set a max TTL parameter.
A token can be renewed any number of times where each call to renew it can extend the token's lifetime by increments of the access token's TTL. A token can be renewed any number of times where each call to renew it can extend the token's lifetime by increments of the access token's TTL.
Regardless of how frequently an access token is renewed, its lifespan remains bound to the maximum TTL determined at its creation. Regardless of how frequently an access token is renewed, its lifespan remains bound to the maximum TTL determined at its creation.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

After

Width:  |  Height:  |  Size: 430 KiB

View File

@ -6,7 +6,7 @@ description: "Learn how to fetch secrets from Infisical with Terraform using bot
This guide demonstrates how to use Infisical to manage secrets in your Terraform infrastructure code, supporting both traditional data sources and ephemeral resources for enhanced security. It uses: This guide demonstrates how to use Infisical to manage secrets in your Terraform infrastructure code, supporting both traditional data sources and ephemeral resources for enhanced security. It uses:
- Infisical (you can use [Infisical Cloud](https://app.infisical.com) or a [self-hosted instance of Infisical](https://infisical.com/docs/self-hosting/overview)) to store your secrets - Infisical (you can use [Infisical Cloud](https://app.infisical.com) or a [self-hosted instance of Infisical](https://infisical.com/docs/self-hosting/overview)) to store your secrets
- The [Terraform Provider](https://registry.terraform.io/providers/Infisical/infisical/latest) to fetch secrets for your infrastructure - The [Terraform Provider](https://registry.terraform.io/providers/Infisical/infisical/latest/docs) to fetch secrets for your infrastructure
## Prerequisites ## Prerequisites

View File

@ -0,0 +1,5 @@
---
title: "Apache Airflow"
description: "Learn how to use Infisical as your custom secrets backend in Apache Airflow."
url: "https://github.com/Infisical/airflow-provider-infisical?tab=readme-ov-file#airflow-infisical-provider"
---

View File

@ -402,7 +402,8 @@
] ]
}, },
"integrations/frameworks/terraform", "integrations/frameworks/terraform",
"integrations/platforms/ansible" "integrations/platforms/ansible",
"integrations/platforms/apache-airflow"
] ]
}, },
{ {

View File

@ -362,26 +362,26 @@
} }
}, },
"node_modules/@babel/helpers": { "node_modules/@babel/helpers": {
"version": "7.26.0", "version": "7.26.10",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.10.tgz",
"integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==", "integrity": "sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/template": "^7.25.9", "@babel/template": "^7.26.9",
"@babel/types": "^7.26.0" "@babel/types": "^7.26.10"
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@babel/parser": { "node_modules/@babel/parser": {
"version": "7.26.3", "version": "7.26.10",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.3.tgz", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.10.tgz",
"integrity": "sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==", "integrity": "sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/types": "^7.26.3" "@babel/types": "^7.26.10"
}, },
"bin": { "bin": {
"parser": "bin/babel-parser.js" "parser": "bin/babel-parser.js"
@ -423,9 +423,9 @@
} }
}, },
"node_modules/@babel/runtime": { "node_modules/@babel/runtime": {
"version": "7.26.0", "version": "7.26.10",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz",
"integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==", "integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"regenerator-runtime": "^0.14.0" "regenerator-runtime": "^0.14.0"
@ -435,14 +435,14 @@
} }
}, },
"node_modules/@babel/template": { "node_modules/@babel/template": {
"version": "7.25.9", "version": "7.26.9",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz",
"integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", "integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.25.9", "@babel/code-frame": "^7.26.2",
"@babel/parser": "^7.25.9", "@babel/parser": "^7.26.9",
"@babel/types": "^7.25.9" "@babel/types": "^7.26.9"
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
@ -476,9 +476,9 @@
} }
}, },
"node_modules/@babel/types": { "node_modules/@babel/types": {
"version": "7.26.3", "version": "7.26.10",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.10.tgz",
"integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", "integrity": "sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/helper-string-parser": "^7.25.9", "@babel/helper-string-parser": "^7.25.9",
@ -1010,6 +1010,23 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.1.tgz",
"integrity": "sha512-O96poM2XGhLtpTh+s4+nP7YCCAfb4tJNRVZHfIE7dgmax+yMP2WgMd2OecBuaATHKTHsLWHQeuaxMRnCsH8+5g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": { "node_modules/@esbuild/netbsd-x64": {
"version": "0.21.5", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
@ -1028,9 +1045,9 @@
} }
}, },
"node_modules/@esbuild/openbsd-arm64": { "node_modules/@esbuild/openbsd-arm64": {
"version": "0.23.1", "version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.1.tgz",
"integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==", "integrity": "sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -1647,12 +1664,12 @@
} }
}, },
"node_modules/@octokit/endpoint": { "node_modules/@octokit/endpoint": {
"version": "10.1.1", "version": "10.1.3",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz", "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.3.tgz",
"integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==", "integrity": "sha512-nBRBMpKPhQUxCsQQeW+rCJ/OPSMcj3g0nfHn01zGYZXuNDvvXudF/TYY6APj5THlurerpFN4a/dQAIAaM6BYhA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@octokit/types": "^13.0.0", "@octokit/types": "^13.6.2",
"universal-user-agent": "^7.0.2" "universal-user-agent": "^7.0.2"
}, },
"engines": { "engines": {
@ -1674,18 +1691,18 @@
} }
}, },
"node_modules/@octokit/openapi-types": { "node_modules/@octokit/openapi-types": {
"version": "22.2.0", "version": "24.2.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz", "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz",
"integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==", "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@octokit/plugin-paginate-rest": { "node_modules/@octokit/plugin-paginate-rest": {
"version": "11.3.6", "version": "11.6.0",
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.3.6.tgz", "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.6.0.tgz",
"integrity": "sha512-zcvqqf/+TicbTCa/Z+3w4eBJcAxCFymtc0UAIsR3dEVoNilWld4oXdscQ3laXamTszUZdusw97K8+DrbFiOwjw==", "integrity": "sha512-n5KPteiF7pWKgBIBJSk8qzoZWcUkza2O6A0za97pMGVrGfPdltxrfmfF5GucHYvHGZD8BdaZmmHGz5cX/3gdpw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@octokit/types": "^13.6.2" "@octokit/types": "^13.10.0"
}, },
"engines": { "engines": {
"node": ">= 18" "node": ">= 18"
@ -1722,14 +1739,15 @@
} }
}, },
"node_modules/@octokit/request": { "node_modules/@octokit/request": {
"version": "9.1.3", "version": "9.2.2",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.1.3.tgz", "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.2.2.tgz",
"integrity": "sha512-V+TFhu5fdF3K58rs1pGUJIDH5RZLbZm5BI+MNF+6o/ssFNT4vWlCh/tVpF3NxGtP15HUxTTMUbsG5llAuU2CZA==", "integrity": "sha512-dZl0ZHx6gOQGcffgm1/Sf6JfEpmh34v3Af2Uci02vzUYz6qEN6zepoRtmybWXIGXFIK8K9ylE3b+duCWqhArtg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@octokit/endpoint": "^10.0.0", "@octokit/endpoint": "^10.1.3",
"@octokit/request-error": "^6.0.1", "@octokit/request-error": "^6.1.7",
"@octokit/types": "^13.1.0", "@octokit/types": "^13.6.2",
"fast-content-type-parse": "^2.0.0",
"universal-user-agent": "^7.0.2" "universal-user-agent": "^7.0.2"
}, },
"engines": { "engines": {
@ -1737,12 +1755,12 @@
} }
}, },
"node_modules/@octokit/request-error": { "node_modules/@octokit/request-error": {
"version": "6.1.5", "version": "6.1.7",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.5.tgz", "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.7.tgz",
"integrity": "sha512-IlBTfGX8Yn/oFPMwSfvugfncK2EwRLjzbrpifNaMY8o/HTEAFqCA1FZxjD9cWvSKBHgrIhc4CSBIzMxiLsbzFQ==", "integrity": "sha512-69NIppAwaauwZv6aOzb+VVLwt+0havz9GT5YplkeJv7fG7a40qpLt/yZKyiDxAhgz0EtgNdNcb96Z0u+Zyuy2g==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@octokit/types": "^13.0.0" "@octokit/types": "^13.6.2"
}, },
"engines": { "engines": {
"node": ">= 18" "node": ">= 18"
@ -1764,12 +1782,12 @@
} }
}, },
"node_modules/@octokit/types": { "node_modules/@octokit/types": {
"version": "13.6.2", "version": "13.10.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.6.2.tgz", "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz",
"integrity": "sha512-WpbZfZUcZU77DrSW4wbsSgTPfKcp286q3ItaIgvSbBpZJlu6mnYXAkjZz6LVZPXkEvLIM8McanyZejKTYUHipA==", "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@octokit/openapi-types": "^22.2.0" "@octokit/openapi-types": "^24.2.0"
} }
}, },
"node_modules/@peculiar/asn1-cms": { "node_modules/@peculiar/asn1-cms": {
@ -4956,9 +4974,9 @@
} }
}, },
"node_modules/axios": { "node_modules/axios": {
"version": "1.7.9", "version": "1.8.3",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz",
"integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", "integrity": "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"follow-redirects": "^1.15.6", "follow-redirects": "^1.15.6",
@ -5451,9 +5469,9 @@
"license": "CC-BY-4.0" "license": "CC-BY-4.0"
}, },
"node_modules/canvg": { "node_modules/canvg": {
"version": "3.0.10", "version": "3.0.11",
"resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.10.tgz", "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz",
"integrity": "sha512-qwR2FRNO9NlzTeKIPIKpnTY6fqwuYSequ8Ru8c0YkYU7U0oW+hLUvWadLvAu1Rl72OMNiFhoLu4f8eUjQ7l/+Q==", "integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==",
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
@ -7314,6 +7332,22 @@
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
}, },
"node_modules/fast-content-type-parse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-2.0.1.tgz",
"integrity": "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "MIT"
},
"node_modules/fast-deep-equal": { "node_modules/fast-deep-equal": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@ -12588,13 +12622,13 @@
"license": "0BSD" "license": "0BSD"
}, },
"node_modules/tsx": { "node_modules/tsx": {
"version": "4.19.2", "version": "4.19.3",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.2.tgz", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.3.tgz",
"integrity": "sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==", "integrity": "sha512-4H8vUNGNjQ4V2EOoGw005+c+dGuPSnhpPBPHBtsZdGZBk/iJb4kguGlPWaZTZ3q5nMtFOEsY0nRDlh9PJyd6SQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"esbuild": "~0.23.0", "esbuild": "~0.25.0",
"get-tsconfig": "^4.7.5" "get-tsconfig": "^4.7.5"
}, },
"bin": { "bin": {
@ -12608,9 +12642,9 @@
} }
}, },
"node_modules/tsx/node_modules/@esbuild/aix-ppc64": { "node_modules/tsx/node_modules/@esbuild/aix-ppc64": {
"version": "0.23.1", "version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.1.tgz",
"integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==", "integrity": "sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@ -12625,9 +12659,9 @@
} }
}, },
"node_modules/tsx/node_modules/@esbuild/android-arm": { "node_modules/tsx/node_modules/@esbuild/android-arm": {
"version": "0.23.1", "version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.1.tgz",
"integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==", "integrity": "sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -12642,9 +12676,9 @@
} }
}, },
"node_modules/tsx/node_modules/@esbuild/android-arm64": { "node_modules/tsx/node_modules/@esbuild/android-arm64": {
"version": "0.23.1", "version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.1.tgz",
"integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==", "integrity": "sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -12659,9 +12693,9 @@
} }
}, },
"node_modules/tsx/node_modules/@esbuild/android-x64": { "node_modules/tsx/node_modules/@esbuild/android-x64": {
"version": "0.23.1", "version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.1.tgz",
"integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==", "integrity": "sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -12676,9 +12710,9 @@
} }
}, },
"node_modules/tsx/node_modules/@esbuild/darwin-arm64": { "node_modules/tsx/node_modules/@esbuild/darwin-arm64": {
"version": "0.23.1", "version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.1.tgz",
"integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==", "integrity": "sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -12693,9 +12727,9 @@
} }
}, },
"node_modules/tsx/node_modules/@esbuild/darwin-x64": { "node_modules/tsx/node_modules/@esbuild/darwin-x64": {
"version": "0.23.1", "version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.1.tgz",
"integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==", "integrity": "sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -12710,9 +12744,9 @@
} }
}, },
"node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": {
"version": "0.23.1", "version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.1.tgz",
"integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==", "integrity": "sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -12727,9 +12761,9 @@
} }
}, },
"node_modules/tsx/node_modules/@esbuild/freebsd-x64": { "node_modules/tsx/node_modules/@esbuild/freebsd-x64": {
"version": "0.23.1", "version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.1.tgz",
"integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==", "integrity": "sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -12744,9 +12778,9 @@
} }
}, },
"node_modules/tsx/node_modules/@esbuild/linux-arm": { "node_modules/tsx/node_modules/@esbuild/linux-arm": {
"version": "0.23.1", "version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.1.tgz",
"integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==", "integrity": "sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -12761,9 +12795,9 @@
} }
}, },
"node_modules/tsx/node_modules/@esbuild/linux-arm64": { "node_modules/tsx/node_modules/@esbuild/linux-arm64": {
"version": "0.23.1", "version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.1.tgz",
"integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==", "integrity": "sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -12778,9 +12812,9 @@
} }
}, },
"node_modules/tsx/node_modules/@esbuild/linux-ia32": { "node_modules/tsx/node_modules/@esbuild/linux-ia32": {
"version": "0.23.1", "version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.1.tgz",
"integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==", "integrity": "sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@ -12795,9 +12829,9 @@
} }
}, },
"node_modules/tsx/node_modules/@esbuild/linux-loong64": { "node_modules/tsx/node_modules/@esbuild/linux-loong64": {
"version": "0.23.1", "version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.1.tgz",
"integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==", "integrity": "sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg==",
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
@ -12812,9 +12846,9 @@
} }
}, },
"node_modules/tsx/node_modules/@esbuild/linux-mips64el": { "node_modules/tsx/node_modules/@esbuild/linux-mips64el": {
"version": "0.23.1", "version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.1.tgz",
"integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==", "integrity": "sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg==",
"cpu": [ "cpu": [
"mips64el" "mips64el"
], ],
@ -12829,9 +12863,9 @@
} }
}, },
"node_modules/tsx/node_modules/@esbuild/linux-ppc64": { "node_modules/tsx/node_modules/@esbuild/linux-ppc64": {
"version": "0.23.1", "version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.1.tgz",
"integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==", "integrity": "sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@ -12846,9 +12880,9 @@
} }
}, },
"node_modules/tsx/node_modules/@esbuild/linux-riscv64": { "node_modules/tsx/node_modules/@esbuild/linux-riscv64": {
"version": "0.23.1", "version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.1.tgz",
"integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==", "integrity": "sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@ -12863,9 +12897,9 @@
} }
}, },
"node_modules/tsx/node_modules/@esbuild/linux-s390x": { "node_modules/tsx/node_modules/@esbuild/linux-s390x": {
"version": "0.23.1", "version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.1.tgz",
"integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==", "integrity": "sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
@ -12880,9 +12914,9 @@
} }
}, },
"node_modules/tsx/node_modules/@esbuild/linux-x64": { "node_modules/tsx/node_modules/@esbuild/linux-x64": {
"version": "0.23.1", "version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.1.tgz",
"integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==", "integrity": "sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -12897,9 +12931,9 @@
} }
}, },
"node_modules/tsx/node_modules/@esbuild/netbsd-x64": { "node_modules/tsx/node_modules/@esbuild/netbsd-x64": {
"version": "0.23.1", "version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.1.tgz",
"integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==", "integrity": "sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -12914,9 +12948,9 @@
} }
}, },
"node_modules/tsx/node_modules/@esbuild/openbsd-x64": { "node_modules/tsx/node_modules/@esbuild/openbsd-x64": {
"version": "0.23.1", "version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.1.tgz",
"integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==", "integrity": "sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -12931,9 +12965,9 @@
} }
}, },
"node_modules/tsx/node_modules/@esbuild/sunos-x64": { "node_modules/tsx/node_modules/@esbuild/sunos-x64": {
"version": "0.23.1", "version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.1.tgz",
"integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==", "integrity": "sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -12948,9 +12982,9 @@
} }
}, },
"node_modules/tsx/node_modules/@esbuild/win32-arm64": { "node_modules/tsx/node_modules/@esbuild/win32-arm64": {
"version": "0.23.1", "version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.1.tgz",
"integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==", "integrity": "sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -12965,9 +12999,9 @@
} }
}, },
"node_modules/tsx/node_modules/@esbuild/win32-ia32": { "node_modules/tsx/node_modules/@esbuild/win32-ia32": {
"version": "0.23.1", "version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.1.tgz",
"integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==", "integrity": "sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@ -12982,9 +13016,9 @@
} }
}, },
"node_modules/tsx/node_modules/@esbuild/win32-x64": { "node_modules/tsx/node_modules/@esbuild/win32-x64": {
"version": "0.23.1", "version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.1.tgz",
"integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==", "integrity": "sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -12999,9 +13033,9 @@
} }
}, },
"node_modules/tsx/node_modules/esbuild": { "node_modules/tsx/node_modules/esbuild": {
"version": "0.23.1", "version": "0.25.1",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.1.tgz",
"integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==", "integrity": "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
@ -13012,30 +13046,31 @@
"node": ">=18" "node": ">=18"
}, },
"optionalDependencies": { "optionalDependencies": {
"@esbuild/aix-ppc64": "0.23.1", "@esbuild/aix-ppc64": "0.25.1",
"@esbuild/android-arm": "0.23.1", "@esbuild/android-arm": "0.25.1",
"@esbuild/android-arm64": "0.23.1", "@esbuild/android-arm64": "0.25.1",
"@esbuild/android-x64": "0.23.1", "@esbuild/android-x64": "0.25.1",
"@esbuild/darwin-arm64": "0.23.1", "@esbuild/darwin-arm64": "0.25.1",
"@esbuild/darwin-x64": "0.23.1", "@esbuild/darwin-x64": "0.25.1",
"@esbuild/freebsd-arm64": "0.23.1", "@esbuild/freebsd-arm64": "0.25.1",
"@esbuild/freebsd-x64": "0.23.1", "@esbuild/freebsd-x64": "0.25.1",
"@esbuild/linux-arm": "0.23.1", "@esbuild/linux-arm": "0.25.1",
"@esbuild/linux-arm64": "0.23.1", "@esbuild/linux-arm64": "0.25.1",
"@esbuild/linux-ia32": "0.23.1", "@esbuild/linux-ia32": "0.25.1",
"@esbuild/linux-loong64": "0.23.1", "@esbuild/linux-loong64": "0.25.1",
"@esbuild/linux-mips64el": "0.23.1", "@esbuild/linux-mips64el": "0.25.1",
"@esbuild/linux-ppc64": "0.23.1", "@esbuild/linux-ppc64": "0.25.1",
"@esbuild/linux-riscv64": "0.23.1", "@esbuild/linux-riscv64": "0.25.1",
"@esbuild/linux-s390x": "0.23.1", "@esbuild/linux-s390x": "0.25.1",
"@esbuild/linux-x64": "0.23.1", "@esbuild/linux-x64": "0.25.1",
"@esbuild/netbsd-x64": "0.23.1", "@esbuild/netbsd-arm64": "0.25.1",
"@esbuild/openbsd-arm64": "0.23.1", "@esbuild/netbsd-x64": "0.25.1",
"@esbuild/openbsd-x64": "0.23.1", "@esbuild/openbsd-arm64": "0.25.1",
"@esbuild/sunos-x64": "0.23.1", "@esbuild/openbsd-x64": "0.25.1",
"@esbuild/win32-arm64": "0.23.1", "@esbuild/sunos-x64": "0.25.1",
"@esbuild/win32-ia32": "0.23.1", "@esbuild/win32-arm64": "0.25.1",
"@esbuild/win32-x64": "0.23.1" "@esbuild/win32-ia32": "0.25.1",
"@esbuild/win32-x64": "0.25.1"
} }
}, },
"node_modules/tsyringe": { "node_modules/tsyringe": {
@ -13552,9 +13587,9 @@
} }
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "5.4.11", "version": "5.4.14",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.14.tgz",
"integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==", "integrity": "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {

View File

@ -143,6 +143,7 @@ export const useGetProjectSecretsOverview = (
search = "", search = "",
includeSecrets, includeSecrets,
includeFolders, includeFolders,
includeImports,
includeDynamicSecrets, includeDynamicSecrets,
environments environments
}: TGetDashboardProjectSecretsOverviewDTO, }: TGetDashboardProjectSecretsOverviewDTO,
@ -170,6 +171,7 @@ export const useGetProjectSecretsOverview = (
projectId, projectId,
includeSecrets, includeSecrets,
includeFolders, includeFolders,
includeImports,
includeDynamicSecrets, includeDynamicSecrets,
environments environments
}), }),
@ -184,6 +186,7 @@ export const useGetProjectSecretsOverview = (
projectId, projectId,
includeSecrets, includeSecrets,
includeFolders, includeFolders,
includeImports,
includeDynamicSecrets, includeDynamicSecrets,
environments environments
}), }),
@ -197,12 +200,15 @@ export const useGetProjectSecretsOverview = (
? unique(select.dynamicSecrets, (i) => i.name) ? unique(select.dynamicSecrets, (i) => i.name)
: []; : [];
const uniqueSecretImports = select.imports ? unique(select.imports, (i) => i.id) : [];
return { return {
...select, ...select,
secrets: secrets ? mergePersonalSecrets(secrets) : undefined, secrets: secrets ? mergePersonalSecrets(secrets) : undefined,
totalUniqueSecretsInPage: uniqueSecrets.length, totalUniqueSecretsInPage: uniqueSecrets.length,
totalUniqueDynamicSecretsInPage: uniqueDynamicSecrets.length, totalUniqueDynamicSecretsInPage: uniqueDynamicSecrets.length,
totalUniqueFoldersInPage: uniqueFolders.length totalUniqueFoldersInPage: uniqueFolders.length,
totalUniqueSecretImportsInPage: uniqueSecretImports.length
}; };
}, []), }, []),
placeholderData: (previousData) => previousData placeholderData: (previousData) => previousData

View File

@ -9,13 +9,16 @@ export type DashboardProjectSecretsOverviewResponse = {
folders?: (TSecretFolder & { environment: string })[]; folders?: (TSecretFolder & { environment: string })[];
dynamicSecrets?: (TDynamicSecret & { environment: string })[]; dynamicSecrets?: (TDynamicSecret & { environment: string })[];
secrets?: SecretV3Raw[]; secrets?: SecretV3Raw[];
imports?: TSecretImport[];
totalSecretCount?: number; totalSecretCount?: number;
totalFolderCount?: number; totalFolderCount?: number;
totalDynamicSecretCount?: number; totalDynamicSecretCount?: number;
totalImportCount?: number;
totalCount: number; totalCount: number;
totalUniqueSecretsInPage: number; totalUniqueSecretsInPage: number;
totalUniqueDynamicSecretsInPage: number; totalUniqueDynamicSecretsInPage: number;
totalUniqueFoldersInPage: number; totalUniqueFoldersInPage: number;
totalUniqueSecretImportsInPage: number;
}; };
export type DashboardProjectSecretsDetailsResponse = { export type DashboardProjectSecretsDetailsResponse = {
@ -63,6 +66,7 @@ export type TGetDashboardProjectSecretsOverviewDTO = {
includeSecrets?: boolean; includeSecrets?: boolean;
includeFolders?: boolean; includeFolders?: boolean;
includeDynamicSecrets?: boolean; includeDynamicSecrets?: boolean;
includeImports?: boolean;
environments: string[]; environments: string[];
}; };

View File

@ -350,7 +350,7 @@ export type AddIdentityKubernetesAuthDTO = {
organizationId: string; organizationId: string;
identityId: string; identityId: string;
kubernetesHost: string; kubernetesHost: string;
tokenReviewerJwt: string; tokenReviewerJwt?: string;
allowedNamespaces: string; allowedNamespaces: string;
allowedNames: string; allowedNames: string;
allowedAudience: string; allowedAudience: string;
@ -367,7 +367,7 @@ export type UpdateIdentityKubernetesAuthDTO = {
organizationId: string; organizationId: string;
identityId: string; identityId: string;
kubernetesHost?: string; kubernetesHost?: string;
tokenReviewerJwt?: string; tokenReviewerJwt?: string | null;
allowedNamespaces?: string; allowedNamespaces?: string;
allowedNames?: string; allowedNames?: string;
allowedAudience?: string; allowedAudience?: string;

View File

@ -14,6 +14,7 @@ export type TSecretImport = {
isReplicationSuccess?: boolean; isReplicationSuccess?: boolean;
replicationStatus?: string; replicationStatus?: string;
lastReplicated?: string; lastReplicated?: string;
environment?: string;
}; };
export type TGetImportedFoldersByEnvDTO = { export type TGetImportedFoldersByEnvDTO = {

View File

@ -22,14 +22,37 @@ import "./translation";
// have a look at the Quick start guide // have a look at the Quick start guide
// for passing in lng and translations on init/ // for passing in lng and translations on init/
// https://vite.dev/guide/build#load-error-handling
window.addEventListener("vite:preloadError", () => {
window.location.reload(); // for example, refresh the page
});
// Create a new router instance // Create a new router instance
NProgress.configure({ showSpinner: false }); NProgress.configure({ showSpinner: false });
window.addEventListener("vite:preloadError", async (event) => {
event.preventDefault();
// Get current count from session storage or initialize to 0
const reloadCount = parseInt(sessionStorage.getItem("vitePreloadErrorCount") || "0", 10);
// Check if we've already tried 3 times
if (reloadCount >= 2) {
console.warn("Vite preload has failed multiple times. Stopping automatic reload.");
// Optionally show a user-facing message here
return;
}
try {
if ("caches" in window) {
const keys = await caches.keys();
await Promise.all(keys.map((key) => caches.delete(key)));
}
} catch (cleanupError) {
console.error(cleanupError);
}
//
// Increment and save the counter
sessionStorage.setItem("vitePreloadErrorCount", (reloadCount + 1).toString());
console.log(`Reloading page (attempt ${reloadCount + 1} of 2)...`);
window.location.reload(); // for example, refresh the page
});
const router = createRouter({ const router = createRouter({
routeTree, routeTree,
context: { serverConfig: null, queryClient }, context: { serverConfig: null, queryClient },

View File

@ -18,14 +18,6 @@ export const RequestNewInvitePage = () => {
<span className="rounded-md bg-primary-500/40 px-1 text-black">Note:</span> If it still <span className="rounded-md bg-primary-500/40 px-1 text-black">Note:</span> If it still
doesn&apos;t work, please reach out to us at support@infisical.com doesn&apos;t work, please reach out to us at support@infisical.com
</p> </p>
<div className="">
<img
src="/images/invitation-expired.svg"
height={500}
width={800}
alt="invitation expired illustration"
/>
</div>
</div> </div>
</div> </div>
); );

View File

@ -223,15 +223,10 @@ export const SignupInvitePage = () => {
// Step 4 of the sign up process (download the emergency kit pdf) // Step 4 of the sign up process (download the emergency kit pdf)
const stepConfirmEmail = ( const stepConfirmEmail = (
<div className="h-7/12 mx-1 mb-36 flex w-full max-w-xs flex-col items-center rounded-xl border border-mineshaft-600 bg-mineshaft-800 px-4 py-8 drop-shadow-xl md:mb-16 md:max-w-lg md:px-6"> <div className="h-7/12 mx-1 mb-36 flex w-full max-w-xs flex-col items-center rounded-xl border border-mineshaft-600 bg-mineshaft-800 px-4 py-8 drop-shadow-xl md:mb-16 md:max-w-lg md:px-6">
<p className="mb-6 flex justify-center text-center text-4xl font-semibold text-primary-100"> <p className="mb-2 flex justify-center text-center text-4xl font-semibold text-primary-100">
Confirm your email Confirm your email
</p> </p>
<img <div className="mx-auto mb-2 mt-4 flex max-h-24 max-w-md flex-col items-center justify-center px-4 text-lg md:p-2">
src="/images/dragon-signupinvite.svg"
style={{ height: "262px", width: "410px" }}
alt="verify email"
/>
<div className="mx-auto mb-2 mt-10 flex max-h-24 max-w-md flex-col items-center justify-center px-4 text-lg md:p-2">
<Button <Button
onClick={async () => { onClick={async () => {
try { try {

View File

@ -31,7 +31,7 @@ import { IdentityFormTab } from "./types";
const schema = z const schema = z
.object({ .object({
kubernetesHost: z.string().min(1), kubernetesHost: z.string().min(1),
tokenReviewerJwt: z.string().min(1), tokenReviewerJwt: z.string().optional(),
allowedNames: z.string(), allowedNames: z.string(),
allowedNamespaces: z.string(), allowedNamespaces: z.string(),
allowedAudience: z.string(), allowedAudience: z.string(),
@ -166,7 +166,7 @@ export const IdentityKubernetesAuthForm = ({
await updateMutateAsync({ await updateMutateAsync({
organizationId: orgId, organizationId: orgId,
kubernetesHost, kubernetesHost,
tokenReviewerJwt, tokenReviewerJwt: tokenReviewerJwt || null,
allowedNames, allowedNames,
allowedNamespaces, allowedNamespaces,
allowedAudience, allowedAudience,
@ -182,7 +182,7 @@ export const IdentityKubernetesAuthForm = ({
organizationId: orgId, organizationId: orgId,
identityId, identityId,
kubernetesHost: kubernetesHost || "", kubernetesHost: kubernetesHost || "",
tokenReviewerJwt, tokenReviewerJwt: tokenReviewerJwt || undefined,
allowedNames: allowedNames || "", allowedNames: allowedNames || "",
allowedNamespaces: allowedNamespaces || "", allowedNamespaces: allowedNamespaces || "",
allowedAudience: allowedAudience || "", allowedAudience: allowedAudience || "",
@ -255,11 +255,11 @@ export const IdentityKubernetesAuthForm = ({
name="tokenReviewerJwt" name="tokenReviewerJwt"
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => (
<FormControl <FormControl
tooltipClassName="max-w-md"
label="Token Reviewer JWT" label="Token Reviewer JWT"
isError={Boolean(error)} isError={Boolean(error)}
errorText={error?.message} errorText={error?.message}
tooltipText="A long-lived service account JWT token for Infisical to access the TokenReview API to validate other service account JWT tokens submitted by applications/pods." tooltipText="Optional JWT token for accessing Kubernetes TokenReview API. If provided, this long-lived token will be used to validate service account tokens during authentication. If omitted, the client's own JWT will be used instead, which requires the client to have the system:auth-delegator ClusterRole binding."
isRequired
> >
<Input {...field} placeholder="" type="password" /> <Input {...field} placeholder="" type="password" />
</FormControl> </FormControl>

View File

@ -18,7 +18,7 @@ export const AuditLogsPage = () => {
title="Audit logs" title="Audit logs"
description="Audit logs for security and compliance teams to monitor information access." description="Audit logs for security and compliance teams to monitor information access."
/> />
<LogsSection filterClassName="static py-2" showFilters isOrgAuditLogs showActorColumn /> <LogsSection filterClassName="static py-2" showFilters />
</div> </div>
</div> </div>
</div> </div>

View File

@ -10,7 +10,7 @@ import { ActorType, EventType, UserAgentType } from "@app/hooks/api/auditLogs/en
import { usePopUp } from "@app/hooks/usePopUp"; import { usePopUp } from "@app/hooks/usePopUp";
import { LogsFilter } from "./LogsFilter"; import { LogsFilter } from "./LogsFilter";
import { LogsTable, TAuditLogTableHeader } from "./LogsTable"; import { LogsTable } from "./LogsTable";
import { AuditLogFilterFormData, auditLogFilterFormSchema } from "./types"; import { AuditLogFilterFormData, auditLogFilterFormSchema } from "./types";
type Props = { type Props = {
@ -25,22 +25,11 @@ type Props = {
showFilters?: boolean; showFilters?: boolean;
filterClassName?: string; filterClassName?: string;
isOrgAuditLogs?: boolean;
showActorColumn?: boolean;
remappedHeaders?: Partial<Record<TAuditLogTableHeader, string>>;
refetchInterval?: number; refetchInterval?: number;
}; };
export const LogsSection = withPermission( export const LogsSection = withPermission(
({ ({ presets, filterClassName, refetchInterval, showFilters }: Props) => {
presets,
filterClassName,
remappedHeaders,
isOrgAuditLogs,
showActorColumn,
refetchInterval,
showFilters
}: Props) => {
const { subscription } = useSubscription(); const { subscription } = useSubscription();
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const); const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const);
@ -90,9 +79,6 @@ export const LogsSection = withPermission(
)} )}
<LogsTable <LogsTable
refetchInterval={refetchInterval} refetchInterval={refetchInterval}
remappedHeaders={remappedHeaders}
isOrgAuditLogs={isOrgAuditLogs}
showActorColumn={!!showActorColumn}
filter={{ filter={{
secretPath: debouncedSecretPath || undefined, secretPath: debouncedSecretPath || undefined,
eventMetadata: presets?.eventMetadata, eventMetadata: presets?.eventMetadata,

View File

@ -1,5 +1,6 @@
import { Fragment } from "react"; import { Fragment } from "react";
import { faFile } from "@fortawesome/free-solid-svg-icons"; import { faFile, faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { import {
Button, Button,
@ -11,6 +12,7 @@ import {
Td, Td,
Th, Th,
THead, THead,
Tooltip,
Tr Tr
} from "@app/components/v2"; } from "@app/components/v2";
import { useGetAuditLogs } from "@app/hooks/api"; import { useGetAuditLogs } from "@app/hooks/api";
@ -19,32 +21,13 @@ import { TGetAuditLogsFilter } from "@app/hooks/api/auditLogs/types";
import { LogsTableRow } from "./LogsTableRow"; import { LogsTableRow } from "./LogsTableRow";
type Props = { type Props = {
isOrgAuditLogs?: boolean;
showActorColumn: boolean;
filter?: TGetAuditLogsFilter; filter?: TGetAuditLogsFilter;
remappedHeaders?: Partial<Record<TAuditLogTableHeader, string>>;
refetchInterval?: number; refetchInterval?: number;
}; };
const AUDIT_LOG_LIMIT = 15; const AUDIT_LOG_LIMIT = 15;
const TABLE_HEADERS = [ export const LogsTable = ({ filter, refetchInterval }: Props) => {
"Timestamp (MM/DD/YYYY)",
"Event",
"Project",
"Actor",
"Source",
"Metadata"
] as const;
export type TAuditLogTableHeader = (typeof TABLE_HEADERS)[number];
export const LogsTable = ({
showActorColumn,
isOrgAuditLogs,
filter,
remappedHeaders,
refetchInterval
}: Props) => {
// Determine the project ID for filtering // Determine the project ID for filtering
const filterProjectId = const filterProjectId =
// Use the projectId from the filter if it exists // Use the projectId from the filter if it exists
@ -69,38 +52,37 @@ export const LogsTable = ({
<Table> <Table>
<THead> <THead>
<Tr> <Tr>
{TABLE_HEADERS.map((header, idx) => { <Th className="w-24" />
if ( <Th className="w-64">
(header === "Project" && !isOrgAuditLogs) || Timestamp
(header === "Actor" && !showActorColumn) <Tooltip
) { className="normal-case"
return null; content="Time displayed in your system's time zone."
} sideOffset={10}
>
return ( <FontAwesomeIcon icon={faInfoCircle} className="ml-1" />
<Th key={`table-header-${idx + 1}`}>{remappedHeaders?.[header] || header}</Th> </Tooltip>
); </Th>
})} <Th>Event</Th>
</Tr> </Tr>
</THead> </THead>
<TBody> <TBody>
{!isPending && {!isPending &&
data?.pages?.map((group, i) => ( data?.pages?.map((group, i) => (
<Fragment key={`audit-log-fragment-${i + 1}`}> <Fragment key={`audit-log-fragment-${i + 1}`}>
{group.map((auditLog) => ( {group.map((auditLog, index) => (
<LogsTableRow <LogsTableRow
showActorColumn={showActorColumn} rowNumber={index + i * AUDIT_LOG_LIMIT + 1}
isOrgAuditLogs={isOrgAuditLogs}
auditLog={auditLog} auditLog={auditLog}
key={`audit-log-${auditLog.id}`} key={`audit-log-${auditLog.id}`}
/> />
))} ))}
</Fragment> </Fragment>
))} ))}
{isPending && <TableSkeleton innerKey="logs-table" columns={5} key="logs" />} {isPending && <TableSkeleton innerKey="logs-table" columns={3} key="logs-loading" />}
{isEmpty && ( {isEmpty && (
<Tr> <Tr>
<Td colSpan={5}> <Td colSpan={3}>
<EmptyState title="No audit logs on file" icon={faFile} /> <EmptyState title="No audit logs on file" icon={faFile} />
</Td> </Td>
</Tr> </Tr>

View File

@ -1,128 +1,76 @@
import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons"; import { faCaretDown, faCaretRight } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format } from "date-fns";
import { Td, Tooltip, Tr } from "@app/components/v2"; import { Td, Tr } from "@app/components/v2";
import { eventToNameMap, userAgentTTypeoNameMap } from "@app/hooks/api/auditLogs/constants"; import { useToggle } from "@app/hooks";
import { ActorType, EventType } from "@app/hooks/api/auditLogs/enums"; import { ActorType } from "@app/hooks/api/auditLogs/enums";
import { Actor, AuditLog } from "@app/hooks/api/auditLogs/types"; import { AuditLog } from "@app/hooks/api/auditLogs/types";
type Props = { type Props = {
auditLog: AuditLog; auditLog: AuditLog;
isOrgAuditLogs?: boolean; rowNumber: number;
showActorColumn: boolean;
}; };
export const LogsTableRow = ({ auditLog, isOrgAuditLogs, showActorColumn }: Props) => { type TagProps = {
const renderActor = (actor: Actor) => { label: string;
if (!actor) { value?: string;
return <Td />; };
} const Tag = ({ label, value }: TagProps) => {
if (!value) return null;
switch (actor.type) {
case ActorType.USER:
return ( return (
<Td> <div className="flex items-center space-x-1.5">
<p>{actor.metadata.email}</p> <div className="rounded bg-mineshaft-600 p-0.5 pl-1 font-mono">{label}:</div>
<p>User</p> <div>{value}</div>
</Td> </div>
); );
case ActorType.SERVICE: };
export const LogsTableRow = ({ auditLog, rowNumber }: Props) => {
const [isOpen, setIsOpen] = useToggle();
return ( return (
<Td> <>
<p>{`${actor.metadata.name}`}</p> <Tr
<p>Service token</p> className="h-10 cursor-pointer border-x-0 border-b border-t-0 hover:bg-mineshaft-700"
role="button"
tabIndex={0}
onClick={() => setIsOpen.toggle()}
onKeyDown={(evt) => {
if (evt.key === "Enter") setIsOpen.toggle();
}}
isHoverable
>
<Td className="flex items-center gap-2 pr-0 align-top">
<FontAwesomeIcon icon={isOpen ? faCaretDown : faCaretRight} />
{rowNumber}
</Td> </Td>
); <Td className="align-top">
case ActorType.IDENTITY: {format(new Date(auditLog.createdAt), "MMM do yyyy, hh:mm a")}
return (
<Td>
<p>{`${actor.metadata.name}`}</p>
<p>Machine Identity</p>
</Td> </Td>
);
case ActorType.PLATFORM:
return (
<Td> <Td>
<p>Platform</p> <div className="flex flex-wrap gap-4 text-sm">
</Td> <Tag label="event" value={auditLog.event.type} />
); <Tag label="actor" value={auditLog.actor.type} />
case ActorType.KMIP_CLIENT: {auditLog.actor.type === ActorType.USER && (
return ( <Tag label="user_email" value={auditLog.actor.metadata.email} />
<Td> )}
<p>{actor.metadata.name}</p> {auditLog.actor.type === ActorType.IDENTITY && (
<p>KMIP Client</p> <Tag label="identity_name" value={auditLog.actor.metadata.name} />
</Td> )}
);
case ActorType.UNKNOWN_USER:
return (
<Td>
<div className="flex items-center gap-2">
<p>Unknown User</p>
<Tooltip content="This action was performed by a user who was not authenticated at the time.">
<FontAwesomeIcon className="text-mineshaft-400" icon={faQuestionCircle} />
</Tooltip>
</div> </div>
</Td> </Td>
);
default:
return <Td />;
}
};
const formatDate = (dateToFormat: string) => {
const date = new Date(dateToFormat);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
let hours = date.getHours();
const minutes = String(date.getMinutes()).padStart(2, "0");
// convert from 24h to 12h format
const period = hours >= 12 ? "PM" : "AM";
hours %= 12;
hours = hours || 12; // the hour '0' should be '12'
const formattedDate = `${month}-${day}-${year} at ${hours}:${minutes} ${period}`;
return formattedDate;
};
const renderSource = () => {
const { event, actor } = auditLog;
if (event.type === EventType.INTEGRATION_SYNCED) {
if (actor.type === ActorType.USER) {
return (
<Td>
<p>Manually triggered by {actor.metadata.email}</p>
</Td>
);
}
// Platform / automatic syncs
return (
<Td>
<p>Automatically synced by Infisical</p>
</Td>
);
}
return (
<Td>
<p>{userAgentTTypeoNameMap[auditLog.userAgentType]}</p>
<p>{auditLog.ipAddress}</p>
</Td>
);
};
return (
<Tr className={`log-${auditLog.id} h-10 border-x-0 border-b border-t-0`}>
<Td>{formatDate(auditLog.createdAt)}</Td>
<Td>{`${eventToNameMap[auditLog.event.type]}`}</Td>
{isOrgAuditLogs && <Td>{auditLog?.projectName ?? auditLog?.projectId ?? "N/A"}</Td>}
{showActorColumn && renderActor(auditLog.actor)}
{renderSource()}
<Td className="max-w-xs break-all">{JSON.stringify(auditLog.event.metadata || {})}</Td>
</Tr> </Tr>
{isOpen && (
<Tr className={`log-${auditLog.id} h-10 border-x-0 border-b border-t-0`}>
<Td colSpan={3} className="px-3 py-2">
<div className="thin-scrollbar my-1 max-h-96 overflow-auto whitespace-pre-wrap rounded border border-mineshaft-600 bg-bunker-800 p-2 font-mono leading-6">
{JSON.stringify(auditLog, null, 4)}
</div>
</Td>
</Tr>
)}
</>
); );
}; };

View File

@ -70,11 +70,14 @@ export const ViewIdentityKubernetesAuthContent = ({
{data.kubernetesHost} {data.kubernetesHost}
</IdentityAuthFieldDisplay> </IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay className="col-span-2" label="Token Reviewer JWT"> <IdentityAuthFieldDisplay className="col-span-2" label="Token Reviewer JWT">
{data.tokenReviewerJwt ? (
<Tooltip <Tooltip
side="right" side="right"
className="max-w-xl p-2" className="max-w-xl p-2"
content={ content={
<p className="break-words rounded bg-mineshaft-600 p-2">{data.tokenReviewerJwt}</p> <p className="break-words rounded bg-mineshaft-600 p-2">
{data.tokenReviewerJwt || "Not provided"}
</p>
} }
> >
<div className="w-min"> <div className="w-min">
@ -84,6 +87,9 @@ export const ViewIdentityKubernetesAuthContent = ({
</Badge> </Badge>
</div> </div>
</Tooltip> </Tooltip>
) : (
<p className="text-base italic leading-4 text-bunker-400">Not set</p>
)}
</IdentityAuthFieldDisplay> </IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay className="col-span-2" label="Allowed Service Account Names"> <IdentityAuthFieldDisplay className="col-span-2" label="Allowed Service Account Names">
{data.allowedNames {data.allowedNames

View File

@ -45,7 +45,6 @@ export const UserAuditLogsSection = withPermission(
presets={{ presets={{
actorId: orgMembership.user.id actorId: orgMembership.user.id
}} }}
isOrgAuditLogs
/> />
</div> </div>
) )

View File

@ -29,9 +29,6 @@ export const IntegrationAuditLogsSection = ({ integration }: Props) => {
</div> </div>
<LogsSection <LogsSection
refetchInterval={4000} refetchInterval={4000}
remappedHeaders={{
Metadata: "Sync Status"
}}
showFilters={false} showFilters={false}
presets={{ presets={{
eventMetadata: { integrationId: integration.id }, eventMetadata: { integrationId: integration.id },

View File

@ -192,6 +192,15 @@ export const IntegrationConnectionSection = ({ integration }: Props) => {
); );
} }
if (integration.integration === "windmill" && integration.url) {
return (
<div>
<FormLabel className="text-sm font-semibold text-mineshaft-300" label="Instance URL" />
<div className="text-sm text-mineshaft-300">{integration.url}</div>
</div>
);
}
return null; return null;
}; };

View File

@ -7,6 +7,7 @@ import {
faAngleDown, faAngleDown,
faArrowDown, faArrowDown,
faArrowUp, faArrowUp,
faFileImport,
faFingerprint, faFingerprint,
faFolder, faFolder,
faFolderBlank, faFolderBlank,
@ -80,6 +81,7 @@ import { CreateSecretForm } from "./components/CreateSecretForm";
import { FolderBreadCrumbs } from "./components/FolderBreadCrumbs"; import { FolderBreadCrumbs } from "./components/FolderBreadCrumbs";
import { SecretOverviewDynamicSecretRow } from "./components/SecretOverviewDynamicSecretRow"; import { SecretOverviewDynamicSecretRow } from "./components/SecretOverviewDynamicSecretRow";
import { SecretOverviewFolderRow } from "./components/SecretOverviewFolderRow"; import { SecretOverviewFolderRow } from "./components/SecretOverviewFolderRow";
import { SecretOverviewImportListView } from "./components/SecretOverviewImportListView";
import { import {
SecretNoAccessOverviewTableRow, SecretNoAccessOverviewTableRow,
SecretOverviewTableRow SecretOverviewTableRow
@ -97,7 +99,8 @@ export enum EntryType {
enum RowType { enum RowType {
Folder = "folder", Folder = "folder",
DynamicSecret = "dynamic", DynamicSecret = "dynamic",
Secret = "secret" Secret = "secret",
Import = "import"
} }
type Filter = { type Filter = {
@ -107,7 +110,8 @@ type Filter = {
const DEFAULT_FILTER_STATE = { const DEFAULT_FILTER_STATE = {
[RowType.Folder]: true, [RowType.Folder]: true,
[RowType.DynamicSecret]: true, [RowType.DynamicSecret]: true,
[RowType.Secret]: true [RowType.Secret]: true,
[RowType.Import]: true
}; };
export const OverviewPage = () => { export const OverviewPage = () => {
@ -216,6 +220,7 @@ export const OverviewPage = () => {
includeFolders: filter.folder, includeFolders: filter.folder,
includeDynamicSecrets: filter.dynamic, includeDynamicSecrets: filter.dynamic,
includeSecrets: filter.secret, includeSecrets: filter.secret,
includeImports: filter.import,
search: debouncedSearchFilter, search: debouncedSearchFilter,
limit, limit,
offset offset
@ -227,15 +232,29 @@ export const OverviewPage = () => {
secrets, secrets,
folders, folders,
dynamicSecrets, dynamicSecrets,
imports,
totalFolderCount, totalFolderCount,
totalSecretCount, totalSecretCount,
totalDynamicSecretCount, totalDynamicSecretCount,
totalImportCount,
totalCount = 0, totalCount = 0,
totalUniqueFoldersInPage, totalUniqueFoldersInPage,
totalUniqueSecretsInPage, totalUniqueSecretsInPage,
totalUniqueSecretImportsInPage,
totalUniqueDynamicSecretsInPage totalUniqueDynamicSecretsInPage
} = overview ?? {}; } = overview ?? {};
const importsShaped = imports
?.filter((el) => !el.isReserved)
?.map(({ importPath, importEnv }) => ({ importPath, importEnv }))
.filter(
(el, index, self) =>
index ===
self.findIndex(
(item) => item.importPath === el.importPath && item.importEnv.slug === el.importEnv.slug
)
);
useResetPageHelper({ useResetPageHelper({
totalCount, totalCount,
offset, offset,
@ -678,7 +697,6 @@ export const OverviewPage = () => {
<SecretV2MigrationSection /> <SecretV2MigrationSection />
</div> </div>
); );
return ( return (
<div className=""> <div className="">
<Helmet> <Helmet>
@ -767,6 +785,19 @@ export const OverviewPage = () => {
</Button> </Button>
</DropdownMenuItem> */} </DropdownMenuItem> */}
<DropdownMenuLabel>Filter project resources</DropdownMenuLabel> <DropdownMenuLabel>Filter project resources</DropdownMenuLabel>
<DropdownMenuItem
onClick={(e) => {
e.preventDefault();
handleToggleRowType(RowType.Import);
}}
icon={filter[RowType.Import] && <FontAwesomeIcon icon={faCheckCircle} />}
iconPos="right"
>
<div className="flex items-center gap-2">
<FontAwesomeIcon icon={faFileImport} className="text-green-700" />
<span>Imports</span>
</div>
</DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
@ -1093,6 +1124,17 @@ export const OverviewPage = () => {
key={`overview-${dynamicSecretName}-${index + 1}`} key={`overview-${dynamicSecretName}-${index + 1}`}
/> />
))} ))}
{filter.import &&
importsShaped &&
importsShaped?.length > 0 &&
importsShaped?.map((item, index) => (
<SecretOverviewImportListView
secretImport={item}
environments={visibleEnvs}
key={`overview-secret-input-${index + 1}`}
allSecretImports={imports}
/>
))}
{secKeys.map((key, index) => ( {secKeys.map((key, index) => (
<SecretOverviewTableRow <SecretOverviewTableRow
isSelected={Boolean(selectedEntries.secret[key])} isSelected={Boolean(selectedEntries.secret[key])}
@ -1116,7 +1158,8 @@ export const OverviewPage = () => {
(page * perPage > totalCount ? totalCount % perPage : perPage) - (page * perPage > totalCount ? totalCount % perPage : perPage) -
(totalUniqueFoldersInPage || 0) - (totalUniqueFoldersInPage || 0) -
(totalUniqueDynamicSecretsInPage || 0) - (totalUniqueDynamicSecretsInPage || 0) -
(totalUniqueSecretsInPage || 0), (totalUniqueSecretsInPage || 0) -
(totalUniqueSecretImportsInPage || 0),
0 0
)} )}
/> />
@ -1156,6 +1199,7 @@ export const OverviewPage = () => {
dynamicSecretCount={totalDynamicSecretCount} dynamicSecretCount={totalDynamicSecretCount}
secretCount={totalSecretCount} secretCount={totalSecretCount}
folderCount={totalFolderCount} folderCount={totalFolderCount}
importCount={totalImportCount}
/> />
} }
className="rounded-b-md border-t border-solid border-t-mineshaft-600" className="rounded-b-md border-t border-solid border-t-mineshaft-600"

View File

@ -0,0 +1,85 @@
import { faCheck, faFileImport, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { Td, Tr } from "@app/components/v2";
import { TSecretImport, WorkspaceEnv } from "@app/hooks/api/types";
import { EnvFolderIcon } from "@app/pages/secret-manager/SecretDashboardPage/components/SecretImportListView/SecretImportItem";
type Props = {
secretImport: { importPath: string; importEnv: WorkspaceEnv };
environments: { name: string; slug: string }[];
allSecretImports?: TSecretImport[];
};
export const SecretOverviewImportListView = ({
secretImport,
environments = [],
allSecretImports = []
}: Props) => {
const isSecretPresentInEnv = (envSlug: string) => {
return allSecretImports.some((item) => {
if (item.isReplication) {
if (
item.importPath === secretImport.importPath &&
item.importEnv.slug === secretImport.importEnv.slug
) {
const reservedItem = allSecretImports.find((element) =>
element.importPath.includes(`__reserve_replication_${item.id}`)
);
// If the reserved item exists, check if the envSlug matches
if (reservedItem) {
return reservedItem.environment === envSlug;
}
}
} else {
// If the item is not replication, check if the envSlug matches directly
return (
item.environment === envSlug &&
item.importPath === secretImport.importPath &&
item.importEnv.slug === secretImport.importEnv.slug
);
}
return false;
});
};
return (
<Tr className="group">
<Td className="sticky left-0 z-10 border-r border-mineshaft-600 bg-mineshaft-800 bg-clip-padding px-0 py-0 group-hover:bg-mineshaft-700">
<div className="group flex cursor-pointer">
<div className="flex w-11 items-center py-2 pl-5 text-green-700">
<FontAwesomeIcon icon={faFileImport} />
</div>
<div className="flex flex-grow items-center py-2 pl-4 pr-2">
<EnvFolderIcon
env={secretImport.importEnv.slug || ""}
secretPath={secretImport.importPath || ""}
/>
</div>
</div>
</Td>
{environments.map(({ slug }, i) => {
const isPresent = isSecretPresentInEnv(slug);
return (
<Td
key={`sec-overview-${slug}-${i + 1}-value`}
className={twMerge(
"px-0 py-0 group-hover:bg-mineshaft-700",
isPresent ? "text-green-600" : "text-red-600"
)}
>
<div className="h-full w-full border-r border-mineshaft-600 px-5 py-[0.85rem]">
<div className="flex justify-center">
<FontAwesomeIcon
// eslint-disable-next-line no-nested-ternary
icon={isSecretPresentInEnv(slug) ? faCheck : faXmark}
/>
</div>
</div>
</Td>
);
})}
</Tr>
);
};

View File

@ -0,0 +1 @@
export { SecretOverviewImportListView } from "./SecretOverviewImportListView";

View File

@ -35,9 +35,6 @@ export const SecretSyncAuditLogsSection = ({ secretSync }: Props) => {
{subscription.auditLogs ? ( {subscription.auditLogs ? (
<LogsSection <LogsSection
refetchInterval={4000} refetchInterval={4000}
remappedHeaders={{
Metadata: "Sync Status"
}}
showFilters={false} showFilters={false}
presets={{ presets={{
eventMetadata: { syncId: secretSync.id }, eventMetadata: { syncId: secretSync.id },

View File

@ -3,31 +3,75 @@ import { useNavigate } from "@tanstack/react-router";
import { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2"; import { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2";
import { useWorkspace } from "@app/context"; import { useWorkspace } from "@app/context";
import { isInfisicalCloud } from "@app/helpers/platform";
import { useSaveIntegrationAccessToken } from "@app/hooks/api"; import { useSaveIntegrationAccessToken } from "@app/hooks/api";
export const WindmillAuthorizePage = () => { export const WindmillAuthorizePage = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { mutateAsync } = useSaveIntegrationAccessToken(); const { mutateAsync } = useSaveIntegrationAccessToken();
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const [apiKey, setApiKey] = useState(""); const [apiKey, setApiKey] = useState("");
const [apiKeyErrorText, setApiKeyErrorText] = useState(""); const [apiKeyErrorText, setApiKeyErrorText] = useState("");
const [apiUrl, setApiUrl] = useState<string | null>(null);
const [apiUrlErrorText, setApiUrlErrorText] = useState("");
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const isLocalOrPrivateIpAddress = (url: string): boolean => {
try {
const validUrl = new URL(url);
// Check for localhost
if (validUrl.hostname === "localhost" || validUrl.hostname === "127.0.0.1") {
return true;
}
// Check for 10.x.x.x
if (validUrl.hostname.match(/^10\.\d+\.\d+\.\d+/)) {
return true;
}
// Check for host.docker.internal
if (validUrl.hostname === "host.docker.internal") {
return true;
}
// Check for 192.168.x.x
if (validUrl.hostname.match(/^192\.168\.\d+\.\d+/)) {
return true;
}
return false;
} catch (err) {
console.error(err);
return true;
}
};
const handleButtonClick = async () => { const handleButtonClick = async () => {
try { try {
setApiKeyErrorText(""); setApiKeyErrorText("");
setApiUrlErrorText("");
if (apiKey.length === 0) { if (apiKey.length === 0) {
setApiKeyErrorText("API Key cannot be blank"); setApiKeyErrorText("API Key cannot be blank");
return; return;
} }
if (apiUrl) {
if (!apiUrl.startsWith("http://") && !apiUrl.startsWith("https://")) {
setApiUrlErrorText("API URL must start with http:// or https://");
return;
}
if (isInfisicalCloud() && isLocalOrPrivateIpAddress(apiUrl)) {
setApiUrlErrorText("Local IPs not allowed as URL");
return;
}
}
setIsLoading(true); setIsLoading(true);
const integrationAuth = await mutateAsync({ const integrationAuth = await mutateAsync({
workspaceId: currentWorkspace.id, workspaceId: currentWorkspace.id,
integration: "windmill", integration: "windmill",
accessToken: apiKey accessToken: apiKey,
url: apiUrl ?? undefined
}); });
setIsLoading(false); setIsLoading(false);
@ -57,6 +101,18 @@ export const WindmillAuthorizePage = () => {
> >
<Input placeholder="" value={apiKey} onChange={(e) => setApiKey(e.target.value)} /> <Input placeholder="" value={apiKey} onChange={(e) => setApiKey(e.target.value)} />
</FormControl> </FormControl>
<FormControl
label="Windmill Instance URL"
errorText={apiUrlErrorText}
isError={apiUrlErrorText !== ""}
tooltipText="If you are using a custom domain, enter it here. Otherwise, leave it blank."
>
<Input
value={apiUrl ?? ""}
onChange={(e) => setApiUrl(e.target.value.trim() === "" ? null : e.target.value.trim())}
placeholder="https://xxxx.windmill.dev"
/>
</FormControl>
<Button <Button
onClick={handleButtonClick} onClick={handleButtonClick}
color="mineshaft" color="mineshaft"

View File

@ -67,7 +67,8 @@ export const WindmillConfigurePage = () => {
(integrationAuthApp) => integrationAuthApp.name === targetApp (integrationAuthApp) => integrationAuthApp.name === targetApp
)?.appId, )?.appId,
sourceEnvironment: selectedSourceEnvironment, sourceEnvironment: selectedSourceEnvironment,
secretPath secretPath,
url: integrationAuth.url ?? undefined
}); });
setIsLoading(false); setIsLoading(false);