mirror of
https://github.com/Infisical/infisical.git
synced 2025-03-29 22:02:57 +00:00
Compare commits
57 Commits
daniel/kub
...
fix/keepDo
Author | SHA1 | Date | |
---|---|---|---|
d4728e31c1 | |||
f9a5b46365 | |||
d65deab0af | |||
61591742e4 | |||
54b13a9daa | |||
4adf0aa1e2 | |||
3d3ee746cf | |||
07e4358d00 | |||
962dd5d919 | |||
52bd1afb0a | |||
d918dd8967 | |||
e2e0f6a346 | |||
326cb99732 | |||
341b63c61c | |||
81b026865c | |||
f50c72c033 | |||
e1046e2d56 | |||
ed3fa8add1 | |||
d123283849 | |||
d7fd44b845 | |||
3ffee049ee | |||
524462d7bc | |||
351e573fea | |||
f1bc26e2e5 | |||
8aeb607f6e | |||
e530b7a788 | |||
bf61090b5a | |||
106b068a51 | |||
6f0a97a2fa | |||
5d604be091 | |||
905cf47d90 | |||
2c40d316f4 | |||
32521523c1 | |||
3a2e8939b1 | |||
e5947fcab9 | |||
a6d9c74054 | |||
f7cf2bb78f | |||
6ac802b6c9 | |||
ff92e00503 | |||
b20474c505 | |||
e19ffc91c6 | |||
61eb66efca | |||
82520a7f0a | |||
af236ba892 | |||
c4b7d4618d | |||
003f2b003d | |||
747b5ec68d | |||
ed0dc324a3 | |||
1c13ed54af | |||
8abfea0409 | |||
ce4adccc80 | |||
dcd3b5df56 | |||
f6425480ca | |||
a3e9392a2f | |||
633a2ae985 | |||
e67a8f9c05 | |||
ad110f490c |
backend/src
@types
db
migrations
schemas
ee/services
audit-log
license
permission
lib
queue
server
plugins
routes
services
identity-kubernetes-auth
identity-ua
integration-auth
secret-folder
secret-import
cli/packages
docs
frontend
package-lock.json
src
hooks/api
main.tsxpages
auth
organization
AccessManagementPage/components/OrgIdentityTab/components/IdentitySection
AuditLogsPage
IdentityDetailsByIDPage/components/ViewIdentityAuthModal
UserDetailsByIDPage/components/UserProjectsSection
secret-manager
IntegrationsDetailsByIDPage/components
OverviewPage
SecretSyncDetailsByIDPage/components
integrations
1
backend/src/@types/fastify.d.ts
vendored
1
backend/src/@types/fastify.d.ts
vendored
@ -106,6 +106,7 @@ declare module "@fastify/request-context" {
|
||||
claims: Record<string, string>;
|
||||
};
|
||||
};
|
||||
identityPermissionMetadata?: Record<string, unknown>; // filled by permission service
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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"]);
|
||||
});
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -28,7 +28,7 @@ export const IdentityKubernetesAuthsSchema = z.object({
|
||||
allowedNamespaces: z.string(),
|
||||
allowedNames: z.string(),
|
||||
allowedAudience: z.string(),
|
||||
encryptedKubernetesTokenReviewerJwt: zodBuffer,
|
||||
encryptedKubernetesTokenReviewerJwt: zodBuffer.nullable().optional(),
|
||||
encryptedKubernetesCaCertificate: zodBuffer.nullable().optional()
|
||||
});
|
||||
|
||||
|
@ -1,8 +1,10 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { requestContext } from "@fastify/request-context";
|
||||
|
||||
import { ActionProjectType } from "@app/db/schemas";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
@ -81,8 +83,12 @@ export const auditLogServiceFactory = ({
|
||||
if (!data.projectId && !data.orgId)
|
||||
throw new BadRequestError({ message: "Must specify either project id or org id" });
|
||||
}
|
||||
|
||||
return auditLogQueue.pushToLog(data);
|
||||
const el = { ...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 {
|
||||
|
@ -290,6 +290,7 @@ interface UserActorMetadata {
|
||||
userId: string;
|
||||
email?: string | null;
|
||||
username: string;
|
||||
permission?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface ServiceActorMetadata {
|
||||
@ -300,6 +301,7 @@ interface ServiceActorMetadata {
|
||||
interface IdentityActorMetadata {
|
||||
identityId: string;
|
||||
name: string;
|
||||
permission?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface ScimClientActorMetadata {}
|
||||
|
@ -50,7 +50,7 @@ export type TLicenseServiceFactory = ReturnType<typeof licenseServiceFactory>;
|
||||
const LICENSE_SERVER_CLOUD_LOGIN = "/api/auth/v1/license-server-login";
|
||||
const LICENSE_SERVER_ON_PREM_LOGIN = "/api/auth/v1/license-login";
|
||||
|
||||
const LICENSE_SERVER_CLOUD_PLAN_TTL = 30; // 30 second
|
||||
const LICENSE_SERVER_CLOUD_PLAN_TTL = 5 * 60; // 5 mins
|
||||
const FEATURE_CACHE_KEY = (orgId: string) => `infisical-cloud-plan-${orgId}`;
|
||||
|
||||
export const licenseServiceFactory = ({
|
||||
@ -142,7 +142,10 @@ export const licenseServiceFactory = ({
|
||||
try {
|
||||
if (instanceType === InstanceType.Cloud) {
|
||||
const cachedPlan = await keyStore.getItem(FEATURE_CACHE_KEY(orgId));
|
||||
if (cachedPlan) return JSON.parse(cachedPlan) as TFeatureSet;
|
||||
if (cachedPlan) {
|
||||
logger.info(`getPlan: plan fetched from cache [orgId=${orgId}] [projectId=${projectId}]`);
|
||||
return JSON.parse(cachedPlan) as TFeatureSet;
|
||||
}
|
||||
|
||||
const org = await orgDAL.findOrgById(orgId);
|
||||
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
|
||||
@ -170,6 +173,8 @@ export const licenseServiceFactory = ({
|
||||
JSON.stringify(onPremFeatures)
|
||||
);
|
||||
return onPremFeatures;
|
||||
} finally {
|
||||
logger.info(`getPlan: Process done for [orgId=${orgId}] [projectId=${projectId}]`);
|
||||
}
|
||||
return onPremFeatures;
|
||||
};
|
||||
|
@ -244,22 +244,20 @@ export const permissionServiceFactory = ({
|
||||
|
||||
const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges));
|
||||
const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false });
|
||||
const metadataKeyValuePair = escapeHandlebarsMissingDict(
|
||||
objectify(
|
||||
userProjectPermission.metadata,
|
||||
(i) => i.key,
|
||||
(i) => i.value
|
||||
),
|
||||
"identity.metadata"
|
||||
const unescapedMetadata = objectify(
|
||||
userProjectPermission.metadata,
|
||||
(i) => i.key,
|
||||
(i) => i.value
|
||||
);
|
||||
const templateValue = {
|
||||
id: userProjectPermission.userId,
|
||||
username: userProjectPermission.username,
|
||||
metadata: metadataKeyValuePair
|
||||
};
|
||||
const metadataKeyValuePair = escapeHandlebarsMissingDict(unescapedMetadata, "identity.metadata");
|
||||
requestContext.set("identityPermissionMetadata", { metadata: unescapedMetadata });
|
||||
const interpolateRules = templatedRules(
|
||||
{
|
||||
identity: templateValue
|
||||
identity: {
|
||||
id: userProjectPermission.userId,
|
||||
username: userProjectPermission.username,
|
||||
metadata: metadataKeyValuePair
|
||||
}
|
||||
},
|
||||
{ data: false }
|
||||
);
|
||||
@ -331,15 +329,16 @@ export const permissionServiceFactory = ({
|
||||
? escapeHandlebarsMissingDict(unescapedIdentityAuthInfo as never, "identity.auth")
|
||||
: {};
|
||||
const metadataKeyValuePair = escapeHandlebarsMissingDict(unescapedMetadata, "identity.metadata");
|
||||
const templateValue = {
|
||||
id: identityProjectPermission.identityId,
|
||||
username: identityProjectPermission.username,
|
||||
metadata: metadataKeyValuePair,
|
||||
auth: identityAuthInfo
|
||||
};
|
||||
|
||||
requestContext.set("identityPermissionMetadata", { metadata: unescapedMetadata, auth: unescapedIdentityAuthInfo });
|
||||
const interpolateRules = templatedRules(
|
||||
{
|
||||
identity: templateValue
|
||||
identity: {
|
||||
id: identityProjectPermission.identityId,
|
||||
username: identityProjectPermission.username,
|
||||
metadata: metadataKeyValuePair,
|
||||
auth: identityAuthInfo
|
||||
}
|
||||
},
|
||||
{ data: false }
|
||||
);
|
||||
@ -440,14 +439,13 @@ export const permissionServiceFactory = ({
|
||||
),
|
||||
"identity.metadata"
|
||||
);
|
||||
const templateValue = {
|
||||
id: userProjectPermission.userId,
|
||||
username: userProjectPermission.username,
|
||||
metadata: metadataKeyValuePair
|
||||
};
|
||||
const interpolateRules = templatedRules(
|
||||
{
|
||||
identity: templateValue
|
||||
identity: {
|
||||
id: userProjectPermission.userId,
|
||||
username: userProjectPermission.username,
|
||||
metadata: metadataKeyValuePair
|
||||
}
|
||||
},
|
||||
{ data: false }
|
||||
);
|
||||
@ -487,14 +485,13 @@ export const permissionServiceFactory = ({
|
||||
),
|
||||
"identity.metadata"
|
||||
);
|
||||
const templateValue = {
|
||||
id: identityProjectPermission.identityId,
|
||||
username: identityProjectPermission.username,
|
||||
metadata: metadataKeyValuePair
|
||||
};
|
||||
const interpolateRules = templatedRules(
|
||||
{
|
||||
identity: templateValue
|
||||
identity: {
|
||||
id: identityProjectPermission.identityId,
|
||||
username: identityProjectPermission.username,
|
||||
metadata: metadataKeyValuePair
|
||||
}
|
||||
},
|
||||
{ data: false }
|
||||
);
|
||||
|
@ -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.",
|
||||
caCert: "The PEM-encoded CA cert for the Kubernetes API server.",
|
||||
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:
|
||||
"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.",
|
||||
@ -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.",
|
||||
caCert: "The new PEM-encoded CA cert for the Kubernetes API server.",
|
||||
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:
|
||||
"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.",
|
||||
@ -631,7 +631,8 @@ export const FOLDERS = {
|
||||
workspaceId: "The ID of the project to list folders from.",
|
||||
environment: "The slug of the environment 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: {
|
||||
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.",
|
||||
includeSecrets: "Whether to include project secrets 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: {
|
||||
projectId: "The ID of the project to list secrets/folders from.",
|
||||
|
@ -56,6 +56,7 @@ const envSchema = z
|
||||
// TODO(akhilmhdh): will be changed to one
|
||||
ENCRYPTION_KEY: zpStr(z.string().optional()),
|
||||
ROOT_ENCRYPTION_KEY: zpStr(z.string().optional()),
|
||||
QUEUE_WORKERS_ENABLED: zodStrBool.default("true"),
|
||||
HTTPS_ENABLED: zodStrBool,
|
||||
// smtp options
|
||||
SMTP_HOST: zpStr(z.string().optional()),
|
||||
|
@ -93,6 +93,7 @@ export const pingGatewayAndVerify = async ({
|
||||
let lastError: Error | null = null;
|
||||
const quicClient = await createQuicConnection(relayHost, relayPort, tlsOptions, identityId, orgId).catch((err) => {
|
||||
throw new BadRequestError({
|
||||
message: (err as Error)?.message,
|
||||
error: err as Error
|
||||
});
|
||||
});
|
||||
|
@ -272,10 +272,13 @@ export const queueServiceFactory = (
|
||||
connection
|
||||
});
|
||||
|
||||
workerContainer[name] = new Worker<TQueueJobTypes[T]["payload"], void, TQueueJobTypes[T]["name"]>(name, jobFn, {
|
||||
...queueSettings,
|
||||
connection
|
||||
});
|
||||
const appCfg = getConfig();
|
||||
if (appCfg.QUEUE_WORKERS_ENABLED) {
|
||||
workerContainer[name] = new Worker<TQueueJobTypes[T]["payload"], void, TQueueJobTypes[T]["name"]>(name, jobFn, {
|
||||
...queueSettings,
|
||||
connection
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const startPg = async <T extends QueueName>(
|
||||
@ -307,6 +310,11 @@ export const queueServiceFactory = (
|
||||
event: U,
|
||||
listener: WorkerListener<TQueueJobTypes[T]["payload"], void, TQueueJobTypes[T]["name"]>[U]
|
||||
) => {
|
||||
const appCfg = getConfig();
|
||||
if (!appCfg.QUEUE_WORKERS_ENABLED) {
|
||||
return;
|
||||
}
|
||||
|
||||
const worker = workerContainer[name];
|
||||
worker.on(event, listener);
|
||||
};
|
||||
|
@ -65,7 +65,7 @@ export const registerSecretScannerGhApp = async (server: FastifyZodProvider) =>
|
||||
payload: JSON.stringify(req.body),
|
||||
signature: signatureSHA256
|
||||
});
|
||||
void res.send("ok");
|
||||
return res.send("ok");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -34,7 +34,7 @@ export const registerServeUI = async (
|
||||
TELEMETRY_CAPTURING_ENABLED: appCfg.TELEMETRY_ENABLED
|
||||
};
|
||||
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();
|
||||
return;
|
||||
}
|
||||
void reply.sendFile("index.html");
|
||||
return reply.sendFile("index.html");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { CronJob } from "cron";
|
||||
import { Knex } from "knex";
|
||||
import { monitorEventLoopDelay } from "perf_hooks";
|
||||
import { z } from "zod";
|
||||
|
||||
import { registerCertificateEstRouter } from "@app/ee/routes/est/certificate-est-router";
|
||||
@ -96,6 +97,7 @@ import { trustedIpDALFactory } from "@app/ee/services/trusted-ip/trusted-ip-dal"
|
||||
import { trustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service";
|
||||
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||
import { getConfig, TEnvConfig } from "@app/lib/config/env";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { TQueueServiceFactory } from "@app/queue";
|
||||
import { readLimit } from "@app/server/config/rateLimiter";
|
||||
import { accessTokenQueueServiceFactory } from "@app/services/access-token-queue/access-token-queue";
|
||||
@ -246,6 +248,9 @@ import { registerV1Routes } from "./v1";
|
||||
import { registerV2Routes } from "./v2";
|
||||
import { registerV3Routes } from "./v3";
|
||||
|
||||
const histogram = monitorEventLoopDelay({ resolution: 20 });
|
||||
histogram.enable();
|
||||
|
||||
export const registerRoutes = async (
|
||||
server: FastifyZodProvider,
|
||||
{
|
||||
@ -1630,6 +1635,18 @@ export const registerRoutes = async (
|
||||
const cfg = getConfig();
|
||||
const serverCfg = await getServerCfg();
|
||||
|
||||
const meanLagMs = histogram.mean / 1e6;
|
||||
const maxLagMs = histogram.max / 1e6;
|
||||
const p99LagMs = histogram.percentile(99) / 1e6;
|
||||
|
||||
logger.info(
|
||||
`Event loop stats - Mean: ${meanLagMs.toFixed(2)}ms, Max: ${maxLagMs.toFixed(2)}ms, p99: ${p99LagMs.toFixed(
|
||||
2
|
||||
)}ms`
|
||||
);
|
||||
|
||||
logger.info(`Raw event loop stats: ${JSON.stringify(histogram, null, 2)}`);
|
||||
|
||||
// try {
|
||||
// await db.raw("SELECT NOW()");
|
||||
// } catch (err) {
|
||||
|
@ -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(
|
||||
z.object({
|
||||
environment: z.object({
|
||||
|
@ -16,7 +16,12 @@ import { secretsLimit } from "@app/server/config/rateLimiter";
|
||||
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||
import { getUserAgentType } from "@app/server/plugins/audit-log";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { 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 { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema";
|
||||
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
|
||||
|
||||
// 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) => {
|
||||
if (!search)
|
||||
return {
|
||||
@ -109,6 +100,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
search: z.string().trim().describe(DASHBOARD.SECRET_OVERVIEW_LIST.search).optional(),
|
||||
includeSecrets: booleanSchema.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeSecrets),
|
||||
includeFolders: booleanSchema.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeFolders),
|
||||
includeImports: booleanSchema.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeImports),
|
||||
includeDynamicSecrets: booleanSchema.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeDynamicSecrets)
|
||||
}),
|
||||
response: {
|
||||
@ -124,9 +116,17 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
})
|
||||
.array()
|
||||
.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(),
|
||||
totalDynamicSecretCount: z.number().optional(),
|
||||
totalSecretCount: z.number().optional(),
|
||||
totalImportCount: z.number().optional(),
|
||||
totalCount: z.number()
|
||||
})
|
||||
}
|
||||
@ -143,6 +143,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
orderDirection,
|
||||
includeFolders,
|
||||
includeSecrets,
|
||||
includeImports,
|
||||
includeDynamicSecrets
|
||||
} = req.query;
|
||||
|
||||
@ -159,6 +160,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
let remainingLimit = limit;
|
||||
let adjustedOffset = offset;
|
||||
|
||||
let imports: Awaited<ReturnType<typeof server.services.secretImport.getImportsMultiEnv>> | undefined;
|
||||
let folders: Awaited<ReturnType<typeof server.services.folder.getFoldersMultiEnv>> | undefined;
|
||||
let secrets: Awaited<ReturnType<typeof server.services.secret.getSecretsRawMultiEnv>> | undefined;
|
||||
let dynamicSecrets:
|
||||
@ -168,6 +170,53 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
let totalFolderCount: number | undefined;
|
||||
let totalDynamicSecretCount: 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) {
|
||||
// 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,
|
||||
dynamicSecrets,
|
||||
secrets,
|
||||
imports,
|
||||
totalFolderCount,
|
||||
totalDynamicSecretCount,
|
||||
totalImportCount,
|
||||
totalSecretCount,
|
||||
totalCount: (totalFolderCount ?? 0) + (totalDynamicSecretCount ?? 0) + (totalSecretCount ?? 0)
|
||||
totalCount:
|
||||
(totalFolderCount ?? 0) + (totalDynamicSecretCount ?? 0) + (totalSecretCount ?? 0) + (totalImportCount ?? 0)
|
||||
};
|
||||
}
|
||||
});
|
||||
|
@ -24,7 +24,7 @@ const IdentityKubernetesAuthResponseSchema = IdentityKubernetesAuthsSchema.pick(
|
||||
allowedAudience: true
|
||||
}).extend({
|
||||
caCert: z.string(),
|
||||
tokenReviewerJwt: z.string()
|
||||
tokenReviewerJwt: z.string().optional().nullable()
|
||||
});
|
||||
|
||||
export const registerIdentityKubernetesRouter = async (server: FastifyZodProvider) => {
|
||||
@ -98,7 +98,7 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
|
||||
.object({
|
||||
kubernetesHost: z.string().trim().min(1).describe(KUBERNETES_AUTH.ATTACH.kubernetesHost),
|
||||
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
|
||||
allowedNames: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedNames),
|
||||
allowedAudience: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedAudience),
|
||||
@ -195,7 +195,7 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
|
||||
.object({
|
||||
kubernetesHost: z.string().trim().min(1).optional().describe(KUBERNETES_AUTH.UPDATE.kubernetesHost),
|
||||
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
|
||||
allowedNames: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedNames),
|
||||
allowedAudience: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedAudience),
|
||||
|
@ -9,6 +9,8 @@ import { readLimit, secretsLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
import { booleanSchema } from "../sanitizedSchemas";
|
||||
|
||||
export const registerSecretFolderRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
url: "/",
|
||||
@ -347,11 +349,14 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
||||
.default("/")
|
||||
.transform(prefixWithSlash)
|
||||
.transform(removeTrailingSlash)
|
||||
.describe(FOLDERS.LIST.directory)
|
||||
.describe(FOLDERS.LIST.directory),
|
||||
recursive: booleanSchema.default(false).describe(FOLDERS.LIST.recursive)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
folders: SecretFoldersSchema.array()
|
||||
folders: SecretFoldersSchema.extend({
|
||||
relativePath: z.string().optional()
|
||||
}).array()
|
||||
})
|
||||
}
|
||||
},
|
||||
|
@ -84,6 +84,9 @@ export const identityKubernetesAuthServiceFactory = ({
|
||||
tokenReviewerJwt = decryptor({
|
||||
cipherTextBlob: identityKubernetesAuth.encryptedKubernetesTokenReviewerJwt
|
||||
}).toString();
|
||||
} else {
|
||||
// if no token reviewer is provided means the incoming token has to act as reviewer
|
||||
tokenReviewerJwt = serviceAccountJwt;
|
||||
}
|
||||
|
||||
const { data } = await axios
|
||||
@ -102,7 +105,8 @@ export const identityKubernetesAuthServiceFactory = ({
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${tokenReviewerJwt}`
|
||||
},
|
||||
|
||||
signal: AbortSignal.timeout(10000),
|
||||
timeout: 10000,
|
||||
// if ca cert, rejectUnauthorized: true
|
||||
httpsAgent: new https.Agent({
|
||||
ca: caCert,
|
||||
@ -290,7 +294,9 @@ export const identityKubernetesAuthServiceFactory = ({
|
||||
accessTokenTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
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
|
||||
},
|
||||
tx
|
||||
@ -386,10 +392,12 @@ export const identityKubernetesAuthServiceFactory = ({
|
||||
updateQuery.encryptedKubernetesCaCertificate = encryptor({ plainText: Buffer.from(caCert) }).cipherTextBlob;
|
||||
}
|
||||
|
||||
if (tokenReviewerJwt !== undefined) {
|
||||
if (tokenReviewerJwt) {
|
||||
updateQuery.encryptedKubernetesTokenReviewerJwt = encryptor({
|
||||
plainText: Buffer.from(tokenReviewerJwt)
|
||||
}).cipherTextBlob;
|
||||
} else if (tokenReviewerJwt === null) {
|
||||
updateQuery.encryptedKubernetesTokenReviewerJwt = null;
|
||||
}
|
||||
|
||||
const updatedKubernetesAuth = await identityKubernetesAuthDAL.updateById(identityKubernetesAuth.id, updateQuery);
|
||||
|
@ -9,7 +9,7 @@ export type TAttachKubernetesAuthDTO = {
|
||||
identityId: string;
|
||||
kubernetesHost: string;
|
||||
caCert: string;
|
||||
tokenReviewerJwt: string;
|
||||
tokenReviewerJwt?: string;
|
||||
allowedNamespaces: string;
|
||||
allowedNames: string;
|
||||
allowedAudience: string;
|
||||
@ -24,7 +24,7 @@ export type TUpdateKubernetesAuthDTO = {
|
||||
identityId: string;
|
||||
kubernetesHost?: string;
|
||||
caCert?: string;
|
||||
tokenReviewerJwt?: string;
|
||||
tokenReviewerJwt?: string | null;
|
||||
allowedNamespaces?: string;
|
||||
allowedNames?: string;
|
||||
allowedAudience?: string;
|
||||
|
@ -69,9 +69,15 @@ export const identityUaServiceFactory = ({
|
||||
isClientSecretRevoked: false
|
||||
});
|
||||
|
||||
const validClientSecretInfo = clientSecrtInfo.find(({ clientSecretHash }) =>
|
||||
bcrypt.compareSync(clientSecret, clientSecretHash)
|
||||
);
|
||||
let validClientSecretInfo: (typeof clientSecrtInfo)[0] | null = null;
|
||||
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" });
|
||||
|
||||
const { clientSecretTTL, clientSecretNumUses, clientSecretNumUsesLimit } = validClientSecretInfo;
|
||||
@ -104,7 +110,7 @@ export const identityUaServiceFactory = ({
|
||||
}
|
||||
|
||||
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(
|
||||
{
|
||||
identityId: identityUa.identityId,
|
||||
|
@ -923,16 +923,14 @@ const getAppsCodefresh = async ({ accessToken }: { accessToken: string }) => {
|
||||
/**
|
||||
* Return list of projects for Windmill integration
|
||||
*/
|
||||
const getAppsWindmill = async ({ accessToken }: { accessToken: string }) => {
|
||||
const { data } = await request.get<{ id: string; name: string }[]>(
|
||||
`${IntegrationUrls.WINDMILL_API_URL}/workspaces/list`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
const getAppsWindmill = async ({ accessToken, url }: { accessToken: string; url?: string | null }) => {
|
||||
const apiUrl = url ? `${url}/api` : IntegrationUrls.WINDMILL_API_URL;
|
||||
const { data } = await request.get<{ id: string; name: string }[]>(`${apiUrl}/workspaces/list`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// check for write access of secrets in windmill workspaces
|
||||
const writeAccessCheck = data.map(async (app) => {
|
||||
@ -941,7 +939,7 @@ const getAppsWindmill = async ({ accessToken }: { accessToken: string }) => {
|
||||
const folderPath = "f/folder/variable";
|
||||
|
||||
const { data: writeUser } = await request.post<object>(
|
||||
`${IntegrationUrls.WINDMILL_API_URL}/w/${app.id}/variables/create`,
|
||||
`${apiUrl}/w/${app.id}/variables/create`,
|
||||
{
|
||||
path: userPath,
|
||||
value: "variable",
|
||||
@ -957,7 +955,7 @@ const getAppsWindmill = async ({ accessToken }: { accessToken: string }) => {
|
||||
);
|
||||
|
||||
const { data: writeFolder } = await request.post<object>(
|
||||
`${IntegrationUrls.WINDMILL_API_URL}/w/${app.id}/variables/create`,
|
||||
`${apiUrl}/w/${app.id}/variables/create`,
|
||||
{
|
||||
path: folderPath,
|
||||
value: "variable",
|
||||
@ -974,14 +972,14 @@ const getAppsWindmill = async ({ accessToken }: { accessToken: string }) => {
|
||||
|
||||
// is write access is allowed then delete the created secrets from workspace
|
||||
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: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"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: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json"
|
||||
@ -1316,7 +1314,8 @@ export const getApps = async ({
|
||||
|
||||
case Integrations.WINDMILL:
|
||||
return getAppsWindmill({
|
||||
accessToken
|
||||
accessToken,
|
||||
url
|
||||
});
|
||||
|
||||
case Integrations.DIGITAL_OCEAN_APP_PLATFORM:
|
||||
|
@ -4127,10 +4127,10 @@ const syncSecretsWindmill = async ({
|
||||
is_secret: boolean;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
const apiUrl = integration.url ? `${integration.url}/api` : IntegrationUrls.WINDMILL_API_URL;
|
||||
// get secrets stored in windmill workspace
|
||||
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: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json"
|
||||
@ -4146,7 +4146,6 @@ const syncSecretsWindmill = async ({
|
||||
|
||||
// eslint-disable-next-line
|
||||
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)) {
|
||||
if ((key.startsWith("u/") || key.startsWith("f/")) && pattern.test(key)) {
|
||||
if (!(key in res)) {
|
||||
@ -4154,7 +4153,7 @@ const syncSecretsWindmill = async ({
|
||||
// -> create secret
|
||||
|
||||
await request.post(
|
||||
`${IntegrationUrls.WINDMILL_API_URL}/w/${integration.appId}/variables/create`,
|
||||
`${apiUrl}/w/${integration.appId}/variables/create`,
|
||||
{
|
||||
path: key,
|
||||
value: secrets[key].value,
|
||||
@ -4171,7 +4170,7 @@ const syncSecretsWindmill = async ({
|
||||
} else {
|
||||
// -> update secret
|
||||
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,
|
||||
value: secrets[key].value,
|
||||
@ -4192,16 +4191,13 @@ const syncSecretsWindmill = async ({
|
||||
for await (const key of Object.keys(res)) {
|
||||
if (!(key in secrets)) {
|
||||
// -> delete secret
|
||||
await request.delete(
|
||||
`${IntegrationUrls.WINDMILL_API_URL}/w/${integration.appId}/variables/delete/${res[key].path}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
await request.delete(`${apiUrl}/w/${integration.appId}/variables/delete/${res[key].path}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
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 { groupBy, removeTrailingSlash } from "@app/lib/fn";
|
||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
@ -41,12 +41,12 @@ const sqlFindMultipleFolderByEnvPathQuery = (db: Knex, query: Array<{ envId: str
|
||||
void baseQb
|
||||
.select({
|
||||
depth: 1,
|
||||
// latestFolderVerId: db.raw("NULL::uuid"),
|
||||
path: db.raw("'/'")
|
||||
})
|
||||
.from(TableName.SecretFolder)
|
||||
.where({
|
||||
parentId: null
|
||||
parentId: null,
|
||||
name: "root"
|
||||
})
|
||||
.whereIn(
|
||||
"envId",
|
||||
@ -69,9 +69,7 @@ const sqlFindMultipleFolderByEnvPathQuery = (db: Knex, query: Array<{ envId: str
|
||||
.where((wb) =>
|
||||
formatedQuery.map(({ secretPath }) =>
|
||||
wb.orWhereRaw(
|
||||
`depth = array_position(ARRAY[${secretPath.map(() => "?").join(",")}]::varchar[], ${
|
||||
TableName.SecretFolder
|
||||
}.name,depth)`,
|
||||
`secret_folders.name = (ARRAY[${secretPath.map(() => "?").join(",")}]::varchar[])[depth]`,
|
||||
[...secretPath]
|
||||
)
|
||||
)
|
||||
@ -107,7 +105,6 @@ const sqlFindFolderByPathQuery = (db: Knex, projectId: string, environments: str
|
||||
void baseQb
|
||||
.select({
|
||||
depth: 1,
|
||||
// latestFolderVerId: db.raw("NULL::uuid"),
|
||||
path: db.raw("'/'")
|
||||
})
|
||||
.from(TableName.SecretFolder)
|
||||
@ -117,6 +114,11 @@ const sqlFindFolderByPathQuery = (db: Knex, projectId: string, environments: str
|
||||
parentId: null
|
||||
})
|
||||
.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))
|
||||
.union(
|
||||
(qb) =>
|
||||
@ -128,21 +130,20 @@ const sqlFindFolderByPathQuery = (db: Knex, projectId: string, environments: str
|
||||
depth: db.raw("parent.depth + 1"),
|
||||
path: db.raw(
|
||||
"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))
|
||||
.whereRaw(
|
||||
`depth = array_position(ARRAY[${pathSegments
|
||||
.map(() => "?")
|
||||
.join(",")}]::varchar[], secret_folders.name,depth)`,
|
||||
[...pathSegments]
|
||||
)
|
||||
.whereRaw(`secret_folders.name = (ARRAY[${pathSegments.map(() => "?").join(",")}]::varchar[])[depth]`, [
|
||||
...pathSegments
|
||||
])
|
||||
.from(TableName.SecretFolder)
|
||||
.join("parent", "parent.id", `${TableName.SecretFolder}.parentId`)
|
||||
);
|
||||
})
|
||||
.from<TSecretFolders & { depth: number; path: string }>("parent")
|
||||
.leftJoin<TProjectEnvironments>(TableName.Environment, `${TableName.Environment}.id`, "parent.envId")
|
||||
.select<
|
||||
(TSecretFolders & {
|
||||
depth: number;
|
||||
@ -152,13 +153,7 @@ const sqlFindFolderByPathQuery = (db: Knex, projectId: string, environments: str
|
||||
envName: string;
|
||||
projectId: string;
|
||||
})[]
|
||||
>(
|
||||
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)
|
||||
);
|
||||
>(selectAllTableCols("parent" as TableName.SecretFolder));
|
||||
};
|
||||
|
||||
const sqlFindSecretPathByFolderId = (db: Knex, projectId: string, folderIds: string[]) =>
|
||||
@ -220,19 +215,12 @@ export const secretFolderDALFactory = (db: TDbClient) => {
|
||||
throw new BadRequestError({
|
||||
message: "Invalid secret path. Only alphanumeric characters, dashes, and underscores are allowed."
|
||||
});
|
||||
|
||||
const formatedPath = removeTrailingSlash(path);
|
||||
try {
|
||||
const folder = await sqlFindFolderByPathQuery(
|
||||
tx || db.replicaNode(),
|
||||
projectId,
|
||||
[environment],
|
||||
removeTrailingSlash(path)
|
||||
)
|
||||
.orderBy("depth", "desc")
|
||||
const query = sqlFindFolderByPathQuery(tx || db.replicaNode(), projectId, [environment], formatedPath)
|
||||
.where("path", formatedPath)
|
||||
.first();
|
||||
if (folder && folder.path !== removeTrailingSlash(path)) {
|
||||
return;
|
||||
}
|
||||
const folder = await query;
|
||||
if (!folder) return;
|
||||
const { envId: id, envName: name, envSlug: slug, ...el } = folder;
|
||||
return { ...el, envId: id, environment: { id, name, slug } };
|
||||
@ -250,22 +238,13 @@ export const secretFolderDALFactory = (db: TDbClient) => {
|
||||
});
|
||||
|
||||
try {
|
||||
const pathDepth = removeTrailingSlash(path).split("/").filter(Boolean).length + 1;
|
||||
|
||||
const formatedPath = removeTrailingSlash(path);
|
||||
const folders = await sqlFindFolderByPathQuery(
|
||||
tx || db.replicaNode(),
|
||||
projectId,
|
||||
environments,
|
||||
removeTrailingSlash(path)
|
||||
)
|
||||
.orderBy("depth", "desc")
|
||||
.where("depth", pathDepth);
|
||||
|
||||
const firstFolder = folders[0];
|
||||
|
||||
if (firstFolder && firstFolder.path !== removeTrailingSlash(path)) {
|
||||
return [];
|
||||
}
|
||||
formatedPath
|
||||
).where("path", removeTrailingSlash(path));
|
||||
|
||||
return folders.map((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) => {
|
||||
try {
|
||||
const folders = await sqlFindSecretPathByFolderId(tx || db.replicaNode(), projectId, folderIds);
|
||||
|
||||
// travelling all the way from leaf node to root contains real path
|
||||
const rootFolders = groupBy(
|
||||
folders.filter(({ parentId }) => parentId === null),
|
||||
|
@ -401,7 +401,8 @@ export const secretFolderServiceFactory = ({
|
||||
orderBy,
|
||||
orderDirection,
|
||||
limit,
|
||||
offset
|
||||
offset,
|
||||
recursive
|
||||
}: TGetFolderDTO) => {
|
||||
// folder list is allowed to be read by anyone
|
||||
// permission to check does user has access
|
||||
@ -420,6 +421,17 @@ export const secretFolderServiceFactory = ({
|
||||
const parentFolder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
||||
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(
|
||||
{
|
||||
envId: env.id,
|
||||
|
@ -45,6 +45,7 @@ export type TGetFolderDTO = {
|
||||
orderDirection?: OrderByDirection;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
recursive?: boolean;
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TGetFolderByIdDTO = {
|
||||
|
@ -469,6 +469,58 @@ export const secretImportServiceFactory = ({
|
||||
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 ({
|
||||
path: secretPath,
|
||||
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 {
|
||||
createImport,
|
||||
updateImport,
|
||||
@ -698,6 +803,8 @@ export const secretImportServiceFactory = ({
|
||||
getRawSecretsFromImports,
|
||||
resyncSecretImportReplication,
|
||||
getProjectImportCount,
|
||||
fnSecretsFromImports
|
||||
fnSecretsFromImports,
|
||||
getProjectImportMultiEnvCount,
|
||||
getImportsMultiEnv
|
||||
};
|
||||
};
|
||||
|
@ -18,10 +18,10 @@ import (
|
||||
)
|
||||
|
||||
var gatewayCmd = &cobra.Command{
|
||||
Use: "gateway",
|
||||
Short: "Run the Infisical gateway or manage its systemd service",
|
||||
Long: "Run the Infisical gateway in the foreground or manage its systemd service installation. Use 'gateway install' to set up the systemd service.",
|
||||
Example: `infisical gateway --token=<token>
|
||||
Use: "gateway",
|
||||
Short: "Run the Infisical gateway or manage its systemd service",
|
||||
Long: "Run the Infisical gateway in the foreground or manage its systemd service installation. Use 'gateway install' to set up the systemd service.",
|
||||
Example: `infisical gateway --token=<token>
|
||||
sudo infisical gateway install --token=<token> --domain=<domain>`,
|
||||
DisableFlagsInUseLine: true,
|
||||
Args: cobra.NoArgs,
|
||||
@ -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{
|
||||
Example: `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")
|
||||
|
||||
gatewayCmd.AddCommand(gatewayInstallCmd)
|
||||
gatewayCmd.AddCommand(gatewayUninstallCmd)
|
||||
gatewayCmd.AddCommand(gatewayRelayCmd)
|
||||
rootCmd.AddCommand(gatewayCmd)
|
||||
}
|
||||
|
@ -89,7 +89,7 @@ func (g *Gateway) ConnectWithRelay() error {
|
||||
turnClientCfg.Conn = turn.NewSTUNConn(conn)
|
||||
} else {
|
||||
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 {
|
||||
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:
|
||||
log.Debug().Msg("Performing relay connection health check")
|
||||
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++
|
||||
log.Warn().Err(err).Int("failures", failures).Msg("Failed to refresh TURN permissions")
|
||||
if failures >= maxFailures {
|
||||
@ -351,6 +353,7 @@ func (g *Gateway) registerRelayIsActive(ctx context.Context, errCh chan error) e
|
||||
}
|
||||
continue
|
||||
}
|
||||
failures = 0 // reset
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
@ -15,7 +15,8 @@ Description=Infisical Gateway Service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
Type=notify
|
||||
NotifyAccess=all
|
||||
EnvironmentFile=/etc/infisical/gateway.conf
|
||||
ExecStart=infisical gateway
|
||||
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)
|
||||
if domain != "" {
|
||||
configContent += fmt.Sprintf("INFISICAL_API_URL=%s\n", domain)
|
||||
} else {
|
||||
configContent += "INFISICAL_API_URL=\n"
|
||||
}
|
||||
|
||||
configPath := filepath.Join(configDir, "gateway.conf")
|
||||
@ -60,11 +59,6 @@ func InstallGatewaySystemdService(token string, domain string) error {
|
||||
}
|
||||
|
||||
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 {
|
||||
return fmt.Errorf("failed to write systemd service file: %v", err)
|
||||
}
|
||||
@ -80,3 +74,48 @@ func InstallGatewaySystemdService(token string, domain string) error {
|
||||
|
||||
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
|
||||
}
|
||||
|
@ -56,6 +56,7 @@ func WriteInitalConfig(userCredentials *models.UserCredentials) error {
|
||||
LoggedInUsers: existingConfigFile.LoggedInUsers,
|
||||
VaultBackendType: existingConfigFile.VaultBackendType,
|
||||
VaultBackendPassphrase: existingConfigFile.VaultBackendPassphrase,
|
||||
Domains: existingConfigFile.Domains,
|
||||
}
|
||||
|
||||
configFileMarshalled, err := json.Marshal(configFile)
|
||||
|
@ -245,8 +245,9 @@ func getCurrentBranch() (string, error) {
|
||||
}
|
||||
|
||||
func AppendAPIEndpoint(address string) string {
|
||||
// if it's empty return as it is
|
||||
// Ensure the address does not already end with "/api"
|
||||
if strings.HasSuffix(address, "/api") {
|
||||
if address == "" || strings.HasSuffix(address, "/api") {
|
||||
return address
|
||||
}
|
||||
|
||||
|
@ -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**,
|
||||
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.
|
||||
|
||||
</Info>
|
||||
|
||||
Infisical provides audit logs for security and compliance teams to monitor information access.
|
||||
With the Audit Log functionality, teams can:
|
||||
|
||||
- **Track** 40+ different events;
|
||||
- **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.
|
||||
|
||||

|
||||
|
||||
## Audit Log Structure
|
||||
|
||||
Each log contains the following data:
|
||||
|
||||
- **Event**: The underlying action such as create, list, read, update, or delete secret(s).
|
||||
- **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.
|
||||
- **Source** (User agent + IP): The software (user agent) and network address (IP) from which the event was initiated.
|
||||
- **Metadata**: Additional data to provide context for each event. For example, this could be the path at which a secret was fetched from etc.
|
||||
| Field | Type | Description | Purpose |
|
||||
| ------------------------- | -------- | --------------------------------------------------------- | ------------------------------------------------------------- |
|
||||
| **event** | Object | Contains details about the action performed | Captures what happened |
|
||||
| event.type | String | The specific action that occurred (e.g., "create-secret") | Identifies the exact operation |
|
||||
| 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>
|
||||
|
@ -37,7 +37,8 @@ then Infisical returns a short-lived access token that can be used to make authe
|
||||
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.
|
||||
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**.
|
||||
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>
|
||||
<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.
|
||||
|
||||
```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:
|
||||
name: infisical-auth
|
||||
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.
|
||||
|
||||
</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">
|
||||
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:
|
||||
|
||||
- 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 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.
|
||||
@ -176,18 +216,19 @@ In the following steps, we explore how to create and use identities for your app
|
||||
</Step>
|
||||
<Step title="Accessing the Infisical API with the identity">
|
||||
To access the Infisical API as the identity, you should first make sure that the pod running your application is bound to a service account specified in the **Allowed Service Account Names** field of the identity's Kubernetes Auth authentication method configuration in step 2.
|
||||
|
||||
|
||||
Once bound, the pod will receive automatically mounted service account credentials that is a JWT token at the `/var/run/secrets/kubernetes.io/serviceaccount/token` path. This token should be used to authenticate with Infisical at the `/api/v1/auth/kubernetes-auth/login` endpoint.
|
||||
|
||||
|
||||
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).
|
||||
|
||||
<Accordion
|
||||
title="Sample code for inside an application"
|
||||
>
|
||||
>
|
||||
The shown example uses Node.js but you can use any other language to retrieve the service account JWT token and use it to authenticate with Infisical.
|
||||
|
||||
```javascript
|
||||
|
||||
```javascript
|
||||
const fs = require("fs");
|
||||
try {
|
||||
const tokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token";
|
||||
@ -237,15 +278,16 @@ In the following steps, we explore how to create and use identities for your app
|
||||
</Accordion>
|
||||
<Accordion title="Why is the Infisical API rejecting my access token?">
|
||||
There are a few reasons for why this might happen:
|
||||
|
||||
- The access token has expired.
|
||||
- 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 access token has expired.
|
||||
- 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.
|
||||
|
||||
</Accordion>
|
||||
<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.
|
||||
|
||||
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 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.
|
||||
|
||||
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.
|
||||
|
Binary file not shown.
Before ![]() (image error) Size: 1.6 MiB After ![]() (image error) Size: 430 KiB ![]() ![]() |
@ -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:
|
||||
|
||||
- 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
|
||||
|
||||
|
5
docs/integrations/platforms/apache-airflow.mdx
Normal file
5
docs/integrations/platforms/apache-airflow.mdx
Normal 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"
|
||||
---
|
@ -402,7 +402,8 @@
|
||||
]
|
||||
},
|
||||
"integrations/frameworks/terraform",
|
||||
"integrations/platforms/ansible"
|
||||
"integrations/platforms/ansible",
|
||||
"integrations/platforms/apache-airflow"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
351
frontend/package-lock.json
generated
351
frontend/package-lock.json
generated
@ -362,26 +362,26 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helpers": {
|
||||
"version": "7.26.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz",
|
||||
"integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==",
|
||||
"version": "7.26.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.10.tgz",
|
||||
"integrity": "sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/template": "^7.25.9",
|
||||
"@babel/types": "^7.26.0"
|
||||
"@babel/template": "^7.26.9",
|
||||
"@babel/types": "^7.26.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.26.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.3.tgz",
|
||||
"integrity": "sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==",
|
||||
"version": "7.26.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.10.tgz",
|
||||
"integrity": "sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.26.3"
|
||||
"@babel/types": "^7.26.10"
|
||||
},
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
@ -423,9 +423,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.26.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz",
|
||||
"integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==",
|
||||
"version": "7.26.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz",
|
||||
"integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"regenerator-runtime": "^0.14.0"
|
||||
@ -435,14 +435,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/template": {
|
||||
"version": "7.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz",
|
||||
"integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==",
|
||||
"version": "7.26.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz",
|
||||
"integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.25.9",
|
||||
"@babel/parser": "^7.25.9",
|
||||
"@babel/types": "^7.25.9"
|
||||
"@babel/code-frame": "^7.26.2",
|
||||
"@babel/parser": "^7.26.9",
|
||||
"@babel/types": "^7.26.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@ -476,9 +476,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.26.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz",
|
||||
"integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==",
|
||||
"version": "7.26.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.10.tgz",
|
||||
"integrity": "sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.25.9",
|
||||
@ -1010,6 +1010,23 @@
|
||||
"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": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
|
||||
@ -1028,9 +1045,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz",
|
||||
"integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==",
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.1.tgz",
|
||||
"integrity": "sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -1647,12 +1664,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/endpoint": {
|
||||
"version": "10.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz",
|
||||
"integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==",
|
||||
"version": "10.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.3.tgz",
|
||||
"integrity": "sha512-nBRBMpKPhQUxCsQQeW+rCJ/OPSMcj3g0nfHn01zGYZXuNDvvXudF/TYY6APj5THlurerpFN4a/dQAIAaM6BYhA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/types": "^13.0.0",
|
||||
"@octokit/types": "^13.6.2",
|
||||
"universal-user-agent": "^7.0.2"
|
||||
},
|
||||
"engines": {
|
||||
@ -1674,18 +1691,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/openapi-types": {
|
||||
"version": "22.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz",
|
||||
"integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==",
|
||||
"version": "24.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz",
|
||||
"integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@octokit/plugin-paginate-rest": {
|
||||
"version": "11.3.6",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.3.6.tgz",
|
||||
"integrity": "sha512-zcvqqf/+TicbTCa/Z+3w4eBJcAxCFymtc0UAIsR3dEVoNilWld4oXdscQ3laXamTszUZdusw97K8+DrbFiOwjw==",
|
||||
"version": "11.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.6.0.tgz",
|
||||
"integrity": "sha512-n5KPteiF7pWKgBIBJSk8qzoZWcUkza2O6A0za97pMGVrGfPdltxrfmfF5GucHYvHGZD8BdaZmmHGz5cX/3gdpw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/types": "^13.6.2"
|
||||
"@octokit/types": "^13.10.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
@ -1722,14 +1739,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/request": {
|
||||
"version": "9.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.1.3.tgz",
|
||||
"integrity": "sha512-V+TFhu5fdF3K58rs1pGUJIDH5RZLbZm5BI+MNF+6o/ssFNT4vWlCh/tVpF3NxGtP15HUxTTMUbsG5llAuU2CZA==",
|
||||
"version": "9.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.2.2.tgz",
|
||||
"integrity": "sha512-dZl0ZHx6gOQGcffgm1/Sf6JfEpmh34v3Af2Uci02vzUYz6qEN6zepoRtmybWXIGXFIK8K9ylE3b+duCWqhArtg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/endpoint": "^10.0.0",
|
||||
"@octokit/request-error": "^6.0.1",
|
||||
"@octokit/types": "^13.1.0",
|
||||
"@octokit/endpoint": "^10.1.3",
|
||||
"@octokit/request-error": "^6.1.7",
|
||||
"@octokit/types": "^13.6.2",
|
||||
"fast-content-type-parse": "^2.0.0",
|
||||
"universal-user-agent": "^7.0.2"
|
||||
},
|
||||
"engines": {
|
||||
@ -1737,12 +1755,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/request-error": {
|
||||
"version": "6.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.5.tgz",
|
||||
"integrity": "sha512-IlBTfGX8Yn/oFPMwSfvugfncK2EwRLjzbrpifNaMY8o/HTEAFqCA1FZxjD9cWvSKBHgrIhc4CSBIzMxiLsbzFQ==",
|
||||
"version": "6.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.7.tgz",
|
||||
"integrity": "sha512-69NIppAwaauwZv6aOzb+VVLwt+0havz9GT5YplkeJv7fG7a40qpLt/yZKyiDxAhgz0EtgNdNcb96Z0u+Zyuy2g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/types": "^13.0.0"
|
||||
"@octokit/types": "^13.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
@ -1764,12 +1782,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/types": {
|
||||
"version": "13.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.6.2.tgz",
|
||||
"integrity": "sha512-WpbZfZUcZU77DrSW4wbsSgTPfKcp286q3ItaIgvSbBpZJlu6mnYXAkjZz6LVZPXkEvLIM8McanyZejKTYUHipA==",
|
||||
"version": "13.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz",
|
||||
"integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/openapi-types": "^22.2.0"
|
||||
"@octokit/openapi-types": "^24.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@peculiar/asn1-cms": {
|
||||
@ -4956,9 +4974,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.7.9",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz",
|
||||
"integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==",
|
||||
"version": "1.8.3",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz",
|
||||
"integrity": "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
@ -5451,9 +5469,9 @@
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/canvg": {
|
||||
"version": "3.0.10",
|
||||
"resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.10.tgz",
|
||||
"integrity": "sha512-qwR2FRNO9NlzTeKIPIKpnTY6fqwuYSequ8Ru8c0YkYU7U0oW+hLUvWadLvAu1Rl72OMNiFhoLu4f8eUjQ7l/+Q==",
|
||||
"version": "3.0.11",
|
||||
"resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz",
|
||||
"integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@ -7314,6 +7332,22 @@
|
||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||
"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": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
@ -12588,13 +12622,13 @@
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.19.2",
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.2.tgz",
|
||||
"integrity": "sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==",
|
||||
"version": "4.19.3",
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.3.tgz",
|
||||
"integrity": "sha512-4H8vUNGNjQ4V2EOoGw005+c+dGuPSnhpPBPHBtsZdGZBk/iJb4kguGlPWaZTZ3q5nMtFOEsY0nRDlh9PJyd6SQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "~0.23.0",
|
||||
"esbuild": "~0.25.0",
|
||||
"get-tsconfig": "^4.7.5"
|
||||
},
|
||||
"bin": {
|
||||
@ -12608,9 +12642,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz",
|
||||
"integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==",
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.1.tgz",
|
||||
"integrity": "sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@ -12625,9 +12659,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/android-arm": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz",
|
||||
"integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==",
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.1.tgz",
|
||||
"integrity": "sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@ -12642,9 +12676,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz",
|
||||
"integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==",
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.1.tgz",
|
||||
"integrity": "sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -12659,9 +12693,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/android-x64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz",
|
||||
"integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==",
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.1.tgz",
|
||||
"integrity": "sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -12676,9 +12710,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz",
|
||||
"integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==",
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.1.tgz",
|
||||
"integrity": "sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -12693,9 +12727,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz",
|
||||
"integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==",
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.1.tgz",
|
||||
"integrity": "sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -12710,9 +12744,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz",
|
||||
"integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==",
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.1.tgz",
|
||||
"integrity": "sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -12727,9 +12761,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz",
|
||||
"integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==",
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.1.tgz",
|
||||
"integrity": "sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -12744,9 +12778,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz",
|
||||
"integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==",
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.1.tgz",
|
||||
"integrity": "sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@ -12761,9 +12795,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz",
|
||||
"integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==",
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.1.tgz",
|
||||
"integrity": "sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -12778,9 +12812,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz",
|
||||
"integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==",
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.1.tgz",
|
||||
"integrity": "sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@ -12795,9 +12829,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz",
|
||||
"integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==",
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.1.tgz",
|
||||
"integrity": "sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
@ -12812,9 +12846,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz",
|
||||
"integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==",
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.1.tgz",
|
||||
"integrity": "sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
@ -12829,9 +12863,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz",
|
||||
"integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==",
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.1.tgz",
|
||||
"integrity": "sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@ -12846,9 +12880,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz",
|
||||
"integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==",
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.1.tgz",
|
||||
"integrity": "sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@ -12863,9 +12897,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz",
|
||||
"integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==",
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.1.tgz",
|
||||
"integrity": "sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@ -12880,9 +12914,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz",
|
||||
"integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==",
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.1.tgz",
|
||||
"integrity": "sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -12897,9 +12931,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz",
|
||||
"integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==",
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.1.tgz",
|
||||
"integrity": "sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -12914,9 +12948,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz",
|
||||
"integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==",
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.1.tgz",
|
||||
"integrity": "sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -12931,9 +12965,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz",
|
||||
"integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==",
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.1.tgz",
|
||||
"integrity": "sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -12948,9 +12982,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz",
|
||||
"integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==",
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.1.tgz",
|
||||
"integrity": "sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -12965,9 +12999,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz",
|
||||
"integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==",
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.1.tgz",
|
||||
"integrity": "sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@ -12982,9 +13016,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz",
|
||||
"integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==",
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.1.tgz",
|
||||
"integrity": "sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -12999,9 +13033,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/esbuild": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz",
|
||||
"integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==",
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.1.tgz",
|
||||
"integrity": "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
@ -13012,30 +13046,31 @@
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.23.1",
|
||||
"@esbuild/android-arm": "0.23.1",
|
||||
"@esbuild/android-arm64": "0.23.1",
|
||||
"@esbuild/android-x64": "0.23.1",
|
||||
"@esbuild/darwin-arm64": "0.23.1",
|
||||
"@esbuild/darwin-x64": "0.23.1",
|
||||
"@esbuild/freebsd-arm64": "0.23.1",
|
||||
"@esbuild/freebsd-x64": "0.23.1",
|
||||
"@esbuild/linux-arm": "0.23.1",
|
||||
"@esbuild/linux-arm64": "0.23.1",
|
||||
"@esbuild/linux-ia32": "0.23.1",
|
||||
"@esbuild/linux-loong64": "0.23.1",
|
||||
"@esbuild/linux-mips64el": "0.23.1",
|
||||
"@esbuild/linux-ppc64": "0.23.1",
|
||||
"@esbuild/linux-riscv64": "0.23.1",
|
||||
"@esbuild/linux-s390x": "0.23.1",
|
||||
"@esbuild/linux-x64": "0.23.1",
|
||||
"@esbuild/netbsd-x64": "0.23.1",
|
||||
"@esbuild/openbsd-arm64": "0.23.1",
|
||||
"@esbuild/openbsd-x64": "0.23.1",
|
||||
"@esbuild/sunos-x64": "0.23.1",
|
||||
"@esbuild/win32-arm64": "0.23.1",
|
||||
"@esbuild/win32-ia32": "0.23.1",
|
||||
"@esbuild/win32-x64": "0.23.1"
|
||||
"@esbuild/aix-ppc64": "0.25.1",
|
||||
"@esbuild/android-arm": "0.25.1",
|
||||
"@esbuild/android-arm64": "0.25.1",
|
||||
"@esbuild/android-x64": "0.25.1",
|
||||
"@esbuild/darwin-arm64": "0.25.1",
|
||||
"@esbuild/darwin-x64": "0.25.1",
|
||||
"@esbuild/freebsd-arm64": "0.25.1",
|
||||
"@esbuild/freebsd-x64": "0.25.1",
|
||||
"@esbuild/linux-arm": "0.25.1",
|
||||
"@esbuild/linux-arm64": "0.25.1",
|
||||
"@esbuild/linux-ia32": "0.25.1",
|
||||
"@esbuild/linux-loong64": "0.25.1",
|
||||
"@esbuild/linux-mips64el": "0.25.1",
|
||||
"@esbuild/linux-ppc64": "0.25.1",
|
||||
"@esbuild/linux-riscv64": "0.25.1",
|
||||
"@esbuild/linux-s390x": "0.25.1",
|
||||
"@esbuild/linux-x64": "0.25.1",
|
||||
"@esbuild/netbsd-arm64": "0.25.1",
|
||||
"@esbuild/netbsd-x64": "0.25.1",
|
||||
"@esbuild/openbsd-arm64": "0.25.1",
|
||||
"@esbuild/openbsd-x64": "0.25.1",
|
||||
"@esbuild/sunos-x64": "0.25.1",
|
||||
"@esbuild/win32-arm64": "0.25.1",
|
||||
"@esbuild/win32-ia32": "0.25.1",
|
||||
"@esbuild/win32-x64": "0.25.1"
|
||||
}
|
||||
},
|
||||
"node_modules/tsyringe": {
|
||||
@ -13552,9 +13587,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "5.4.11",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz",
|
||||
"integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==",
|
||||
"version": "5.4.14",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.14.tgz",
|
||||
"integrity": "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
@ -143,6 +143,7 @@ export const useGetProjectSecretsOverview = (
|
||||
search = "",
|
||||
includeSecrets,
|
||||
includeFolders,
|
||||
includeImports,
|
||||
includeDynamicSecrets,
|
||||
environments
|
||||
}: TGetDashboardProjectSecretsOverviewDTO,
|
||||
@ -170,6 +171,7 @@ export const useGetProjectSecretsOverview = (
|
||||
projectId,
|
||||
includeSecrets,
|
||||
includeFolders,
|
||||
includeImports,
|
||||
includeDynamicSecrets,
|
||||
environments
|
||||
}),
|
||||
@ -184,6 +186,7 @@ export const useGetProjectSecretsOverview = (
|
||||
projectId,
|
||||
includeSecrets,
|
||||
includeFolders,
|
||||
includeImports,
|
||||
includeDynamicSecrets,
|
||||
environments
|
||||
}),
|
||||
@ -197,12 +200,15 @@ export const useGetProjectSecretsOverview = (
|
||||
? unique(select.dynamicSecrets, (i) => i.name)
|
||||
: [];
|
||||
|
||||
const uniqueSecretImports = select.imports ? unique(select.imports, (i) => i.id) : [];
|
||||
|
||||
return {
|
||||
...select,
|
||||
secrets: secrets ? mergePersonalSecrets(secrets) : undefined,
|
||||
totalUniqueSecretsInPage: uniqueSecrets.length,
|
||||
totalUniqueDynamicSecretsInPage: uniqueDynamicSecrets.length,
|
||||
totalUniqueFoldersInPage: uniqueFolders.length
|
||||
totalUniqueFoldersInPage: uniqueFolders.length,
|
||||
totalUniqueSecretImportsInPage: uniqueSecretImports.length
|
||||
};
|
||||
}, []),
|
||||
placeholderData: (previousData) => previousData
|
||||
|
@ -9,13 +9,16 @@ export type DashboardProjectSecretsOverviewResponse = {
|
||||
folders?: (TSecretFolder & { environment: string })[];
|
||||
dynamicSecrets?: (TDynamicSecret & { environment: string })[];
|
||||
secrets?: SecretV3Raw[];
|
||||
imports?: TSecretImport[];
|
||||
totalSecretCount?: number;
|
||||
totalFolderCount?: number;
|
||||
totalDynamicSecretCount?: number;
|
||||
totalImportCount?: number;
|
||||
totalCount: number;
|
||||
totalUniqueSecretsInPage: number;
|
||||
totalUniqueDynamicSecretsInPage: number;
|
||||
totalUniqueFoldersInPage: number;
|
||||
totalUniqueSecretImportsInPage: number;
|
||||
};
|
||||
|
||||
export type DashboardProjectSecretsDetailsResponse = {
|
||||
@ -63,6 +66,7 @@ export type TGetDashboardProjectSecretsOverviewDTO = {
|
||||
includeSecrets?: boolean;
|
||||
includeFolders?: boolean;
|
||||
includeDynamicSecrets?: boolean;
|
||||
includeImports?: boolean;
|
||||
environments: string[];
|
||||
};
|
||||
|
||||
|
@ -350,7 +350,7 @@ export type AddIdentityKubernetesAuthDTO = {
|
||||
organizationId: string;
|
||||
identityId: string;
|
||||
kubernetesHost: string;
|
||||
tokenReviewerJwt: string;
|
||||
tokenReviewerJwt?: string;
|
||||
allowedNamespaces: string;
|
||||
allowedNames: string;
|
||||
allowedAudience: string;
|
||||
@ -367,7 +367,7 @@ export type UpdateIdentityKubernetesAuthDTO = {
|
||||
organizationId: string;
|
||||
identityId: string;
|
||||
kubernetesHost?: string;
|
||||
tokenReviewerJwt?: string;
|
||||
tokenReviewerJwt?: string | null;
|
||||
allowedNamespaces?: string;
|
||||
allowedNames?: string;
|
||||
allowedAudience?: string;
|
||||
|
@ -14,6 +14,7 @@ export type TSecretImport = {
|
||||
isReplicationSuccess?: boolean;
|
||||
replicationStatus?: string;
|
||||
lastReplicated?: string;
|
||||
environment?: string;
|
||||
};
|
||||
|
||||
export type TGetImportedFoldersByEnvDTO = {
|
||||
|
@ -22,14 +22,37 @@ import "./translation";
|
||||
// have a look at the Quick start guide
|
||||
// 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
|
||||
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({
|
||||
routeTree,
|
||||
context: { serverConfig: null, queryClient },
|
||||
|
@ -18,14 +18,6 @@ export const RequestNewInvitePage = () => {
|
||||
<span className="rounded-md bg-primary-500/40 px-1 text-black">Note:</span> If it still
|
||||
doesn't work, please reach out to us at support@infisical.com
|
||||
</p>
|
||||
<div className="">
|
||||
<img
|
||||
src="/images/invitation-expired.svg"
|
||||
height={500}
|
||||
width={800}
|
||||
alt="invitation expired illustration"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -223,15 +223,10 @@ export const SignupInvitePage = () => {
|
||||
// Step 4 of the sign up process (download the emergency kit pdf)
|
||||
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">
|
||||
<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
|
||||
</p>
|
||||
<img
|
||||
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">
|
||||
<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">
|
||||
<Button
|
||||
onClick={async () => {
|
||||
try {
|
||||
|
@ -31,7 +31,7 @@ import { IdentityFormTab } from "./types";
|
||||
const schema = z
|
||||
.object({
|
||||
kubernetesHost: z.string().min(1),
|
||||
tokenReviewerJwt: z.string().min(1),
|
||||
tokenReviewerJwt: z.string().optional(),
|
||||
allowedNames: z.string(),
|
||||
allowedNamespaces: z.string(),
|
||||
allowedAudience: z.string(),
|
||||
@ -166,7 +166,7 @@ export const IdentityKubernetesAuthForm = ({
|
||||
await updateMutateAsync({
|
||||
organizationId: orgId,
|
||||
kubernetesHost,
|
||||
tokenReviewerJwt,
|
||||
tokenReviewerJwt: tokenReviewerJwt || null,
|
||||
allowedNames,
|
||||
allowedNamespaces,
|
||||
allowedAudience,
|
||||
@ -182,7 +182,7 @@ export const IdentityKubernetesAuthForm = ({
|
||||
organizationId: orgId,
|
||||
identityId,
|
||||
kubernetesHost: kubernetesHost || "",
|
||||
tokenReviewerJwt,
|
||||
tokenReviewerJwt: tokenReviewerJwt || undefined,
|
||||
allowedNames: allowedNames || "",
|
||||
allowedNamespaces: allowedNamespaces || "",
|
||||
allowedAudience: allowedAudience || "",
|
||||
@ -255,11 +255,11 @@ export const IdentityKubernetesAuthForm = ({
|
||||
name="tokenReviewerJwt"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
tooltipClassName="max-w-md"
|
||||
label="Token Reviewer JWT"
|
||||
isError={Boolean(error)}
|
||||
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."
|
||||
isRequired
|
||||
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."
|
||||
>
|
||||
<Input {...field} placeholder="" type="password" />
|
||||
</FormControl>
|
||||
|
@ -18,7 +18,7 @@ export const AuditLogsPage = () => {
|
||||
title="Audit logs"
|
||||
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>
|
||||
|
@ -10,7 +10,7 @@ import { ActorType, EventType, UserAgentType } from "@app/hooks/api/auditLogs/en
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { LogsFilter } from "./LogsFilter";
|
||||
import { LogsTable, TAuditLogTableHeader } from "./LogsTable";
|
||||
import { LogsTable } from "./LogsTable";
|
||||
import { AuditLogFilterFormData, auditLogFilterFormSchema } from "./types";
|
||||
|
||||
type Props = {
|
||||
@ -25,22 +25,11 @@ type Props = {
|
||||
|
||||
showFilters?: boolean;
|
||||
filterClassName?: string;
|
||||
isOrgAuditLogs?: boolean;
|
||||
showActorColumn?: boolean;
|
||||
remappedHeaders?: Partial<Record<TAuditLogTableHeader, string>>;
|
||||
refetchInterval?: number;
|
||||
};
|
||||
|
||||
export const LogsSection = withPermission(
|
||||
({
|
||||
presets,
|
||||
filterClassName,
|
||||
remappedHeaders,
|
||||
isOrgAuditLogs,
|
||||
showActorColumn,
|
||||
refetchInterval,
|
||||
showFilters
|
||||
}: Props) => {
|
||||
({ presets, filterClassName, refetchInterval, showFilters }: Props) => {
|
||||
const { subscription } = useSubscription();
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const);
|
||||
@ -90,9 +79,6 @@ export const LogsSection = withPermission(
|
||||
)}
|
||||
<LogsTable
|
||||
refetchInterval={refetchInterval}
|
||||
remappedHeaders={remappedHeaders}
|
||||
isOrgAuditLogs={isOrgAuditLogs}
|
||||
showActorColumn={!!showActorColumn}
|
||||
filter={{
|
||||
secretPath: debouncedSecretPath || undefined,
|
||||
eventMetadata: presets?.eventMetadata,
|
||||
|
@ -1,5 +1,6 @@
|
||||
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 {
|
||||
Button,
|
||||
@ -11,6 +12,7 @@ import {
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { useGetAuditLogs } from "@app/hooks/api";
|
||||
@ -19,32 +21,13 @@ import { TGetAuditLogsFilter } from "@app/hooks/api/auditLogs/types";
|
||||
import { LogsTableRow } from "./LogsTableRow";
|
||||
|
||||
type Props = {
|
||||
isOrgAuditLogs?: boolean;
|
||||
showActorColumn: boolean;
|
||||
filter?: TGetAuditLogsFilter;
|
||||
remappedHeaders?: Partial<Record<TAuditLogTableHeader, string>>;
|
||||
refetchInterval?: number;
|
||||
};
|
||||
|
||||
const AUDIT_LOG_LIMIT = 15;
|
||||
|
||||
const TABLE_HEADERS = [
|
||||
"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) => {
|
||||
export const LogsTable = ({ filter, refetchInterval }: Props) => {
|
||||
// Determine the project ID for filtering
|
||||
const filterProjectId =
|
||||
// Use the projectId from the filter if it exists
|
||||
@ -69,38 +52,37 @@ export const LogsTable = ({
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
{TABLE_HEADERS.map((header, idx) => {
|
||||
if (
|
||||
(header === "Project" && !isOrgAuditLogs) ||
|
||||
(header === "Actor" && !showActorColumn)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Th key={`table-header-${idx + 1}`}>{remappedHeaders?.[header] || header}</Th>
|
||||
);
|
||||
})}
|
||||
<Th className="w-24" />
|
||||
<Th className="w-64">
|
||||
Timestamp
|
||||
<Tooltip
|
||||
className="normal-case"
|
||||
content="Time displayed in your system's time zone."
|
||||
sideOffset={10}
|
||||
>
|
||||
<FontAwesomeIcon icon={faInfoCircle} className="ml-1" />
|
||||
</Tooltip>
|
||||
</Th>
|
||||
<Th>Event</Th>
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{!isPending &&
|
||||
data?.pages?.map((group, i) => (
|
||||
<Fragment key={`audit-log-fragment-${i + 1}`}>
|
||||
{group.map((auditLog) => (
|
||||
{group.map((auditLog, index) => (
|
||||
<LogsTableRow
|
||||
showActorColumn={showActorColumn}
|
||||
isOrgAuditLogs={isOrgAuditLogs}
|
||||
rowNumber={index + i * AUDIT_LOG_LIMIT + 1}
|
||||
auditLog={auditLog}
|
||||
key={`audit-log-${auditLog.id}`}
|
||||
/>
|
||||
))}
|
||||
</Fragment>
|
||||
))}
|
||||
{isPending && <TableSkeleton innerKey="logs-table" columns={5} key="logs" />}
|
||||
{isPending && <TableSkeleton innerKey="logs-table" columns={3} key="logs-loading" />}
|
||||
{isEmpty && (
|
||||
<Tr>
|
||||
<Td colSpan={5}>
|
||||
<Td colSpan={3}>
|
||||
<EmptyState title="No audit logs on file" icon={faFile} />
|
||||
</Td>
|
||||
</Tr>
|
||||
|
@ -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 { format } from "date-fns";
|
||||
|
||||
import { Td, Tooltip, Tr } from "@app/components/v2";
|
||||
import { eventToNameMap, userAgentTTypeoNameMap } from "@app/hooks/api/auditLogs/constants";
|
||||
import { ActorType, EventType } from "@app/hooks/api/auditLogs/enums";
|
||||
import { Actor, AuditLog } from "@app/hooks/api/auditLogs/types";
|
||||
import { Td, Tr } from "@app/components/v2";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { ActorType } from "@app/hooks/api/auditLogs/enums";
|
||||
import { AuditLog } from "@app/hooks/api/auditLogs/types";
|
||||
|
||||
type Props = {
|
||||
auditLog: AuditLog;
|
||||
isOrgAuditLogs?: boolean;
|
||||
showActorColumn: boolean;
|
||||
rowNumber: number;
|
||||
};
|
||||
|
||||
export const LogsTableRow = ({ auditLog, isOrgAuditLogs, showActorColumn }: Props) => {
|
||||
const renderActor = (actor: Actor) => {
|
||||
if (!actor) {
|
||||
return <Td />;
|
||||
}
|
||||
|
||||
switch (actor.type) {
|
||||
case ActorType.USER:
|
||||
return (
|
||||
<Td>
|
||||
<p>{actor.metadata.email}</p>
|
||||
<p>User</p>
|
||||
</Td>
|
||||
);
|
||||
case ActorType.SERVICE:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`${actor.metadata.name}`}</p>
|
||||
<p>Service token</p>
|
||||
</Td>
|
||||
);
|
||||
case ActorType.IDENTITY:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`${actor.metadata.name}`}</p>
|
||||
<p>Machine Identity</p>
|
||||
</Td>
|
||||
);
|
||||
case ActorType.PLATFORM:
|
||||
return (
|
||||
<Td>
|
||||
<p>Platform</p>
|
||||
</Td>
|
||||
);
|
||||
case ActorType.KMIP_CLIENT:
|
||||
return (
|
||||
<Td>
|
||||
<p>{actor.metadata.name}</p>
|
||||
<p>KMIP Client</p>
|
||||
</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>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
type TagProps = {
|
||||
label: string;
|
||||
value?: string;
|
||||
};
|
||||
const Tag = ({ label, value }: TagProps) => {
|
||||
if (!value) return null;
|
||||
|
||||
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>
|
||||
<div className="flex items-center space-x-1.5">
|
||||
<div className="rounded bg-mineshaft-600 p-0.5 pl-1 font-mono">{label}:</div>
|
||||
<div>{value}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const LogsTableRow = ({ auditLog, rowNumber }: Props) => {
|
||||
const [isOpen, setIsOpen] = useToggle();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tr
|
||||
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 className="align-top">
|
||||
{format(new Date(auditLog.createdAt), "MMM do yyyy, hh:mm a")}
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="flex flex-wrap gap-4 text-sm">
|
||||
<Tag label="event" value={auditLog.event.type} />
|
||||
<Tag label="actor" value={auditLog.actor.type} />
|
||||
{auditLog.actor.type === ActorType.USER && (
|
||||
<Tag label="user_email" value={auditLog.actor.metadata.email} />
|
||||
)}
|
||||
{auditLog.actor.type === ActorType.IDENTITY && (
|
||||
<Tag label="identity_name" value={auditLog.actor.metadata.name} />
|
||||
)}
|
||||
</div>
|
||||
</Td>
|
||||
</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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -70,20 +70,26 @@ export const ViewIdentityKubernetesAuthContent = ({
|
||||
{data.kubernetesHost}
|
||||
</IdentityAuthFieldDisplay>
|
||||
<IdentityAuthFieldDisplay className="col-span-2" label="Token Reviewer JWT">
|
||||
<Tooltip
|
||||
side="right"
|
||||
className="max-w-xl p-2"
|
||||
content={
|
||||
<p className="break-words rounded bg-mineshaft-600 p-2">{data.tokenReviewerJwt}</p>
|
||||
}
|
||||
>
|
||||
<div className="w-min">
|
||||
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300">
|
||||
<FontAwesomeIcon icon={faEye} />
|
||||
<span>Reveal</span>
|
||||
</Badge>
|
||||
</div>
|
||||
</Tooltip>
|
||||
{data.tokenReviewerJwt ? (
|
||||
<Tooltip
|
||||
side="right"
|
||||
className="max-w-xl p-2"
|
||||
content={
|
||||
<p className="break-words rounded bg-mineshaft-600 p-2">
|
||||
{data.tokenReviewerJwt || "Not provided"}
|
||||
</p>
|
||||
}
|
||||
>
|
||||
<div className="w-min">
|
||||
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300">
|
||||
<FontAwesomeIcon icon={faEye} />
|
||||
<span>Reveal</span>
|
||||
</Badge>
|
||||
</div>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<p className="text-base italic leading-4 text-bunker-400">Not set</p>
|
||||
)}
|
||||
</IdentityAuthFieldDisplay>
|
||||
<IdentityAuthFieldDisplay className="col-span-2" label="Allowed Service Account Names">
|
||||
{data.allowedNames
|
||||
|
@ -45,7 +45,6 @@ export const UserAuditLogsSection = withPermission(
|
||||
presets={{
|
||||
actorId: orgMembership.user.id
|
||||
}}
|
||||
isOrgAuditLogs
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
@ -29,9 +29,6 @@ export const IntegrationAuditLogsSection = ({ integration }: Props) => {
|
||||
</div>
|
||||
<LogsSection
|
||||
refetchInterval={4000}
|
||||
remappedHeaders={{
|
||||
Metadata: "Sync Status"
|
||||
}}
|
||||
showFilters={false}
|
||||
presets={{
|
||||
eventMetadata: { integrationId: integration.id },
|
||||
|
@ -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;
|
||||
};
|
||||
|
||||
|
@ -7,6 +7,7 @@ import {
|
||||
faAngleDown,
|
||||
faArrowDown,
|
||||
faArrowUp,
|
||||
faFileImport,
|
||||
faFingerprint,
|
||||
faFolder,
|
||||
faFolderBlank,
|
||||
@ -80,6 +81,7 @@ import { CreateSecretForm } from "./components/CreateSecretForm";
|
||||
import { FolderBreadCrumbs } from "./components/FolderBreadCrumbs";
|
||||
import { SecretOverviewDynamicSecretRow } from "./components/SecretOverviewDynamicSecretRow";
|
||||
import { SecretOverviewFolderRow } from "./components/SecretOverviewFolderRow";
|
||||
import { SecretOverviewImportListView } from "./components/SecretOverviewImportListView";
|
||||
import {
|
||||
SecretNoAccessOverviewTableRow,
|
||||
SecretOverviewTableRow
|
||||
@ -97,7 +99,8 @@ export enum EntryType {
|
||||
enum RowType {
|
||||
Folder = "folder",
|
||||
DynamicSecret = "dynamic",
|
||||
Secret = "secret"
|
||||
Secret = "secret",
|
||||
Import = "import"
|
||||
}
|
||||
|
||||
type Filter = {
|
||||
@ -107,7 +110,8 @@ type Filter = {
|
||||
const DEFAULT_FILTER_STATE = {
|
||||
[RowType.Folder]: true,
|
||||
[RowType.DynamicSecret]: true,
|
||||
[RowType.Secret]: true
|
||||
[RowType.Secret]: true,
|
||||
[RowType.Import]: true
|
||||
};
|
||||
|
||||
export const OverviewPage = () => {
|
||||
@ -216,6 +220,7 @@ export const OverviewPage = () => {
|
||||
includeFolders: filter.folder,
|
||||
includeDynamicSecrets: filter.dynamic,
|
||||
includeSecrets: filter.secret,
|
||||
includeImports: filter.import,
|
||||
search: debouncedSearchFilter,
|
||||
limit,
|
||||
offset
|
||||
@ -227,15 +232,29 @@ export const OverviewPage = () => {
|
||||
secrets,
|
||||
folders,
|
||||
dynamicSecrets,
|
||||
imports,
|
||||
totalFolderCount,
|
||||
totalSecretCount,
|
||||
totalDynamicSecretCount,
|
||||
totalImportCount,
|
||||
totalCount = 0,
|
||||
totalUniqueFoldersInPage,
|
||||
totalUniqueSecretsInPage,
|
||||
totalUniqueSecretImportsInPage,
|
||||
totalUniqueDynamicSecretsInPage
|
||||
} = 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({
|
||||
totalCount,
|
||||
offset,
|
||||
@ -678,7 +697,6 @@ export const OverviewPage = () => {
|
||||
<SecretV2MigrationSection />
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
<Helmet>
|
||||
@ -767,6 +785,19 @@ export const OverviewPage = () => {
|
||||
</Button>
|
||||
</DropdownMenuItem> */}
|
||||
<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
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
@ -1093,6 +1124,17 @@ export const OverviewPage = () => {
|
||||
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) => (
|
||||
<SecretOverviewTableRow
|
||||
isSelected={Boolean(selectedEntries.secret[key])}
|
||||
@ -1116,7 +1158,8 @@ export const OverviewPage = () => {
|
||||
(page * perPage > totalCount ? totalCount % perPage : perPage) -
|
||||
(totalUniqueFoldersInPage || 0) -
|
||||
(totalUniqueDynamicSecretsInPage || 0) -
|
||||
(totalUniqueSecretsInPage || 0),
|
||||
(totalUniqueSecretsInPage || 0) -
|
||||
(totalUniqueSecretImportsInPage || 0),
|
||||
0
|
||||
)}
|
||||
/>
|
||||
@ -1156,6 +1199,7 @@ export const OverviewPage = () => {
|
||||
dynamicSecretCount={totalDynamicSecretCount}
|
||||
secretCount={totalSecretCount}
|
||||
folderCount={totalFolderCount}
|
||||
importCount={totalImportCount}
|
||||
/>
|
||||
}
|
||||
className="rounded-b-md border-t border-solid border-t-mineshaft-600"
|
||||
|
85
frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewImportListView/SecretOverviewImportListView.tsx
Normal file
85
frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewImportListView/SecretOverviewImportListView.tsx
Normal 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>
|
||||
);
|
||||
};
|
1
frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewImportListView/index.tsx
Normal file
1
frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewImportListView/index.tsx
Normal file
@ -0,0 +1 @@
|
||||
export { SecretOverviewImportListView } from "./SecretOverviewImportListView";
|
@ -35,9 +35,6 @@ export const SecretSyncAuditLogsSection = ({ secretSync }: Props) => {
|
||||
{subscription.auditLogs ? (
|
||||
<LogsSection
|
||||
refetchInterval={4000}
|
||||
remappedHeaders={{
|
||||
Metadata: "Sync Status"
|
||||
}}
|
||||
showFilters={false}
|
||||
presets={{
|
||||
eventMetadata: { syncId: secretSync.id },
|
||||
|
@ -3,31 +3,75 @@ import { useNavigate } from "@tanstack/react-router";
|
||||
|
||||
import { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { isInfisicalCloud } from "@app/helpers/platform";
|
||||
import { useSaveIntegrationAccessToken } from "@app/hooks/api";
|
||||
|
||||
export const WindmillAuthorizePage = () => {
|
||||
const navigate = useNavigate();
|
||||
const { mutateAsync } = useSaveIntegrationAccessToken();
|
||||
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const [apiKey, setApiKey] = useState("");
|
||||
const [apiKeyErrorText, setApiKeyErrorText] = useState("");
|
||||
const [apiUrl, setApiUrl] = useState<string | null>(null);
|
||||
const [apiUrlErrorText, setApiUrlErrorText] = useState("");
|
||||
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 () => {
|
||||
try {
|
||||
setApiKeyErrorText("");
|
||||
setApiUrlErrorText("");
|
||||
if (apiKey.length === 0) {
|
||||
setApiKeyErrorText("API Key cannot be blank");
|
||||
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);
|
||||
|
||||
const integrationAuth = await mutateAsync({
|
||||
workspaceId: currentWorkspace.id,
|
||||
integration: "windmill",
|
||||
accessToken: apiKey
|
||||
accessToken: apiKey,
|
||||
url: apiUrl ?? undefined
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
@ -57,6 +101,18 @@ export const WindmillAuthorizePage = () => {
|
||||
>
|
||||
<Input placeholder="" value={apiKey} onChange={(e) => setApiKey(e.target.value)} />
|
||||
</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
|
||||
onClick={handleButtonClick}
|
||||
color="mineshaft"
|
||||
|
@ -67,7 +67,8 @@ export const WindmillConfigurePage = () => {
|
||||
(integrationAuthApp) => integrationAuthApp.name === targetApp
|
||||
)?.appId,
|
||||
sourceEnvironment: selectedSourceEnvironment,
|
||||
secretPath
|
||||
secretPath,
|
||||
url: integrationAuth.url ?? undefined
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
|
Reference in New Issue
Block a user